1. 项目缘起为什么要在行空板上做红绿灯检测最近在折腾一个挺有意思的玩意儿——用行空板UNIHIKER做了一个红绿灯检测系统。可能有人会问现在市面上成熟的ADAS高级驾驶辅助系统方案那么多干嘛要自己用一块小小的开发板来“造轮子”这事儿得从我自己的实际需求说起。我手头有一些教育机器人、小型移动平台的项目需要它们具备基础的“视觉”能力比如识别前方的交通信号灯然后做出“前进”或“停止”的决策。直接用商业级的ADAS模块一来成本高二来“黑盒”属性太强对于教学、原型验证或者特定场景下的二次开发并不友好。我需要的是一个足够轻量、完全开源可控、并且能让我从数据采集、模型训练到部署推理全流程都摸一遍的方案。行空板就进入了我的视野。它本质上是一块集成了屏幕、按键、多种传感器和GPIO接口的Python编程单片机核心是双核A7芯片运行着完整的Debian Linux系统。这意味着我可以在上面直接跑Python调用OpenCV、PyTorch或TensorFlow Lite这些库实现一个端到端的计算机视觉应用。用行空板做红绿灯检测更像是一个“麻雀虽小五脏俱全”的微型项目它逼着我去思考在资源算力、内存严格受限的边缘设备上如何平衡检测的精度、速度和系统的实时性所以这个项目的目的不是要做出一个媲美特斯拉的视觉系统而是探索一条可行的技术路径如何利用易得的硬件和开源工具栈构建一个能实际跑起来的、轻量级红绿灯检测原型。这个过程涉及模型选型、数据准备、模型压缩、边缘部署等一系列环节每一个环节都有不少坑要踩也有不少经验可以分享。2. 核心方案选型YOLO vs. 传统视觉方法确定了硬件平台行空板和目标红绿灯检测后下一个关键决策是用什么技术来实现检测传统计算机视觉方法比如基于颜色空间HSV分割和形状匹配圆形、矩形检测是一个直观的起点。它的优点是逻辑简单、计算量极低几乎不依赖外部数据。你可以写一段Python脚本用OpenCV把图像从BGR转到HSV空间设定红色和绿色的阈值范围提取出可能是灯光的区域再用轮廓查找和几何判断来确认。注意纯颜色形状的方法在实验室可控光照下可能工作良好但一到实际场景就“原形毕露”。黄昏、逆光、阴雨天气、LED灯珠的过曝、树叶的遮挡、远处红绿灯像素点过少……这些因素都会导致颜色阈值失效产生大量误检或漏检。它的鲁棒性太差无法作为一个可靠系统的基石。因此我毫不犹豫地选择了基于深度学习的目标检测方案。在众多模型中YOLOYou Only Look Once系列因其在速度和精度间的优秀平衡而成为边缘设备上的首选。它的核心思想是将目标检测视为一个回归问题单次前向传播就能预测出图像中所有目标的边界框和类别效率非常高。对于行空板这样的边缘设备我们需要的是YOLO的“瘦身”版本。主要有几个选择YOLOv5s / YOLOv8n这是Ultralytics官方维护的模型结构清晰生态完善。其中的-s(small) 或-n(nano) 版本参数量在700万到900万之间相对轻量。YOLOv3-Tiny更老但更经典的轻量级模型结构简单速度更快但精度通常低于v5s/v8n。专为移动端优化的模型如MobileNet-SSD但它的检测头设计不如YOLO高效在兼顾多尺度目标尤其是小目标如远处的红绿灯时可能吃力。我的选择是YOLOv5s。原因如下社区与工具链成熟Ultralytics提供的训练和导出工具链非常完善从数据标注格式YOLO格式到模型训练、验证、导出为ONNX或TorchScript都有现成的脚本极大降低了开发门槛。精度与速度的平衡在自制的红绿灯数据集上实测YOLOv5s的mAP0.5能达到95%以上而在行空板CPU上推理一张640x640的图片耗时大约在800-1200毫秒。虽然达不到实时30FPS但对于教育机器人或低速移动平台每秒1-2次的检测频率需求是可以接受的。易于优化YOLOv5的模型结构清晰后续如果需要进一步压缩如通道剪枝、量化有比较多的参考资料和实践案例。确定了主干模型接下来就要解决两个核心问题数据和部署。3. 数据集的“脏活累活”采集、标注与增强“垃圾进垃圾出”在机器学习领域是铁律。没有一个好的数据集再精巧的模型也无用武之地。红绿灯检测的数据集准备是项目中最耗时但也最关键的“脏活累活”。3.1 数据采集我的数据来源主要有三个公开数据集如Bosch Small Traffic Lights Dataset、DriveU Traffic Light Dataset。这些数据集专业、标注质量高但场景可能偏海外与国内常见的红绿灯样式如倒计时读秒、横向排列有差异。行车记录仪视频从网上找一些国内城市道路的行车记录仪视频用OpenCV按帧抽取图片。这是获取真实、多样场景的最有效方式。实拍拿着行空板或手机到路口实地拍摄。这部分数据量不大但能针对性地补充一些特殊角度、光照条件的数据对于提升模型在最终部署环境下的泛化能力很有帮助。总共收集了大约3000张包含红绿灯的图片。这个数量对于YOLOv5s这样的模型做迁移学习来说是一个比较理想的起点。3.2 数据标注我使用LabelImg或Roboflow进行标注。标注时需注意标注格式统一使用YOLO格式归一化的中心点x, y宽度w高度h。类别定义最初我只定义了red,green两类。但在实际标注中发现红绿灯有圆形、箭头形、行人信号等多种形态且存在“红灯亮”、“绿灯亮”和“全灭”三种状态。为了提高模型的实用性我最终定义了6个类别red,green,yellow,left_arrow_red,left_arrow_green,pedestrian_green。这样模型不仅能识别颜色还能识别部分语义。框体精度框要紧贴灯壳但不必过于精确到每个灯珠。对于远处很小的红绿灯框可以适当放大确保包含整个发光区域。3.3 数据增强Data Augmentation这是提升模型鲁棒性的“魔法”。我直接在YOLOv5的训练配置文件中启用了其内置的强大增强功能。以下是一些针对红绿灯场景特别重要的增强策略及其原因增强方法目的与原理对红绿灯检测的针对性作用Mosaic将四张图片随机拼接成一张。让模型在一张图中看到更多不同尺度、背景的红绿灯极大提升对小目标远处红绿灯的检测能力。随机仿射变换旋转、缩放、平移模拟摄像头视角的微小变化。红绿灯在图像中的位置和大小会因车辆行驶而不断变化此增强让模型对此不敏感。色彩抖动HSV调整随机调整图像的色调(H)、饱和度(S)、明度(V)。至关重要模拟不同天气雾、雨、不同时间黄昏、夜晚和不同摄像头白平衡下的颜色变化防止模型只认死板的RGB值。添加噪声在图像上添加高斯噪声或椒盐噪声。模拟图像传感器在低光照下的噪点提升模型在夜间或隧道等环境下的稳定性。在data/hyps/hyp.scratch-low.yaml配置文件中我调整了增强参数比如适度提高了HSV增强的强度以确保模型不会过度依赖绝对颜色。4. 模型训练与优化在云端“炼丹”行空板的算力不足以支持模型训练这一步需要在有GPU的电脑或云端服务器上完成。我使用的是Google Colab的免费GPU资源。4.1 训练环境搭建在Colab笔记本中克隆YOLOv5官方仓库安装依赖。!git clone https://github.com/ultralytics/yolov5 %cd yolov5 !pip install -r requirements.txt然后将准备好的数据集包含images和labels文件夹以及定义数据路径的data.yaml文件上传到Colab环境。4.2 关键训练配置创建data/traffic_light.yaml文件定义数据集# 类别数和类别名 nc: 6 names: [red, green, yellow, left_arrow_red, left_arrow_green, pedestrian_green] # 训练和验证图像的路径Colab中的路径 train: /content/dataset/images/train/ val: /content/dataset/images/val/开始训练关键参数如下!python train.py \ --img 640 \ # 训练图像尺寸 --batch 16 \ # 批次大小根据GPU内存调整 --epochs 100 \ # 训练轮数 --data data/traffic_light.yaml \ # 数据配置文件 --cfg models/yolov5s.yaml \ # 模型结构配置文件 --weights yolov5s.pt \ # 预训练权重加速收敛 --name traffic_light_s \ # 本次训练任务名称 --hyp data/hyps/hyp.scratch-low.yaml \ # 使用轻量化的超参配置 --cache \ # 缓存图像到内存加速训练 --device 0 # 使用GPU 04.3 训练过程监控与调优训练开始后利用TensorBoard或YOLOv5自带的日志可以监控关键指标损失函数box_loss, obj_loss, cls_loss观察其是否平稳下降。如果cls_loss分类损失居高不下可能是类别定义不清或样本不均衡。精度指标mAP0.5这是核心指标。关注其在验证集上的表现。如果训练集mAP很高但验证集很低可能是过拟合需要增加数据增强强度或使用早停Early Stopping。验证集预测结果定期查看模型在验证集图片上的预测效果直观判断问题所在。例如发现模型总是漏检远处的灯就需要回头检查数据集中小目标样本是否充足或者考虑在Mosaic增强中提高小目标的出现概率。我训练了大约100轮最终在验证集上的mAP0.5达到了96.2%满足要求。5. 模型部署与加速让模型在行空板上“跑起来”训练好的.pt模型文件不能直接在行空板上运行需要经过转换和优化。5.1 模型格式转换PyTorch - ONNX - TensorFlow Lite行空板原生对TensorFlow Lite支持较好且TFLite运行时内存占用更少。转换路径如下PyTorch 转 ONNX使用YOLOv5自带的export.py脚本。!python export.py --weights runs/train/traffic_light_s/weights/best.pt --include onnx --img 640 --dynamic得到best.onnx文件。--dynamic参数允许输入动态尺寸但为简化部署我固定为640x640。ONNX 转 TensorFlow Lite这里有个坑。不能直接用onnx-tf转换YOLOv5 v6.0的模型因为其包含的Focus切片操作在转换时容易出错。更稳定的方法是先转成TensorFlow SavedModel再转TFLite。# 安装 onnx-tf (可能需要特定版本) !pip install onnx-tf # 1. ONNX 转 TensorFlow SavedModel import onnx from onnx_tf.backend import prepare onnx_model onnx.load(best.onnx) tf_rep prepare(onnx_model) tf_rep.export_graph(best_savedmodel) # 2. SavedModel 转 TensorFlow Lite import tensorflow as tf converter tf.lite.TFLiteConverter.from_saved_model(best_savedmodel) converter.optimizations [tf.lite.Optimize.DEFAULT] # 应用默认优化量化 tflite_model converter.convert() with open(best_float16.tflite, wb) as f: f.write(tflite_model)我选择了Optimize.DEFAULT它通常会执行权重量化如float16量化能在几乎不损失精度的情况下减小模型体积、提升推理速度。5.2 行空板环境部署传输文件将转换好的best_float16.tflite文件、类别标签文件classes.txt以及一个用于预处理和后处理的Python脚本inference.py通过U盘或SCP传到行空板上。安装依赖在行空板的终端里安装必要的Python库。pip install opencv-python-headless numpy tflite-runtime注意安装opencv-python-headless而不是完整版可以节省空间。tflite-runtime是TensorFlow Lite的独立运行时比安装完整的TensorFlow更轻量。编写推理脚本inference.py是核心其工作流程如下import cv2 import numpy as np import tflite_runtime.interpreter as tflite # 1. 加载TFLite模型 interpreter tflite.Interpreter(model_pathbest_float16.tflite) interpreter.allocate_tensors() # 2. 获取输入输出详情 input_details interpreter.get_input_details() output_details interpreter.get_output_details() input_shape input_details[0][shape] # 通常是 [1, 640, 640, 3] # 3. 图像预处理函数 def preprocess(img): # 缩放到模型输入尺寸 img_resized cv2.resize(img, (input_shape[2], input_shape[1])) # BGR转RGB归一化到[0,1] img_rgb cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB) / 255.0 # 调整维度为 [1, H, W, C] 并转为float32 img_input img_rgb.astype(np.float32)[np.newaxis, ...] return img_input # 4. 后处理函数 (解析YOLO输出) def postprocess(outputs, img_shape, conf_threshold0.5, iou_threshold0.45): # 这里需要根据你的模型输出结构进行解析。 # YOLOv5 TFLite输出通常是 [1, 25200, 85] 或类似格式。 # 需要实现非极大值抑制(NMS)来过滤重叠框。 # 这是一个简化示例实际需要完整的解码和NMS。 detections [] # ... (解码逻辑包含中心点、宽高转成左上右下坐标过滤低置信度框) # ... (使用NMS算法例如通过numpy实现或调用cv2.dnn.NMSBoxes) return detections # 返回格式: [x1, y1, x2, y2, conf, class_id] # 5. 主循环 cap cv2.VideoCapture(0) # 打开行空板摄像头 while True: ret, frame cap.read() if not ret: break # 预处理 input_data preprocess(frame) # 推理 interpreter.set_tensor(input_details[0][index], input_data) interpreter.invoke() # 获取输出 outputs interpreter.get_tensor(output_details[0][index]) # 后处理 dets postprocess(outputs, frame.shape[:2]) # 可视化 for det in dets: x1, y1, x2, y2, conf, cls_id map(int, det[:4]), det[4], int(det[5]) label f{classes[cls_id]} {conf:.2f} cv2.rectangle(frame, (x1, y1), (x2, y2), (0,255,0), 2) cv2.putText(frame, label, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 2) # 显示结果行空板有屏幕 cv2.imshow(Traffic Light Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()6. 性能实测与瓶颈分析将上述脚本在行空板上运行就可以看到实时检测效果了。但“跑起来”和“跑得好”是两回事。我进行了一系列性能测试并分析了瓶颈。6.1 关键性能指标推理速度使用640x640输入在行空板CPU上单次推理耗时约900-1200毫秒。即FPS在0.8~1.1之间。内存占用运行推理脚本后通过htop观察Python进程内存占用约250MB。行空板总内存为512MB尚有余量。检测精度在白天光照良好的室外测试对20米内的红绿灯检测准确率很高几乎无漏检。但对更远的、像素面积小于20x20的红绿灯偶尔会出现漏检。对于箭头灯和行人灯的识别在近距离下效果不错。6.2 主要瓶颈与优化思路CPU算力是最大瓶颈行空板的双核A7 CPU主频较低进行密集的浮点运算即使是float16速度依然不够快。1秒1帧的速度对于需要快速反应的移动机器人来说太慢了。模型输入尺寸640x640对于红绿灯小目标检测是必要的但这也增加了计算量。可以尝试训练一个专门针对小目标优化的、输入尺寸为320x320的模型速度会显著提升但需要重新评估对小目标的检测精度。后处理耗时在Python中实现NMS非极大值抑制是另一个耗时点。如果使用TensorFlow Lite的Delegate委托机制将NMS等操作转移到更高效的底层实现可以节省一些时间。但对于行空板可用的Delegate如GPU、DSP支持有限。摄像头数据流cv2.VideoCapture读帧和cv2.imshow显示本身也有开销。可以考虑使用多线程一个线程专门负责抓取摄像头帧并放入队列另一个线程负责推理和显示避免I/O等待阻塞推理。6.3 一个实用的优化尝试模型量化前面我们用Optimize.DEFAULT做了float16量化。更激进的方法是整型量化INT8。这需要代表性的校准数据集来统计激活值的范围。# 在转换时加入INT8量化 converter tf.lite.TFLiteConverter.from_saved_model(best_savedmodel) converter.optimizations [tf.lite.Optimize.DEFAULT] converter.representative_dataset representative_dataset_gen # 一个生成校准数据的函数 converter.target_spec.supported_ops [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] converter.inference_input_type tf.uint8 # 输入也转为uint8 converter.inference_output_type tf.uint8 tflite_int8_model converter.convert()INT8量化后模型体积可减小至原来的1/4推理速度也能有显著提升在支持INT8加速的硬件上。但在行空板的CPU上速度提升可能不如体积减少那么明显且精度可能会有轻微损失1-2个mAP点需要仔细权衡。7. 系统集成与场景应用思考一个完整的“红绿灯检测系统”不仅仅是模型推理。在行空板上我们可以将其与其他功能模块集成形成一个闭环。7.1 状态判断与决策逻辑模型输出的是“一个红色的圆形”或“一个绿色的左箭头”。我们需要根据业务逻辑将其转化为“可通行”或“不可通行”的指令。def get_traffic_command(detections, frame_center_x): 根据检测结果生成控制指令。 简单逻辑找到图像中心水平线附近置信度最高的红/绿灯。 relevant_dets [] for det in detections: x1, y1, x2, y2, conf, cls_id det box_center_x (x1 x2) / 2 # 假设我们只关心图像下半部分地平线以上的信号灯 if y2 frame.shape[0] * 0.7: if cls_id in [0, 3]: # red, left_arrow_red relevant_dets.append((STOP, conf, box_center_x)) elif cls_id in [1, 4, 5]: # green, left_arrow_green, pedestrian_green relevant_dets.append((GO, conf, box_center_x)) if not relevant_dets: return UNKNOWN # 选择置信度最高的指令如果存在多个可以选择离图像中心最近的那个 relevant_dets.sort(keylambda x: x[1], reverseTrue) # 按置信度降序 return relevant_dets[0][0]这个逻辑非常初级。更复杂的系统可能需要跟踪红绿灯状态识别黄灯、判断闪烁并结合车辆所在车道通过图像分割或先验知识来决定关注哪一组信号灯。7.2 与执行机构联动行空板有GPIO引脚。我们可以将决策指令‘GO’/‘STOP’转化为电平信号控制一个LED灯模拟刹车灯/通行灯或者通过串口/UART发送指令给下位机如STM32进而控制电机的启停。import gpiod # 初始化GPIO chip gpiod.Chip(gpiochip0) line chip.get_line(18) # 假设GPIO18连接绿色LED line.request(consumertraffic_light, typegpiod.LINE_REQ_DIR_OUT) while True: command get_traffic_command(current_detections, center_x) if command GO: line.set_value(1) # 点亮绿灯 # 同时可以通过PWM控制电机转速 elif command STOP: line.set_value(0) # 熄灭绿灯或点亮红灯 # 发送停止指令7.3 项目扩展方向这个原型系统可以沿多个方向深化多任务学习让模型同时检测红绿灯、车道线和行人共享主干网络特征提升整体效率。时序信息利用连续帧检测结果可以用于状态跟踪和滤波减少单帧误检的影响并识别出“绿灯刚亮”或“红灯即将结束”等状态。模型轻量化再进一步尝试使用神经网络架构搜索NAS或知识蒸馏Knowledge Distillation技术获得一个专为行空板定制的、更小更快的模型。系统优化使用C重写核心推理和后处理代码并利用ARM NEON指令集进行加速有望将推理时间压缩到200毫秒以内。通过这个项目我深刻体会到在边缘设备上部署AI应用的挑战与乐趣。它要求你在算法精度、计算资源、功耗和实时性之间不断做出权衡。行空板作为一个功能丰富的教学和原型平台为这类探索提供了绝佳的起点。虽然最终的性能指标无法与专业硬件相比但整个从数据到部署的完整流程走通所带来的经验和对底层细节的理解是只看论文或调用API无法比拟的。