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,98 @@
import torch
import torch.nn.modules
import torchvision
from model.vovnet import VOVNET_ALIASES, STAGE_SPECS
RESNET_BACKBONES = ['18', '34', '50', '101', '152', '50next', '101next', '50wide', '101wide']
VOVMNET_BACKBONES = list(VOVNET_ALIASES.keys())
SUPPORTED_BACKBONES = RESNET_BACKBONES + VOVMNET_BACKBONES + ['vgg']
def is_vovnet(backbone):
return str(backbone) in VOVNET_ALIASES
def get_backbone_spec(backbone):
"""Channel layout for parsingNet pool / aux heads."""
backbone = str(backbone)
if is_vovnet(backbone):
from model.vovnet import VOVNET_ALIASES as _aliases
variant = _aliases[backbone]
ch = STAGE_SPECS[variant]['stage_out_ch']
return {
'pool_in': ch[3],
'aux': (ch[1], ch[2], ch[3]),
'family': 'vov',
}
if backbone in ['18', '34']:
return {'pool_in': 512, 'aux': (128, 256, 512), 'family': 'resnet'}
if backbone == 'vgg':
return {'pool_in': 512, 'aux': (128, 256, 512), 'family': 'vgg'}
return {'pool_in': 2048, 'aux': (512, 1024, 2048), 'family': 'resnet'}
def build_backbone(backbone, pretrained=False):
backbone = str(backbone)
if is_vovnet(backbone):
from model.vovnet import vovnet
return vovnet(backbone, pretrained=pretrained)
if backbone == 'vgg':
return vgg16bn(pretrained=pretrained)
return resnet(backbone, pretrained=pretrained)
class vgg16bn(torch.nn.Module):
def __init__(self, pretrained=False):
super(vgg16bn, self).__init__()
model = list(torchvision.models.vgg16_bn(pretrained=pretrained).features.children())
model = model[:33] + model[34:43]
self.model = torch.nn.Sequential(*model)
def forward(self, x):
return self.model(x)
class resnet(torch.nn.Module):
def __init__(self, layers, pretrained=False):
super(resnet, self).__init__()
if layers == '18':
model = torchvision.models.resnet18(pretrained=pretrained)
elif layers == '34':
model = torchvision.models.resnet34(pretrained=pretrained)
elif layers == '50':
model = torchvision.models.resnet50(pretrained=pretrained)
elif layers == '101':
model = torchvision.models.resnet101(pretrained=pretrained)
elif layers == '152':
model = torchvision.models.resnet152(pretrained=pretrained)
elif layers == '50next':
model = torchvision.models.resnext50_32x4d(pretrained=pretrained)
elif layers == '101next':
model = torchvision.models.resnext101_32x8d(pretrained=pretrained)
elif layers == '50wide':
model = torchvision.models.wide_resnet50_2(pretrained=pretrained)
elif layers == '101wide':
model = torchvision.models.wide_resnet101_2(pretrained=pretrained)
else:
raise NotImplementedError(f'Unknown ResNet backbone: {layers}')
self.conv1 = model.conv1
self.bn1 = model.bn1
self.relu = model.relu
self.maxpool = model.maxpool
self.layer1 = model.layer1
self.layer2 = model.layer2
self.layer3 = model.layer3
self.layer4 = model.layer4
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x2 = self.layer2(x)
x3 = self.layer3(x2)
x4 = self.layer4(x3)
return x2, x3, x4

View File

@@ -0,0 +1,126 @@
import torch
from model.backbone import build_backbone, get_backbone_spec
import numpy as np
class conv_bn_relu(torch.nn.Module):
def __init__(self,in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,bias=False):
super(conv_bn_relu,self).__init__()
self.conv = torch.nn.Conv2d(in_channels,out_channels, kernel_size,
stride = stride, padding = padding, dilation = dilation,bias = bias)
self.bn = torch.nn.BatchNorm2d(out_channels)
self.relu = torch.nn.ReLU()
def forward(self,x):
x = self.conv(x)
x = self.bn(x)
x = self.relu(x)
return x
class parsingNet(torch.nn.Module):
def __init__(self, size=(288, 800), pretrained=True, backbone='50', cls_dim=(37, 10, 4), use_aux=False):
super(parsingNet, self).__init__()
self.size = size
self.w = size[0]
self.h = size[1]
self.cls_dim = cls_dim # (num_gridding, num_cls_per_lane, num_of_lanes)
# num_cls_per_lane is the number of row anchors
self.use_aux = use_aux
self.total_dim = np.prod(cls_dim)
# input : nchw,
# output: (w+1) * sample_rows * 4
spec = get_backbone_spec(backbone)
c2, c3, c4 = spec['aux']
pool_in = spec['pool_in']
self.model = build_backbone(backbone, pretrained=pretrained)
if self.use_aux:
self.aux_header2 = torch.nn.Sequential(
conv_bn_relu(c2, 128, kernel_size=3, stride=1, padding=1),
conv_bn_relu(128, 128, 3, padding=1),
conv_bn_relu(128, 128, 3, padding=1),
conv_bn_relu(128, 128, 3, padding=1),
)
self.aux_header3 = torch.nn.Sequential(
conv_bn_relu(c3, 128, kernel_size=3, stride=1, padding=1),
conv_bn_relu(128, 128, 3, padding=1),
conv_bn_relu(128, 128, 3, padding=1),
)
self.aux_header4 = torch.nn.Sequential(
conv_bn_relu(c4, 128, kernel_size=3, stride=1, padding=1),
conv_bn_relu(128, 128, 3, padding=1),
)
self.aux_combine = torch.nn.Sequential(
conv_bn_relu(384, 256, 3,padding=2,dilation=2),
conv_bn_relu(256, 128, 3,padding=2,dilation=2),
conv_bn_relu(128, 128, 3,padding=2,dilation=2),
conv_bn_relu(128, 128, 3,padding=4,dilation=4),
torch.nn.Conv2d(128, cls_dim[-1] + 1, 1)
# output : n, num_of_lanes+1, h, w
)
initialize_weights(self.aux_header2,self.aux_header3,self.aux_header4,self.aux_combine)
self.cls = torch.nn.Sequential(
torch.nn.Linear(1800, 2048),
torch.nn.ReLU(),
torch.nn.Linear(2048, self.total_dim),
)
self.pool = torch.nn.Conv2d(pool_in, 8, 1)
# 1/32,2048 channel
# 288,800 -> 9,40,2048
# (w+1) * sample_rows * 4
# 37 * 10 * 4
initialize_weights(self.cls)
def forward(self, x):
# n c h w - > n 2048 sh sw
# -> n 2048
x2,x3,fea = self.model(x)
if self.use_aux:
x2 = self.aux_header2(x2)
x3 = self.aux_header3(x3)
x3 = torch.nn.functional.interpolate(x3,scale_factor = 2,mode='bilinear')
x4 = self.aux_header4(fea)
x4 = torch.nn.functional.interpolate(x4,scale_factor = 4,mode='bilinear')
aux_seg = torch.cat([x2,x3,x4],dim=1)
aux_seg = self.aux_combine(aux_seg)
else:
aux_seg = None
fea = self.pool(fea).view(-1, 1800)
group_cls = self.cls(fea).view(-1, *self.cls_dim)
if self.use_aux:
return group_cls, aux_seg
return group_cls
def initialize_weights(*models):
for model in models:
real_init_weights(model)
def real_init_weights(m):
if isinstance(m, list):
for mini_m in m:
real_init_weights(mini_m)
else:
if isinstance(m, torch.nn.Conv2d):
torch.nn.init.kaiming_normal_(m.weight, nonlinearity='relu')
if m.bias is not None:
torch.nn.init.constant_(m.bias, 0)
elif isinstance(m, torch.nn.Linear):
m.weight.data.normal_(0.0, std=0.01)
elif isinstance(m, torch.nn.BatchNorm2d):
torch.nn.init.constant_(m.weight, 1)
torch.nn.init.constant_(m.bias, 0)
elif isinstance(m,torch.nn.Module):
for mini_m in m.children():
real_init_weights(mini_m)
else:
print('unkonwn module', m)

View File

@@ -0,0 +1,120 @@
import torch
from model.backbone import resnet
import numpy as np
class conv_bn_relu(torch.nn.Module):
def __init__(self,in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,bias=False):
super(conv_bn_relu,self).__init__()
self.conv = torch.nn.Conv2d(in_channels,out_channels, kernel_size,
stride = stride, padding = padding, dilation = dilation,bias = bias)
self.bn = torch.nn.BatchNorm2d(out_channels)
self.relu = torch.nn.ReLU()
def forward(self,x):
x = self.conv(x)
x = self.bn(x)
x = self.relu(x)
return x
class parsingNet(torch.nn.Module):
def __init__(self, size=(288, 800), pretrained=True, backbone='50', cls_dim=(37, 10, 4), use_aux=False):
super(parsingNet, self).__init__()
self.size = size
self.w = size[0]
self.h = size[1]
self.cls_dim = cls_dim # (num_gridding, num_cls_per_lane, num_of_lanes)
# num_cls_per_lane is the number of row anchors
self.use_aux = use_aux
self.total_dim = np.prod(cls_dim)
# input : nchw,
# output: (w+1) * sample_rows * 4
self.model = resnet(backbone, pretrained=pretrained)
if self.use_aux:
self.aux_header2 = torch.nn.Sequential(
conv_bn_relu(128, 128, kernel_size=3, stride=1, padding=1) if backbone in ['34','18'] else conv_bn_relu(512, 128, kernel_size=3, stride=1, padding=1),
conv_bn_relu(128,128,3,padding=1),
conv_bn_relu(128,128,3,padding=1),
conv_bn_relu(128,128,3,padding=1),
)
self.aux_header3 = torch.nn.Sequential(
conv_bn_relu(256, 128, kernel_size=3, stride=1, padding=1) if backbone in ['34','18'] else conv_bn_relu(1024, 128, kernel_size=3, stride=1, padding=1),
conv_bn_relu(128,128,3,padding=1),
conv_bn_relu(128,128,3,padding=1),
)
self.aux_header4 = torch.nn.Sequential(
conv_bn_relu(512, 128, kernel_size=3, stride=1, padding=1) if backbone in ['34','18'] else conv_bn_relu(2048, 128, kernel_size=3, stride=1, padding=1),
conv_bn_relu(128,128,3,padding=1),
)
self.aux_combine = torch.nn.Sequential(
conv_bn_relu(384, 256, 3,padding=2,dilation=2),
conv_bn_relu(256, 128, 3,padding=2,dilation=2),
conv_bn_relu(128, 128, 3,padding=2,dilation=2),
conv_bn_relu(128, 128, 3,padding=4,dilation=4),
torch.nn.Conv2d(128, cls_dim[-1] + 1,1)
# output : n, num_of_lanes+1, h, w
)
initialize_weights(self.aux_header2,self.aux_header3,self.aux_header4,self.aux_combine)
self.cls = torch.nn.Sequential(
torch.nn.Linear(1800, 2048),
torch.nn.ReLU(),
torch.nn.Linear(2048, self.total_dim),
)
self.pool = torch.nn.Conv2d(512,8,1) if backbone in ['34','18'] else torch.nn.Conv2d(2048,8,1)
# 1/32,2048 channel
# 288,800 -> 9,40,2048
# (w+1) * sample_rows * 4
# 37 * 10 * 4
initialize_weights(self.cls)
def forward(self, x):
# n c h w - > n 2048 sh sw
# -> n 2048
x2,x3,fea = self.model(x)
if self.use_aux:
x2 = self.aux_header2(x2)
x3 = self.aux_header3(x3)
x3 = torch.nn.functional.interpolate(x3,scale_factor = 2,mode='bilinear')
x4 = self.aux_header4(fea)
x4 = torch.nn.functional.interpolate(x4,scale_factor = 4,mode='bilinear')
aux_seg = torch.cat([x2,x3,x4],dim=1)
aux_seg = self.aux_combine(aux_seg)
else:
aux_seg = None
fea = self.pool(fea).view(-1, 1800)
group_cls = self.cls(fea).view(-1, *self.cls_dim)
if self.use_aux:
return group_cls, aux_seg
return group_cls
def initialize_weights(*models):
for model in models:
real_init_weights(model)
def real_init_weights(m):
if isinstance(m, list):
for mini_m in m:
real_init_weights(mini_m)
else:
if isinstance(m, torch.nn.Conv2d):
torch.nn.init.kaiming_normal_(m.weight, nonlinearity='relu')
if m.bias is not None:
torch.nn.init.constant_(m.bias, 0)
elif isinstance(m, torch.nn.Linear):
m.weight.data.normal_(0.0, std=0.01)
elif isinstance(m, torch.nn.BatchNorm2d):
torch.nn.init.constant_(m.weight, 1)
torch.nn.init.constant_(m.bias, 0)
elif isinstance(m,torch.nn.Module):
for mini_m in m.children():
real_init_weights(mini_m)
else:
print('unkonwn module', m)

View File

@@ -0,0 +1,296 @@
# VoVNet backbone (OSA + eSE), adapted from vovnet-detectron2 (no detectron2 dependency).
# Reference: BK2/archive/vovnet-detectron2-master/vovnet/vovnet.py
from collections import OrderedDict
import torch
import torch.nn as nn
import torch.nn.functional as F
VoVNet19_slim_dw_eSE = {
'stem': [64, 64, 64],
'stage_conv_ch': [64, 80, 96, 112],
'stage_out_ch': [112, 256, 384, 512],
'layer_per_block': 3,
'block_per_stage': [1, 1, 1, 1],
'eSE': True,
'dw': True,
}
VoVNet19_dw_eSE = {
'stem': [64, 64, 64],
'stage_conv_ch': [128, 160, 192, 224],
'stage_out_ch': [256, 512, 768, 1024],
'layer_per_block': 3,
'block_per_stage': [1, 1, 1, 1],
'eSE': True,
'dw': True,
}
VoVNet19_slim_eSE = {
'stem': [64, 64, 128],
'stage_conv_ch': [64, 80, 96, 112],
'stage_out_ch': [112, 256, 384, 512],
'layer_per_block': 3,
'block_per_stage': [1, 1, 1, 1],
'eSE': True,
'dw': False,
}
VoVNet19_eSE = {
'stem': [64, 64, 128],
'stage_conv_ch': [128, 160, 192, 224],
'stage_out_ch': [256, 512, 768, 1024],
'layer_per_block': 3,
'block_per_stage': [1, 1, 1, 1],
'eSE': True,
'dw': False,
}
VoVNet39_eSE = {
'stem': [64, 64, 128],
'stage_conv_ch': [128, 160, 192, 224],
'stage_out_ch': [256, 512, 768, 1024],
'layer_per_block': 5,
'block_per_stage': [1, 1, 2, 2],
'eSE': True,
'dw': False,
}
VoVNet57_eSE = {
'stem': [64, 64, 128],
'stage_conv_ch': [128, 160, 192, 224],
'stage_out_ch': [256, 512, 768, 1024],
'layer_per_block': 5,
'block_per_stage': [1, 1, 4, 3],
'eSE': True,
'dw': False,
}
VoVNet99_eSE = {
'stem': [64, 64, 128],
'stage_conv_ch': [128, 160, 192, 224],
'stage_out_ch': [256, 512, 768, 1024],
'layer_per_block': 5,
'block_per_stage': [1, 3, 9, 3],
'eSE': True,
'dw': False,
}
STAGE_SPECS = {
'V-19-slim-dw-eSE': VoVNet19_slim_dw_eSE,
'V-19-dw-eSE': VoVNet19_dw_eSE,
'V-19-slim-eSE': VoVNet19_slim_eSE,
'V-19-eSE': VoVNet19_eSE,
'V-39-eSE': VoVNet39_eSE,
'V-57-eSE': VoVNet57_eSE,
'V-99-eSE': VoVNet99_eSE,
}
# Short names used in UFLD configs (backbone='vov19slim', ...)
VOVNET_ALIASES = {
'vov19slim_dw': 'V-19-slim-dw-eSE',
'vov19_dw': 'V-19-dw-eSE',
'vov19slim': 'V-19-slim-eSE',
'vov19': 'V-19-eSE',
'vov39': 'V-39-eSE',
'vov57': 'V-57-eSE',
'vov99': 'V-99-eSE',
}
def _bn(ch):
return nn.BatchNorm2d(ch)
def dw_conv3x3(in_channels, out_channels, module_name, postfix, stride=1, kernel_size=3, padding=1):
return [
(f'{module_name}_{postfix}/dw', nn.Conv2d(
in_channels, out_channels, kernel_size=kernel_size, stride=stride,
padding=padding, groups=out_channels, bias=False)),
(f'{module_name}_{postfix}/pw', nn.Conv2d(
in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False)),
(f'{module_name}_{postfix}/bn', _bn(out_channels)),
(f'{module_name}_{postfix}/relu', nn.ReLU(inplace=True)),
]
def conv3x3(in_channels, out_channels, module_name, postfix, stride=1, groups=1,
kernel_size=3, padding=1):
return [
(f'{module_name}_{postfix}/conv', nn.Conv2d(
in_channels, out_channels, kernel_size=kernel_size, stride=stride,
padding=padding, groups=groups, bias=False)),
(f'{module_name}_{postfix}/bn', _bn(out_channels)),
(f'{module_name}_{postfix}/relu', nn.ReLU(inplace=True)),
]
def conv1x1(in_channels, out_channels, module_name, postfix, stride=1, groups=1,
kernel_size=1, padding=0):
return [
(f'{module_name}_{postfix}/conv', nn.Conv2d(
in_channels, out_channels, kernel_size=kernel_size, stride=stride,
padding=padding, groups=groups, bias=False)),
(f'{module_name}_{postfix}/bn', _bn(out_channels)),
(f'{module_name}_{postfix}/relu', nn.ReLU(inplace=True)),
]
class Hsigmoid(nn.Module):
def __init__(self, inplace=True):
super().__init__()
self.inplace = inplace
def forward(self, x):
return F.relu6(x + 3.0, inplace=self.inplace) / 6.0
class eSEModule(nn.Module):
def __init__(self, channel):
super().__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Conv2d(channel, channel, kernel_size=1, padding=0)
self.hsigmoid = Hsigmoid()
def forward(self, x):
return x * self.hsigmoid(self.fc(self.avg_pool(x)))
class _OSA_module(nn.Module):
def __init__(self, in_ch, stage_ch, concat_ch, layer_per_block, module_name,
identity=False, depthwise=False):
super().__init__()
self.identity = identity
self.depthwise = depthwise
self.is_reduced = False
self.layers = nn.ModuleList()
in_channel = in_ch
if self.depthwise and in_channel != stage_ch:
self.is_reduced = True
self.conv_reduction = nn.Sequential(
OrderedDict(conv1x1(in_channel, stage_ch, f'{module_name}_reduction', '0')))
for i in range(layer_per_block):
if self.depthwise:
self.layers.append(nn.Sequential(OrderedDict(
dw_conv3x3(stage_ch, stage_ch, module_name, str(i)))))
else:
self.layers.append(nn.Sequential(OrderedDict(
conv3x3(in_channel, stage_ch, module_name, str(i)))))
in_channel = stage_ch
in_channel = in_ch + layer_per_block * stage_ch
self.concat = nn.Sequential(OrderedDict(
conv1x1(in_channel, concat_ch, module_name, 'concat')))
self.ese = eSEModule(concat_ch)
def forward(self, x):
identity_feat = x
output = [x]
if self.depthwise and self.is_reduced:
x = self.conv_reduction(x)
for layer in self.layers:
x = layer(x)
output.append(x)
x = torch.cat(output, dim=1)
xt = self.ese(self.concat(x))
if self.identity:
xt = xt + identity_feat
return xt
class _OSA_stage(nn.Sequential):
def __init__(self, in_ch, stage_ch, concat_ch, block_per_stage, layer_per_block,
stage_num, depthwise=False):
super().__init__()
if stage_num != 2:
self.add_module('Pooling', nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True))
module_name = f'OSA{stage_num}_1'
self.add_module(module_name, _OSA_module(
in_ch, stage_ch, concat_ch, layer_per_block, module_name, depthwise=depthwise))
for i in range(block_per_stage - 1):
module_name = f'OSA{stage_num}_{i + 2}'
self.add_module(module_name, _OSA_module(
concat_ch, stage_ch, concat_ch, layer_per_block, module_name,
identity=True, depthwise=depthwise))
class VoVNetBody(nn.Module):
"""VoVNet stages; forward returns (stage3, stage4, stage5) at strides 8/16/32."""
def __init__(self, variant='V-19-slim-eSE', input_ch=3):
super().__init__()
if variant in VOVNET_ALIASES:
variant = VOVNET_ALIASES[variant]
if variant not in STAGE_SPECS:
raise KeyError(f'Unknown VoVNet variant {variant!r}, choose from {list(STAGE_SPECS)}')
self.variant = variant
spec = STAGE_SPECS[variant]
stem_ch = spec['stem']
config_stage_ch = spec['stage_conv_ch']
config_concat_ch = spec['stage_out_ch']
block_per_stage = spec['block_per_stage']
layer_per_block = spec['layer_per_block']
depthwise = spec['dw']
conv_type = dw_conv3x3 if depthwise else conv3x3
stem = conv3x3(input_ch, stem_ch[0], 'stem', '1', stride=2)
stem += conv_type(stem_ch[0], stem_ch[1], 'stem', '2', stride=1)
stem += conv_type(stem_ch[1], stem_ch[2], 'stem', '3', stride=2)
self.stem = nn.Sequential(OrderedDict(stem))
in_ch_list = [stem_ch[2]] + config_concat_ch[:-1]
self.stage2 = _OSA_stage(
in_ch_list[0], config_stage_ch[0], config_concat_ch[0],
block_per_stage[0], layer_per_block, 2, depthwise=depthwise)
self.stage3 = _OSA_stage(
in_ch_list[1], config_stage_ch[1], config_concat_ch[1],
block_per_stage[1], layer_per_block, 3, depthwise=depthwise)
self.stage4 = _OSA_stage(
in_ch_list[2], config_stage_ch[2], config_concat_ch[2],
block_per_stage[2], layer_per_block, 4, depthwise=depthwise)
self.stage5 = _OSA_stage(
in_ch_list[3], config_stage_ch[3], config_concat_ch[3],
block_per_stage[3], layer_per_block, 5, depthwise=depthwise)
self.out_channels = {
'c3': config_concat_ch[1],
'c4': config_concat_ch[2],
'c5': config_concat_ch[3],
}
self._initialize_weights()
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight)
def forward(self, x):
x = self.stem(x)
x = self.stage2(x)
c3 = self.stage3(x)
c4 = self.stage4(c3)
c5 = self.stage5(c4)
return c3, c4, c5
class vovnet(nn.Module):
"""UFLD-compatible wrapper (same interface as resnet: x2, x3, x4)."""
def __init__(self, variant='vov19slim', pretrained=False):
super().__init__()
if pretrained:
import warnings
warnings.warn(
'VoVNet has no torchvision pretrained weights in UFLD; '
'train from scratch or load a custom checkpoint.',
UserWarning,
stacklevel=2,
)
key = variant if variant in VOVNET_ALIASES else variant
self.body = VoVNetBody(key)
self.variant = self.body.variant
def forward(self, x):
return self.body(x)