feat: initial HSAP platform

Huaxu Sentinel Active Safety Platform with embedded algorithm code,
Docker Compose setup, and vendored dataset scaffolds for clone-and-run.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-25 16:59:59 +08:00
commit 7c43b44c57
1619 changed files with 373355 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
# row anchors are a series of pre-defined coordinates in image height to detect lanes
# the row anchors are defined according to the evaluation protocol of CULane and Tusimple
# since our method will resize the image to 288x800 for training, the row anchors are defined with the height of 288
# you can modify these row anchors according to your training image resolution
tusimple_row_anchor = [ 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112,
116, 120, 124, 128, 132, 136, 140, 144, 148, 152, 156, 160, 164,
168, 172, 176, 180, 184, 188, 192, 196, 200, 204, 208, 212, 216,
220, 224, 228, 232, 236, 240, 244, 248, 252, 256, 260, 264, 268,
272, 276, 280, 284]
culane_row_anchor = [121, 131, 141, 150, 160, 170, 180, 189, 199, 209, 219, 228, 238, 248, 258, 267, 277, 287]

View File

@@ -0,0 +1,118 @@
import torch, os
import numpy as np
import torchvision.transforms as transforms
import data.mytransforms as mytransforms
from data.constant import tusimple_row_anchor, culane_row_anchor
from data.dataset import LaneClsDataset, LaneTestDataset
def get_train_loader(batch_size, data_root, griding_num, dataset, use_aux, distributed, num_lanes,
train_list='list/train_gt.txt', num_workers=8):
target_transform = transforms.Compose([
mytransforms.FreeScaleMask((288, 800)),
mytransforms.MaskToTensor(),
])
segment_transform = transforms.Compose([
mytransforms.FreeScaleMask((36, 100)),
mytransforms.MaskToTensor(),
])
img_transform = transforms.Compose([
transforms.Resize((288, 800)),
transforms.ToTensor(),
# transforms.Normalize((0.723, 0.704, 0.726), (0.191, 0.178, 0.186)),
transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)),
])
simu_transform = mytransforms.Compose2([
mytransforms.RandomRotate(6),
mytransforms.RandomUDoffsetLABEL(100),
mytransforms.RandomLROffsetLABEL(200)
])
if dataset == 'CULane':
train_dataset = LaneClsDataset(data_root,
os.path.join(data_root, train_list),
img_transform=img_transform, target_transform=target_transform,
simu_transform =simu_transform,
segment_transform=segment_transform,
row_anchor=culane_row_anchor,
griding_num=griding_num, use_aux=use_aux, num_lanes=num_lanes)
cls_num_per_lane = 18
elif dataset == 'Tusimple':
train_dataset = LaneClsDataset(data_root,
os.path.join(data_root, 'train_val_gt.txt'),
img_transform=img_transform, target_transform=target_transform,
simu_transform =simu_transform,
# simu_transform=None,
griding_num=griding_num,
row_anchor =tusimple_row_anchor,
segment_transform=segment_transform, use_aux=use_aux, num_lanes=num_lanes)
cls_num_per_lane = 56
else:
raise NotImplementedError
if distributed:
sampler = torch.utils.data.distributed.DistributedSampler(train_dataset)
else:
sampler = torch.utils.data.RandomSampler(train_dataset)
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=batch_size, sampler=sampler, num_workers=num_workers,
)
return train_loader, cls_num_per_lane
def get_test_loader(batch_size, data_root, dataset, distributed, test_list=None):
img_transforms = transforms.Compose([
transforms.Resize((288, 800)),
transforms.ToTensor(),
transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)),
])
if dataset == 'CULane':
if test_list is None:
test_list = 'list/test.txt'
test_dataset = LaneTestDataset(data_root, os.path.join(data_root, test_list), img_transform=img_transforms)
cls_num_per_lane = 18
elif dataset == 'Tusimple':
if test_list is None:
test_list = 'list/test_gt.txt'
test_dataset = LaneTestDataset(data_root, os.path.join(data_root, test_list), img_transform=img_transforms)
cls_num_per_lane = 56
if distributed:
sampler = SeqDistributedSampler(test_dataset, shuffle=False)
else:
sampler = torch.utils.data.SequentialSampler(test_dataset)
loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, sampler=sampler, num_workers=8)
return loader
class SeqDistributedSampler(torch.utils.data.distributed.DistributedSampler):
'''
Change the behavior of DistributedSampler to sequential distributed sampling.
The sequential sampling helps the stability of multi-thread testing, which needs multi-thread file io.
Without sequentially sampling, the file io on thread may interfere other threads.
'''
def __init__(self, dataset, num_replicas=None, rank=None, shuffle=False):
super().__init__(dataset, num_replicas, rank, shuffle)
def __iter__(self):
g = torch.Generator()
g.manual_seed(self.epoch)
if self.shuffle:
indices = torch.randperm(len(self.dataset), generator=g).tolist()
else:
indices = list(range(len(self.dataset)))
# add extra samples to make it evenly divisible
indices += indices[:(self.total_size - len(indices))]
assert len(indices) == self.total_size
num_per_rank = int(self.total_size // self.num_replicas)
# sequential sampling
indices = indices[num_per_rank * self.rank : num_per_rank * (self.rank + 1)]
assert len(indices) == self.num_samples
return iter(indices)

View File

@@ -0,0 +1,179 @@
import torch
from PIL import Image
import os
import pdb
import numpy as np
import cv2
from data.mytransforms import find_start_pos
def loader_func(path):
return Image.open(path)
class LaneTestDataset(torch.utils.data.Dataset):
def __init__(self, path, list_path, img_transform=None):
super(LaneTestDataset, self).__init__()
self.path = path
self.img_transform = img_transform
with open(list_path, 'r') as f:
self.list = f.readlines()
self.list = [l[1:] if l[0] == '/' else l for l in self.list] # exclude the incorrect path prefix '/' of CULane
def __getitem__(self, index):
name = self.list[index].split()[0]
img_path = os.path.join(self.path, name)
img = loader_func(img_path).convert('RGB')
if self.img_transform is not None:
img = self.img_transform(img)
return img, name
def __len__(self):
return len(self.list)
class LaneClsDataset(torch.utils.data.Dataset):
def __init__(self, path, list_path, img_transform=None, target_transform=None, simu_transform=None, griding_num=50, load_name=False,
row_anchor=None, use_aux=False, segment_transform=None, num_lanes=2):
super(LaneClsDataset, self).__init__()
self.img_transform = img_transform
self.target_transform = target_transform
self.segment_transform = segment_transform
self.simu_transform = simu_transform
self.path = path
self.griding_num = griding_num
self.load_name = load_name
self.use_aux = use_aux
self.num_lanes = num_lanes
with open(list_path, 'r') as f:
self.list = f.readlines()
self.row_anchor = row_anchor
self.row_anchor.sort()
def _normalize_seg_label(self, label):
"""MUFLD masks use 0,2,3,4,5; aux CE needs contiguous 0..num_lanes."""
arr = np.array(label, dtype=np.uint8)
if arr.max() <= self.num_lanes:
return label
out = np.zeros_like(arr, dtype=np.uint8)
for lane_idx in range(1, self.num_lanes + 1):
out[arr == (lane_idx + 1)] = lane_idx
return Image.fromarray(out, mode='L')
def __getitem__(self, index):
l = self.list[index]
l_info = l.split()
img_name, label_name = l_info[0], l_info[1]
# print(img_name, label_name)
if img_name[0] == '/':
img_name = img_name[1:]
label_name = label_name[1:]
label_path = os.path.join(self.path, label_name)
label = loader_func(label_path).convert('L')
img_path = os.path.join(self.path, img_name)
img = loader_func(img_path).convert('RGB')
# print('---------------', img_path, label_path)
if self.simu_transform is not None:
img, label = self.simu_transform(img, label)
# print(',,,,,,,,,,,,,,,,', img.size, label.size)
lane_pts = self._get_index(label)
# get the coordinates of lanes at row anchors
w, h = img.size
cls_label = self._grid_pts(lane_pts, self.griding_num, w)
# make the coordinates to classification label
if self.use_aux:
assert self.segment_transform is not None
seg_label = self.segment_transform(self._normalize_seg_label(label))
if self.img_transform is not None:
img = self.img_transform(img)
if self.use_aux:
return img, cls_label, seg_label
if self.load_name:
return img, cls_label, img_name
return img, cls_label
def __len__(self):
return len(self.list)
def _grid_pts(self, pts, num_cols, w):
# pts : numlane,n,2
num_lane, n, n2 = pts.shape
col_sample = np.linspace(0, w - 1, num_cols)
assert n2 == 2
to_pts = np.zeros((n, num_lane))
for i in range(num_lane):
pti = pts[i, :, 1]
to_pts[:, i] = np.asarray(
[int(pt // (col_sample[1] - col_sample[0])) if pt != -1 else num_cols for pt in pti])
return to_pts.astype(int)
def _get_index(self, label):
w, h = label.size
if h != 288:
scale_f = lambda x : int((x * 1.0/288) * h)
sample_tmp = list(map(scale_f,self.row_anchor))
all_idx = np.zeros((self.num_lanes,len(sample_tmp),2))
for i,r in enumerate(sample_tmp):
label_r = np.asarray(label)[int(round(r))]
for lane_idx in range(1, self.num_lanes + 1):
if np.max(label_r) == 2:
pos = np.where(label_r == lane_idx)[0]
else:
pos = np.where(label_r == (lane_idx + 1))[0]
if len(pos) == 0:
all_idx[lane_idx - 1, i, 0] = r
all_idx[lane_idx - 1, i, 1] = -1
continue
pos = np.mean(pos)
all_idx[lane_idx - 1, i, 0] = r
all_idx[lane_idx - 1, i, 1] = pos
# data augmentation: extend the lane to the boundary of image
all_idx_cp = all_idx.copy()
for i in range(self.num_lanes):
if np.all(all_idx_cp[i,:,1] == -1):
continue
# if there is no lane
valid = all_idx_cp[i,:,1] != -1
# get all valid lane points' index
valid_idx = all_idx_cp[i,valid,:]
# get all valid lane points
if valid_idx[-1,0] == all_idx_cp[0,-1,0]:
# if the last valid lane point's y-coordinate is already the last y-coordinate of all rows
# this means this lane has reached the bottom boundary of the image
# so we skip
continue
if len(valid_idx) < 6:
continue
# if the lane is too short to extend
valid_idx_half = valid_idx[len(valid_idx) // 2:,:]
p = np.polyfit(valid_idx_half[:,0], valid_idx_half[:,1],deg = 1)
start_line = valid_idx_half[-1,0]
pos = find_start_pos(all_idx_cp[i,:,0],start_line) + 1
fitted = np.polyval(p, all_idx_cp[i, pos:, 0])
fitted = np.array([-1 if y < 0 or y > w-1 else y for y in fitted])
assert np.all(all_idx_cp[i,pos:,1] == -1)
all_idx_cp[i,pos:,1] = fitted
if -1 in all_idx[:, :, 0]:
pdb.set_trace()
return all_idx_cp

View File

@@ -0,0 +1,187 @@
import numbers
import random
import numpy as np
from PIL import Image, ImageOps, ImageFilter
#from config import cfg
import torch
import pdb
import cv2
# ===============================img tranforms============================
class Compose2(object):
def __init__(self, transforms):
self.transforms = transforms
def __call__(self, img, mask, bbx=None):
if bbx is None:
for t in self.transforms:
# print(t)
# print('\\: ', img.size, mask.size)
img, mask = t(img, mask)
# print('//: ', img.size, mask.size)
return img, mask
for t in self.transforms:
img, mask, bbx = t(img, mask, bbx)
return img, mask, bbx
class FreeScale(object):
def __init__(self, size):
self.size = size # (h, w)
def __call__(self, img, mask):
return img.resize((self.size[1], self.size[0]), Image.BILINEAR), mask.resize((self.size[1], self.size[0]), Image.NEAREST)
class FreeScaleMask(object):
def __init__(self,size):
self.size = size
def __call__(self,mask):
return mask.resize((self.size[1], self.size[0]), Image.NEAREST)
class Scale(object):
def __init__(self, size):
self.size = size
def __call__(self, img, mask):
# if img.size != mask.size:
# print(img.size)
# print(mask.size)
assert img.size == mask.size
w, h = img.size
if (w <= h and w == self.size) or (h <= w and h == self.size):
return img, mask
if w < h:
ow = self.size
oh = int(self.size * h / w)
return img.resize((ow, oh), Image.BILINEAR), mask.resize((ow, oh), Image.NEAREST)
else:
oh = self.size
ow = int(self.size * w / h)
return img.resize((ow, oh), Image.BILINEAR), mask.resize((ow, oh), Image.NEAREST)
class RandomRotate(object):
"""Crops the given PIL.Image at a random location to have a region of
the given size. size can be a tuple (target_height, target_width)
or an integer, in which case the target will be of a square shape (size, size)
"""
def __init__(self, angle):
self.angle = angle
def __call__(self, image, label):
# w, h = image.size
# if w != 1280 or h != 720:
# image = image.resize(image, (1280, 720))
# print(w, h)
assert label is None or image.size == label.size
angle = random.randint(0, self.angle * 2) - self.angle
label = label.rotate(angle, resample=Image.NEAREST)
image = image.rotate(angle, resample=Image.BILINEAR)
return image, label
# ===============================label tranforms============================
class DeNormalize(object):
def __init__(self, mean, std):
self.mean = mean
self.std = std
def __call__(self, tensor):
for t, m, s in zip(tensor, self.mean, self.std):
t.mul_(s).add_(m)
return tensor
class MaskToTensor(object):
def __call__(self, img):
return torch.from_numpy(np.array(img, dtype=np.int32)).long()
def find_start_pos(row_sample,start_line):
# row_sample = row_sample.sort()
# for i,r in enumerate(row_sample):
# if r >= start_line:
# return i
l,r = 0,len(row_sample)-1
while True:
mid = int((l+r)/2)
if r - l == 1:
return r
if row_sample[mid] < start_line:
l = mid
if row_sample[mid] > start_line:
r = mid
if row_sample[mid] == start_line:
return mid
class RandomLROffsetLABEL(object):
def __init__(self,max_offset):
self.max_offset = max_offset
def __call__(self,img,label):
offset = np.random.randint(-self.max_offset,self.max_offset)
w, h = img.size
# print('max_offset:', self.max_offset, 'ro_offset: ', offset)
img = np.array(img)
if offset > 0:
img[:,offset:,:] = img[:,0:w-offset,:]
img[:,:offset,:] = 0
if offset < 0:
real_offset = -offset
img[:,0:w-real_offset,:] = img[:,real_offset:,:]
img[:,w-real_offset:,:] = 0
label = np.array(label)
if offset > 0:
label[:,offset:] = label[:,0:w-offset]
label[:,:offset] = 0
if offset < 0:
offset = -offset
label[:,0:w-offset] = label[:,offset:]
label[:,w-offset:] = 0
return Image.fromarray(img),Image.fromarray(label)
class RandomUDoffsetLABEL(object):
def __init__(self,max_offset):
self.max_offset = max_offset
def __call__(self,img,label):
offset = np.random.randint(-self.max_offset,self.max_offset)
# offset = np.random.randint(0, self.max_offset)
# offset = 17
# print('max_offset:', self.max_offset, 'do_offset: ', offset)
w, h = img.size
# if w != 1280 or h != 720:
# img = img.resize(img, (1280, 720))
# print(w, h)
img = np.array(img)
if offset > 0:
# print('dim > 0:', h - offset, offset)
# print(img[offset:,:,:].shape, img[0:h-offset,:,:].shape)
img[offset:,:,:] = img[0:h-offset,:,:]
img[:offset,:,:] = 0
if offset < 0:
real_offset = -offset
# print('dim < 0:', h - real_offset, real_offset)
img[0:h-real_offset,:,:] = img[real_offset:,:,:]
img[h-real_offset:,:,:] = 0
label = np.array(label)
if offset > 0:
# print('dla > 0:', h - offset, offset)
# # print(label[0:h-offset,:].shape, label[offset:,:].shape)
# if label[0:h-offset,:].shape == label[offset:,:].shape:
label[offset:,:] = label[0:h-offset,:]
label[:offset,:] = 0
# else:
# print(label[0:h - offset, :].shape, label[offset:, :].shape, '/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/')
if offset < 0:
offset = -offset
# print('dla < 0:', h - offset, offset)
label[0:h-offset,:] = label[offset:,:]
label[h-offset:,:] = 0
return Image.fromarray(img),Image.fromarray(label)