共计 3277 个字符,预计需要花费 9 分钟才能阅读完成。
背景痛点
在 21 届智能车竞赛的视觉赛道中,实时目标检测面临三大核心挑战:

- 实时性要求:赛道规则要求单帧处理延迟必须控制在 30ms 以内,相当于至少 33FPS 的稳定帧率
- 硬件限制:官方指定的 Jetson Nano 开发板仅有 4 核 ARM CPU+128 核 Maxwell GPU,算力仅 472GFLOPS
- 环境干扰:比赛场地存在强烈反光、树荫斑驳等复杂光照条件,传统算法鲁棒性差
通过实测发现,未经优化的 YOLOv5m 模型在 Jetson Nano 上仅能达到 8FPS,且持续推理会出现显存溢出的问题。这就引出了我们的核心优化方向:在保持 mAP>95% 的前提下,将模型体积压缩到 2MB 以内,推理速度提升至 25FPS+。
技术选型
我们对三大轻量级架构进行了横向对比测试(输入分辨率统一为 640×640):
| 模型 | 参数量(M) | FLOPs(G) | mAP@0.5 | Jetson Nano 推理时延(ms) |
|---|---|---|---|---|
| YOLOv5s | 7.2 | 16.5 | 98.2% | 42 |
| MobileNetV3 | 5.4 | 12.8 | 94.7% | 38 |
| NanoDet | 0.95 | 4.8 | 89.3% | 28 |
最终选择 YOLOv5s 的核心考量是:
- 精度优势:对小目标(如锥桶、路标)的检测 AP 高出竞争对手 3 - 8 个百分点
- 生态支持:原生支持 PyTorch->ONNX->TensorRT 完整工具链
- 可扩展性:自带 Focus 结构能有效保留浅层特征,便于后续剪枝优化
核心实现
模型压缩三步走
- 结构化剪枝:
- 使用 BN 层 γ 系数评估通道重要性
- 对 Conv+BN+ReLU 组合进行 L1 正则化训练
-
剪枝率设置为 40%,模型体积从 14.4MB 降至 5.3MB
-
量化感知训练(QAT):
- 在训练时插入伪量化节点模拟 INT8 精度
- 配置
num_bits=8, affine=True参数 -
通过校准集统计激活值动态范围
-
TensorRT 加速:
trtexec --onnx=yolov5s_pruned.onnx \ --saveEngine=yolov5s.engine \ --fp16 \ --workspace=2048关键参数说明:
--fp16启用半精度推理--workspace设置显存缓存池大小
部署流水线设计
整体流程如下图所示:
[摄像头] -> [OpenCV 预处理] -> [TensorRT 引擎] -> [后处理] -> [控制指令]
▲ │
└──[帧缓存队列]←[多线程管理器]
代码示例
以下是核心推理代码片段(完整代码见 GitHub 仓库):
import tensorrt as trt
import pycuda.driver as cuda
class TrtInference:
def __init__(self, engine_path):
# 初始化 CUDA 上下文
cuda.init()
self.device = cuda.Device(0)
self.ctx = self.device.make_context()
# 加载 TensorRT 引擎
with open(engine_path, "rb") as f:
runtime = trt.Runtime(trt.Logger(trt.Logger.WARNING))
self.engine = runtime.deserialize_cuda_engine(f.read())
# 创建执行上下文
self.context = self.engine.create_execution_context()
def detect(self, img):
# 图像预处理
input_blob = cv2.dnn.blobFromImage(
img,
scalefactor=1/255.0,
size=(640, 640),
swapRB=True
)
# 分配显存
inputs, outputs, bindings = [], [], []
stream = cuda.Stream()
for binding in self.engine:
size = trt.volume(self.engine.get_binding_shape(binding))
dtype = trt.nptype(self.engine.get_binding_dtype(binding))
host_mem = cuda.pagelocked_empty(size, dtype)
device_mem = cuda.mem_alloc(host_mem.nbytes)
bindings.append(int(device_mem))
if self.engine.binding_is_input(binding):
inputs.append({"host": host_mem, "device": device_mem})
else:
outputs.append({"host": host_mem, "device": device_mem})
# 执行推理
cuda.memcpy_htod_async(inputs[0]["device"], input_blob, stream)
self.context.execute_async_v2(bindings, stream.handle)
cuda.memcpy_dtoh_async(outputs[0]["host"], outputs[0]["device"], stream)
stream.synchronize()
# 后处理
detections = self.postprocess(outputs[0]["host"])
return detections
性能优化
分辨率影响测试
| 输入尺寸 | mAP@0.5 | FPS | 显存占用(MB) |
|---|---|---|---|
| 320×320 | 92.1% | 38 | 780 |
| 640×640 | 98.2% | 25 | 1250 |
选择 640×640 的权衡点在于:
– 锥桶目标在 320×320 下仅占 15-20 像素,易漏检
– 帧率 25FPS 仍能满足控制周期要求
多线程安全实现
采用生产者 - 消费者模式解决帧缓存竞争:
from queue import Queue
from threading import Lock
frame_queue = Queue(maxsize=3)
queue_lock = Lock()
# 生产者线程
class CameraThread(Thread):
def run(self):
while True:
ret, frame = cap.read()
with queue_lock:
if frame_queue.full():
frame_queue.get() # 丢弃最旧帧
frame_queue.put(frame)
# 消费者线程
class InferThread(Thread):
def run(self):
while True:
with queue_lock:
if not frame_queue.empty():
frame = frame_queue.get()
detections = trt_engine.detect(frame)
避坑指南
- ONNX 转换问题:
- 遇到
Unsupported ONNX opset version 12错误时,需指定opset=11 -
YOLOv5 的 Focus 层需替换为等效 Conv 实现
python export.py --weights yolov5s.pt --include onnx --opset 11 -
光照增强策略:
- 采用 CLAHE 算法处理过曝区域
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) lab[...,0] = clahe.apply(lab[...,0]) enhanced = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
延伸思考
为进一步提升小目标检测能力,可尝试:
1. 知识蒸馏:用 YOLOv5x 作为教师模型,在特征图层面进行 Hint Learning
2. 自适应分辨率:根据检测置信度动态调整输入尺寸
3. 时序融合:利用卡尔曼滤波跟踪相邻帧的检测结果
通过上述方法,我们的方案在正式比赛中实现了 98.3% 的 mAP 和 27FPS 的稳定性能,完整代码已开源在 GitHub。希望这篇实战经验能为嵌入式 AI 开发者提供有价值的参考。
正文完
