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:
155
algorithms/lane_ufld/code/CLRNet-main/tools/export_onnx.py
Normal file
155
algorithms/lane_ufld/code/CLRNet-main/tools/export_onnx.py
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export CLRNet to ONNX (bilinear_grid_sample, no GridSample)."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
|
||||
def _stub_nms_cuda_ext():
|
||||
"""Export does not need CUDA NMS; stub so clrnet.ops imports on CPU-only hosts."""
|
||||
if 'clrnet.ops.nms_impl' in sys.modules:
|
||||
return
|
||||
stub = types.ModuleType('clrnet.ops.nms_impl')
|
||||
|
||||
def _nms_forward(boxes, scores, overlap, top_k):
|
||||
raise NotImplementedError('NMS runs on CPU after RKNN inference, not in exported graph')
|
||||
|
||||
stub.nms_forward = _nms_forward
|
||||
sys.modules['clrnet.ops.nms_impl'] = stub
|
||||
|
||||
|
||||
_stub_nms_cuda_ext()
|
||||
|
||||
from clrnet.utils.config import Config # noqa: E402
|
||||
from clrnet.models.registry import build_net # noqa: E402
|
||||
|
||||
|
||||
class CLRNetOnnxWrapper(nn.Module):
|
||||
def __init__(self, net):
|
||||
super().__init__()
|
||||
self.net = net
|
||||
|
||||
def forward(self, img):
|
||||
# Detector returns (B, num_priors, 77) in eval mode
|
||||
return self.net({'img': img})
|
||||
|
||||
|
||||
def _remap_clrernet_keys(key):
|
||||
"""Map CLRerNet (mmdet) checkpoint keys to Turoad CLRNet-main module names."""
|
||||
key = key.replace('bbox_head.', 'heads.')
|
||||
key = key.replace('heads.sample_x_indices', 'heads.sample_x_indexs')
|
||||
key = key.replace('heads.anchor_generator.prior_embeddings',
|
||||
'heads.prior_embeddings')
|
||||
key = key.replace('heads.attention.attention.', 'heads.roi_gather.')
|
||||
key = key.replace('heads.attention.', 'heads.roi_gather.')
|
||||
return key
|
||||
|
||||
|
||||
def load_weights(net, path):
|
||||
ckpt = torch.load(path, map_location='cpu')
|
||||
if isinstance(ckpt, dict):
|
||||
if 'net' in ckpt:
|
||||
state = ckpt['net']
|
||||
elif 'state_dict' in ckpt:
|
||||
state = ckpt['state_dict']
|
||||
else:
|
||||
state = ckpt
|
||||
else:
|
||||
state = ckpt
|
||||
remapped = {_remap_clrernet_keys(k): v for k, v in state.items()}
|
||||
missing, unexpected = net.load_state_dict(remapped, strict=False)
|
||||
print('load_state_dict: missing', len(missing), 'unexpected', len(unexpected))
|
||||
if missing:
|
||||
print(' missing sample:', missing[:8])
|
||||
if unexpected:
|
||||
print(' unexpected sample:', unexpected[:8])
|
||||
|
||||
|
||||
def compare_outputs(wrapper, dummy, onnx_path):
|
||||
try:
|
||||
import onnxruntime as ort
|
||||
except ImportError:
|
||||
print('onnxruntime not installed, skip numeric check')
|
||||
return
|
||||
|
||||
wrapper.eval()
|
||||
with torch.no_grad():
|
||||
pt_out = wrapper(dummy).numpy()
|
||||
|
||||
sess = ort.InferenceSession(onnx_path, providers=['CPUExecutionProvider'])
|
||||
ort_out = sess.run(None, {'img': dummy.numpy()})[0]
|
||||
diff = np.abs(pt_out - ort_out).max()
|
||||
print(f'PyTorch vs ONNX max diff: {diff:.6f}')
|
||||
print(f'output shape: {pt_out.shape}')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--config', default='configs/clrnet/clr_dla34_culane.py')
|
||||
parser.add_argument('--checkpoint', default='models/clrernet_culane_dla34.pth')
|
||||
parser.add_argument('--output', default='models/clrernet_culane_dla34.onnx')
|
||||
parser.add_argument('--opset', type=int, default=11)
|
||||
parser.add_argument('--height', type=int, default=None)
|
||||
parser.add_argument('--width', type=int, default=None)
|
||||
parser.add_argument('--check', action='store_true', help='run onnxruntime compare')
|
||||
args = parser.parse_args()
|
||||
|
||||
cfg = Config.fromfile(os.path.join(ROOT, args.config))
|
||||
h = args.height or cfg.img_h
|
||||
w = args.width or cfg.img_w
|
||||
|
||||
# Avoid downloading ImageNet weights; we load the CLRNet checkpoint below.
|
||||
if cfg.get('backbone', None) is not None:
|
||||
cfg.backbone.pretrained = False
|
||||
|
||||
net = build_net(cfg)
|
||||
load_weights(net, os.path.join(ROOT, args.checkpoint))
|
||||
net.eval()
|
||||
|
||||
wrapper = CLRNetOnnxWrapper(net)
|
||||
dummy = torch.randn(1, 3, h, w)
|
||||
out_path = os.path.join(ROOT, args.output)
|
||||
os.makedirs(os.path.dirname(out_path) or '.', exist_ok=True)
|
||||
|
||||
print(f'export ONNX: {out_path}')
|
||||
print(f'input: (1, 3, {h}, {w})')
|
||||
with torch.no_grad():
|
||||
test_out = wrapper(dummy)
|
||||
print(f'output: {tuple(test_out.shape)}')
|
||||
|
||||
torch.onnx.export(
|
||||
wrapper,
|
||||
dummy,
|
||||
out_path,
|
||||
opset_version=args.opset,
|
||||
input_names=['img'],
|
||||
output_names=['predictions'],
|
||||
dynamic_axes={'img': {0: 'batch'}, 'predictions': {0: 'batch'}},
|
||||
do_constant_folding=True,
|
||||
)
|
||||
print('saved:', out_path)
|
||||
|
||||
try:
|
||||
import onnx
|
||||
model = onnx.load(out_path)
|
||||
ops = {n.op_type for n in model.graph.node}
|
||||
print('GridSample in graph:', 'GridSample' in ops)
|
||||
print('node op types (sample):', sorted(ops)[:20], '...')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if args.check:
|
||||
compare_outputs(wrapper, dummy, out_path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pre-generate .lines.txt from masks for faster CLRNet training."""
|
||||
import argparse
|
||||
import os
|
||||
import os.path as osp
|
||||
import sys
|
||||
|
||||
import cv2
|
||||
from tqdm import tqdm
|
||||
|
||||
ROOT = osp.dirname(osp.dirname(osp.abspath(__file__)))
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
from clrnet.utils.mask_to_lanes import lanes_from_mask, lanes_to_lines_txt
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--data-root", required=True)
|
||||
ap.add_argument("--list", required=True, help="train_gt.txt path")
|
||||
ap.add_argument("--sample-y", type=int, nargs="+", default=list(range(710, 150, -10)))
|
||||
ap.add_argument("--num-lanes", type=int, default=4)
|
||||
ap.add_argument("--cache-dir", default="cache/mufld_lines")
|
||||
args = ap.parse_args()
|
||||
|
||||
list_path = args.list if osp.isabs(args.list) else osp.join(args.data_root, args.list)
|
||||
n_ok = 0
|
||||
with open(list_path) as f:
|
||||
lines = [ln.strip() for ln in f if ln.strip()]
|
||||
for line in tqdm(lines):
|
||||
parts = line.split()
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
img_rel, _ = parts[0].lstrip("/"), parts[1].lstrip("/")
|
||||
img_path = osp.join(args.data_root, img_rel)
|
||||
base = img_path[:-4]
|
||||
out = osp.join(args.data_root, args.cache_dir, osp.relpath(base, args.data_root) + ".lines.txt")
|
||||
mask_path = osp.join(args.data_root, parts[1].lstrip("/"))
|
||||
mask = cv2.imread(mask_path, cv2.IMREAD_UNCHANGED)
|
||||
if mask is None:
|
||||
continue
|
||||
if mask.ndim > 2:
|
||||
mask = mask[:, :, 0]
|
||||
lanes = lanes_from_mask(mask, args.sample_y, args.num_lanes)
|
||||
os.makedirs(osp.dirname(out), exist_ok=True)
|
||||
with open(out, "w") as fp:
|
||||
fp.write(lanes_to_lines_txt(lanes))
|
||||
n_ok += 1
|
||||
print("wrote", n_ok, "lines files under", args.cache_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,126 @@
|
||||
import json
|
||||
import numpy as np
|
||||
import cv2
|
||||
import os
|
||||
import argparse
|
||||
|
||||
TRAIN_SET = ['label_data_0313.json', 'label_data_0601.json']
|
||||
VAL_SET = ['label_data_0531.json']
|
||||
TRAIN_VAL_SET = TRAIN_SET + VAL_SET
|
||||
TEST_SET = ['test_label.json']
|
||||
|
||||
|
||||
def gen_label_for_json(args, image_set):
|
||||
H, W = 720, 1280
|
||||
SEG_WIDTH = 30
|
||||
save_dir = args.savedir
|
||||
|
||||
os.makedirs(os.path.join(args.root, args.savedir, "list"), exist_ok=True)
|
||||
list_f = open(
|
||||
os.path.join(args.root, args.savedir, "list",
|
||||
"{}_gt.txt".format(image_set)), "w")
|
||||
|
||||
json_path = os.path.join(args.root, args.savedir,
|
||||
"{}.json".format(image_set))
|
||||
with open(json_path) as f:
|
||||
for line in f:
|
||||
label = json.loads(line)
|
||||
# ---------- clean and sort lanes -------------
|
||||
lanes = []
|
||||
_lanes = []
|
||||
slope = [
|
||||
] # identify 0th, 1st, 2nd, 3rd, 4th, 5th lane through slope
|
||||
for i in range(len(label['lanes'])):
|
||||
l = [(x, y)
|
||||
for x, y in zip(label['lanes'][i], label['h_samples'])
|
||||
if x >= 0]
|
||||
if (len(l) > 1):
|
||||
_lanes.append(l)
|
||||
slope.append(
|
||||
np.arctan2(l[-1][1] - l[0][1], l[0][0] - l[-1][0]) /
|
||||
np.pi * 180)
|
||||
_lanes = [_lanes[i] for i in np.argsort(slope)]
|
||||
slope = [slope[i] for i in np.argsort(slope)]
|
||||
|
||||
idx = [None for i in range(6)]
|
||||
for i in range(len(slope)):
|
||||
if slope[i] <= 90:
|
||||
idx[2] = i
|
||||
idx[1] = i - 1 if i > 0 else None
|
||||
idx[0] = i - 2 if i > 1 else None
|
||||
else:
|
||||
idx[3] = i
|
||||
idx[4] = i + 1 if i + 1 < len(slope) else None
|
||||
idx[5] = i + 2 if i + 2 < len(slope) else None
|
||||
break
|
||||
for i in range(6):
|
||||
lanes.append([] if idx[i] is None else _lanes[idx[i]])
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
img_path = label['raw_file']
|
||||
seg_img = np.zeros((H, W, 3))
|
||||
list_str = [] # str to be written to list.txt
|
||||
for i in range(len(lanes)):
|
||||
coords = lanes[i]
|
||||
if len(coords) < 4:
|
||||
list_str.append('0')
|
||||
continue
|
||||
for j in range(len(coords) - 1):
|
||||
cv2.line(seg_img, coords[j], coords[j + 1],
|
||||
(i + 1, i + 1, i + 1), SEG_WIDTH // 2)
|
||||
list_str.append('1')
|
||||
|
||||
seg_path = img_path.split("/")
|
||||
seg_path, img_name = os.path.join(args.root, args.savedir,
|
||||
seg_path[1],
|
||||
seg_path[2]), seg_path[3]
|
||||
os.makedirs(seg_path, exist_ok=True)
|
||||
seg_path = os.path.join(seg_path, img_name[:-3] + "png")
|
||||
cv2.imwrite(seg_path, seg_img)
|
||||
|
||||
seg_path = "/".join([
|
||||
args.savedir, *img_path.split("/")[1:3], img_name[:-3] + "png"
|
||||
])
|
||||
if seg_path[0] != '/':
|
||||
seg_path = '/' + seg_path
|
||||
if img_path[0] != '/':
|
||||
img_path = '/' + img_path
|
||||
list_str.insert(0, seg_path)
|
||||
list_str.insert(0, img_path)
|
||||
list_str = " ".join(list_str) + "\n"
|
||||
list_f.write(list_str)
|
||||
|
||||
|
||||
def generate_json_file(save_dir, json_file, image_set):
|
||||
with open(os.path.join(save_dir, json_file), "w") as outfile:
|
||||
for json_name in (image_set):
|
||||
with open(os.path.join(args.root, json_name)) as infile:
|
||||
for line in infile:
|
||||
outfile.write(line)
|
||||
|
||||
|
||||
def generate_label(args):
|
||||
save_dir = os.path.join(args.root, args.savedir)
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
generate_json_file(save_dir, "train_val.json", TRAIN_VAL_SET)
|
||||
generate_json_file(save_dir, "test.json", TEST_SET)
|
||||
|
||||
print("generating train_val set...")
|
||||
gen_label_for_json(args, 'train_val')
|
||||
print("generating test set...")
|
||||
gen_label_for_json(args, 'test')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--root',
|
||||
required=True,
|
||||
help='The root of the Tusimple dataset')
|
||||
parser.add_argument('--savedir',
|
||||
type=str,
|
||||
default='seg_label',
|
||||
help='The root of the Tusimple dataset')
|
||||
args = parser.parse_args()
|
||||
|
||||
generate_label(args)
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write a calibration image list for RKNN (one absolute path per line)."""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--glob', default='**/*.jpg', help='glob under --root')
|
||||
parser.add_argument('--root', default='.', help='search root')
|
||||
parser.add_argument('--out', default='models/calib_images.txt')
|
||||
parser.add_argument('--max', type=int, default=20)
|
||||
args = parser.parse_args()
|
||||
|
||||
root = os.path.abspath(args.root)
|
||||
paths = []
|
||||
for p in sorted(glob.glob(os.path.join(root, args.glob), recursive=True)):
|
||||
if os.path.isfile(p):
|
||||
paths.append(os.path.abspath(p))
|
||||
if len(paths) >= args.max:
|
||||
break
|
||||
|
||||
if not paths:
|
||||
raise SystemExit(f'no images under {root} with {args.glob}')
|
||||
|
||||
out = os.path.abspath(args.out)
|
||||
os.makedirs(os.path.dirname(out) or '.', exist_ok=True)
|
||||
with open(out, 'w') as f:
|
||||
f.write('\n'.join(paths) + '\n')
|
||||
print(f'wrote {len(paths)} paths -> {out}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
56
algorithms/lane_ufld/code/CLRNet-main/tools/onnx_to_rknn.py
Normal file
56
algorithms/lane_ufld/code/CLRNet-main/tools/onnx_to_rknn.py
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ONNX -> RKNN for CLRNet (platform rk3576). Requires rknn-toolkit2 on x86 Linux."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--onnx', default='models/clrernet_culane_dla34.onnx')
|
||||
parser.add_argument('--rknn', default='models/clrernet_culane_dla34_rk3576.rknn')
|
||||
parser.add_argument('--platform', default='rk3576')
|
||||
parser.add_argument('--dtype', default='i8', choices=['i8', 'fp'])
|
||||
parser.add_argument('--dataset', default=None,
|
||||
help='txt list of calibration images (one path per line)')
|
||||
parser.add_argument('--height', type=int, default=320)
|
||||
parser.add_argument('--width', type=int, default=800)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
from rknn.api import RKNN
|
||||
except ImportError as e:
|
||||
raise SystemExit(
|
||||
'Install rknn-toolkit2 in a separate env: pip install rknn-toolkit2\n' + str(e)
|
||||
) from e
|
||||
|
||||
if not args.dataset:
|
||||
raise SystemExit(
|
||||
'Provide --dataset with a txt of RGB image paths for INT8 calibration.'
|
||||
)
|
||||
|
||||
rknn = RKNN(verbose=True)
|
||||
print('config:', args.platform, args.dtype)
|
||||
rknn.config(mean_values=[[103.939, 116.779, 123.68]],
|
||||
std_values=[[1, 1, 1]],
|
||||
target_platform=args.platform,
|
||||
quantized_dtype='asymmetric_quantized-8' if args.dtype == 'i8' else 'float16')
|
||||
|
||||
ret = rknn.load_onnx(model=args.onnx)
|
||||
if ret != 0:
|
||||
raise SystemExit('load_onnx failed')
|
||||
|
||||
ret = rknn.build(do_quantization=(args.dtype == 'i8'), dataset=args.dataset)
|
||||
if ret != 0:
|
||||
raise SystemExit('build failed')
|
||||
|
||||
os.makedirs(os.path.dirname(args.rknn) or '.', exist_ok=True)
|
||||
ret = rknn.export_rknn(args.rknn)
|
||||
if ret != 0:
|
||||
raise SystemExit('export_rknn failed')
|
||||
print('saved:', os.path.abspath(args.rknn))
|
||||
rknn.release()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user