1. 项目缘起为什么选择行空板做车牌识别最近在折腾一个智能停车场的边缘计算节点核心需求是在一个低功耗、低成本、体积小巧的设备上实现实时、准确的车牌识别。市面上常见的方案要么是直接用树莓派跑OpenCV的传统图像处理识别率在复杂光照和角度下容易拉胯要么就是上Jetson Nano这类带GPU的板子性能是强了但功耗和成本也跟着上去了对于需要多点部署的场景来说预算压力不小。就在我纠结选型的时候行空板进入了视野。这块板子集成了高性能的AI协处理器NPU主打的就是在嵌入式端高效运行YOLO这类目标检测模型。这正好切中了我的痛点既需要YOLO算法带来的高精度和鲁棒性又受限于边缘设备的算力和功耗。用行空板来跑Yolo做车牌识别听起来就像是为这个场景量身定制的方案。它不像纯CPU方案如树莓派那样吃力不讨好也不像GPU方案如Jetson那样“大炮打蚊子”在性能、功耗和成本之间找到了一个不错的平衡点。这个项目的目标很明确打造一个软硬件一体的车牌识别装置。硬件上以行空板为核心搭配一个普通的USB摄像头或CSI摄像头软件上完成YOLO模型的训练、优化、部署并编写一个能够实时捕获视频流、进行推理、并输出车牌号码的Python应用。整个过程会涉及到模型选型、数据集处理、训练技巧、模型转换ONNX等、以及在行空板NPU上的部署优化是一套完整的嵌入式AI应用开发流程。无论你是想学习如何在嵌入式设备上部署YOLO还是正在为某个物联网项目寻找轻量级视觉解决方案相信这个实践都能给你带来不少启发。2. 核心组件解析行空板与YOLO的强强联合要理解这个方案为什么可行得先拆开看看手里的“牌”行空板的硬件特性和YOLO算法的软件特性。2.1 行空板为边缘AI而生的计算平台行空板并不是一个通用型的单板计算机它的设计初衷非常聚焦——边缘AI推理。其核心优势在于那颗专为神经网络计算优化的NPU神经网络处理单元。与树莓派的Broadcom CPU或Jetson的GPU不同NPU采用了针对矩阵乘加运算等AI计算核心操作的特殊硬件架构在执行YOLO这类卷积神经网络时能效比每瓦特性能远高于通用处理器。这意味着在识别一张图片中的车牌时行空板NPU可以在更短的时间内、消耗更少的电量完成计算。对于需要7x24小时不间断运行、且可能由电池或太阳能供电的停车场监控点来说低功耗是刚性需求。此外行空板通常也集成了足够的外设接口如USB、CSI摄像头接口、GPIO等方便连接摄像头、补光灯、继电器控制道闸等外围设备形成一个完整的嵌入式系统。选择行空板而非树莓派跑YOLO根本原因在于算力瓶颈。在树莓派上即使使用经过高度优化的TensorFlow Lite或PyTorch Mobile用YOLOv5或v8处理一帧高清图像也可能需要数百毫秒甚至上秒级很难达到“实时”例如10 FPS的要求。而行空板的NPU针对YOLO模型经过特定驱动和运行时优化后完全有可能将单帧推理时间压缩到几十毫秒以内为后续的字符识别留出充足时间。2.2 YOLO算法为何是车牌检测的首选YOLOYou Only Look Once系列算法是当前目标检测领域的标杆之一其“单阶段”one-stage的设计理念将目标定位和分类在一个网络前向传播中完成速度上具有天然优势。对于车牌识别这个任务可以分解为两个子任务1) 车牌定位检测2) 字符识别OCR。在这个项目中我们主要利用YOLO完成第一个也是最关键的任务从复杂的车辆图像中快速、准确地框出车牌的位置。相比于传统的基于边缘检测、颜色分割的车牌定位方法YOLO方案的优势是压倒性的高鲁棒性对光照变化逆光、夜间、角度倾斜、部分遮挡等情况有更好的适应性。端到端训练特征提取和检测框回归都由网络自动学习省去了繁琐的手工特征设计。速度快配合专用硬件如NPU能满足实时性要求。目前YOLOv5和YOLOv8是社区最活跃、最易用的两个版本。v5以其极致的工程化和丰富的文档著称部署生态非常成熟v8则集成了检测、分割、分类等多种任务并且官方提供了更先进的训练技巧和模型结构。对于车牌检测这个相对标准的任务两者都是优秀的选择。我个人更倾向于从YOLOv5入手因为其社区资源特别是关于模型转换和量化对于行空板这类特定平台的支持可能更成熟一些。2.3 技术栈选型与工作流程整个项目的技术栈可以概括为“训练在云端推理在边缘”。模型训练端在一台拥有GPU的电脑或云服务器上使用PyTorch框架和YOLO官方代码库进行模型训练。我们需要准备一个高质量的车牌检测数据集。模型转换端将训练好的PyTorch模型.pt文件转换为行空板NPU支持的格式。这通常需要经过ONNX开放神经网络交换格式作为中间桥梁再通过厂商提供的专用转换工具如RKNN-Toolkit之于瑞芯微芯片转换成最终的部署模型如.rknn文件。这个步骤是打通训练和部署的关键也是最容易出错的环节。边缘推理端在行空板上使用Python调用厂商提供的NPU推理运行时库RKNN Runtime加载转换好的模型并编写图像采集、预处理、推理、后处理解码检测框以及字符识别可接另一个轻量级OCR模型或传统算法的完整应用逻辑。这个流程中每一个环节都有其技术细节和“坑点”接下来我们会逐一深入。3. 从零开始构建与训练YOLO车牌检测模型模型是项目的核心一个好的模型是后续一切工作的基础。训练一个专用于车牌检测的YOLO模型并不像直接用COCO预训练模型那么简单需要针对性的数据处理和训练策略。3.1 数据集的准备与标注“垃圾进垃圾出”在机器学习领域是铁律。对于车牌检测数据集需要尽可能覆盖你的实际应用场景。数据来源可以从公开数据集中筛选如CCPD但更推荐自己采集。用摄像头在实际部署环境停车场入口、道路旁拍摄不同时段早、中、晚、不同天气、不同车型的车辆图片这样的数据最具代表性。初始数据量建议在500-1000张左右后续可以基于模型在测试中的bad case进行增量收集和标注。标注工具LabelImg是经典选择。这里需要注意一个关键设置标签保存路径和保存类型。在LabelImg中建议将“保存目录”设置为一个独立的文件夹如labels/与图片文件夹如images/平行。保存格式务必选择“YOLO”格式这样会为每张图片生成一个.txt文件其中每一行包含class_id x_center y_center width height坐标是归一化后的0-1之间。绝对不要用绝对坐标。标注技巧标注框应紧密贴合车牌的四边即使车牌有轻微倾斜也应使用矩形框框住整个车牌区域。对于一张图中有多个车牌的情况每个车牌都需要单独标注。类别通常就设一个如“license_plate”。3.2 YOLO模型训练的关键配置与技巧准备好数据集后按照YOLO官方仓库的说明组织目录结构通常创建dataset/images/train/,dataset/labels/train/,dataset/images/val/等。接下来是训练阶段的重头戏。模型选择与输入尺寸YOLOv5提供了n, s, m, l, x等不同大小的模型权衡速度和精度。对于车牌检测YOLOv5s或YOLOv5m通常是不错的起点。输入图像的尺寸img-size是一个重要参数。常见的误区是盲目使用高分辨率如1920x1080。YOLO网络内部会进行多次下采样直接喂入大图会极大增加计算量可能不会带来精度显著提升反而拖慢速度。建议将原始图片缩放到一个固定的正方形尺寸如640x640。YOLO训练代码会自动使用letterbox操作即保持原图长宽比进行缩放然后在上下或左右填充灰边将图像补成正方形。这能有效避免图像失真。在数据增强配置中可以开启mosaic和mixup来提升模型泛化能力。针对小目标的优化车牌在整张车辆图像中属于典型的小目标。YOLO在检测小目标时有时会出现检测框偏离目标甚至漏检的情况。为了解决这个问题可以采取以下措施修改模型结构适当减少下采样倍率。例如在YOLOv5的配置文件中可以将最深层的Detect层负责检测大目标的输入特征图来源调整一下或者增加一个更浅层的检测头但需要对应修改网络结构较复杂。数据增强多使用随机裁剪、缩放等增强让模型看到更多不同大小和位置的车牌。调整锚框Anchor使用k-means算法在自己的数据集上重新聚类生成锚框尺寸使其更贴合车牌的实际宽高比。YOLOv5/v8的训练脚本通常自带这个功能。损失函数权重可以尝试调高针对小目标检测的损失权重如box_loss但需谨慎避免破坏训练平衡。训练过程监控使用TensorBoard或WB等工具监控损失曲线和评估指标mAP0.5。确保训练损失平稳下降验证集mAP稳步上升。如果出现验证集指标震荡或下降可能是过拟合需要增加数据增强、使用早停Early Stopping或减小模型容量。注意训练完成后不要只盯着在验证集上的指标。一定要用一批从未参与训练和验证的“真实场景”测试图片进行可视化测试观察模型在复杂情况下的表现这是发现模型弱点的最好方法。4. 模型转换与优化让YOLO在行空板上飞起来训练得到一个.pt文件只是万里长征第一步接下来要让它能在行空板的NPU上高效运行。这个过程俗称“模型部署”是嵌入式AI项目中最具挑战性的环节之一。4.1 从PyTorch到ONNX打通框架壁垒ONNX是一个开放的模型表示格式它成为了从训练框架PyTorch, TensorFlow到终端推理引擎如RKNN之间的桥梁。YOLO官方代码通常提供了导出ONNX模型的脚本。导出时有几个关键参数opset_version建议使用12或13兼容性较好。dynamic_axes对于动态批处理或可变输入尺寸很有用。但为了NPU推理效率最大化更推荐固定输入尺寸。例如在导出时指定--img-size 640 640并设置dynamic_axesNone得到一个输入为[1, 3, 640, 640]的静态图模型。这能允许NPU编译器进行更深度的图优化。simplify务必使用onnx-simplifier工具对导出的ONNX模型进行简化。它会优化计算图结构移除冗余操作这对后续转换成功至关重要。命令示例针对YOLOv5python export.py --weights best.pt --img 640 --batch 1 --include onnx --opset 12 python -m onnxsim best.onnx best_sim.onnx4.2 深入NPU部署RKNN模型转换与调优行空板采用的通常是瑞芯微Rockchip或类似厂商的芯片其NPU开发套件一般为RKNN-Toolkit。我们需要在x86开发机上安装这个工具链将ONNX模型转换为.rknn格式。转换过程并非一键完成充满了配置和调优量化Quantization这是提升NPU推理速度最关键的一步。它将模型权重和激活值从浮点数FP32转换为低精度整数如INT8大幅减少计算量和内存占用。RKNN-Toolkit支持后训练量化PTQ。你需要准备一个“量化数据集”——一小部分100-200张来自训练集的代表性图片。量化质量直接决定模型精度损失程度。经验如果量化后精度下降严重可以尝试1) 增加量化数据集的数量和多样性2) 使用dataset.txt文件明确指定每张量化图片的路径3) 在转换配置中尝试不同的量化算法如normalmmse等。预编译Pre-compile在转换时开启预编译选项可以将模型针对目标芯片如RK3566进行编译优化生成更高效的指令。但预编译后的模型通常与芯片型号绑定移植性变差。输入输出配置在转换时必须明确指定模型的输入和输出节点名称、尺寸和数据类型。对于YOLO模型输出节点可能不止一个如三个检测头的输出需要仔细核对ONNX模型的输出名并在RKNN构建配置中正确设置。一个典型的RKNN转换Python脚本片段如下from rknn.api import RKNN rknn RKNN() # 配置 rknn.config(mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3566) # 加载ONNX ret rknn.load_onnx(modelbest_sim.onnx) if ret ! 0: print(Load model failed!) exit(ret) # 构建模型 ret rknn.build(do_quantizationTrue, dataset./quant_dataset.txt) if ret ! 0: print(Build model failed!) exit(ret) # 导出RKNN模型 ret rknn.export_rknn(./license_plate_det.rknn)这个过程可能反复多次需要根据转换日志和后续推理测试结果来调整参数。5. 边缘侧应用开发行空板上的实时推理程序模型转换成功后就可以在行空板上编写最终的应用程序了。这个程序需要完成视频流捕获、图像预处理、模型推理、结果后处理以及车牌字符识别可选的全流程。5.1 环境搭建与依赖安装行空板通常运行基于Linux的系统如Debian。首先需要通过SSH连接到板子。安装Python环境行空板可能已预装Python3确保版本符合要求如3.8/3.9。使用pip安装必要依赖opencv-python-headless用于图像处理、numpy、以及瑞芯微提供的rknn-toolkit-lite或rknpu运行时库。注意运行时库的版本必须与转换模型时使用的RKNN-Toolkit版本匹配否则可能无法加载模型。部署模型将转换好的.rknn模型文件拷贝到行空板的存储空间中。5.2 推理流程的完整实现以下是核心推理代码的逻辑框架重点在于处理NPU推理的特殊性。import cv2 import numpy as np from rknnlite.api import RKNNLite # 使用RKNN Lite运行时 class LicensePlateDetector: def __init__(self, rknn_model_path): self.rknn RKNNLite() ret self.rknn.load_rknn(rknn_model_path) if ret ! 0: raise Exception(fLoad RKNN model failed! Error code: {ret}) ret self.rknn.init_runtime(core_maskRKNNLite.NPU_CORE_0) # 指定NPU核心 if ret ! 0: raise Exception(fInit runtime failed! Error code: {ret}) self.input_size (640, 640) # 与模型输入一致 self.conf_thres 0.5 # 置信度阈值 self.iou_thres 0.45 # NMS的IOU阈值 def preprocess(self, img): 图像预处理缩放、归一化、转换通道 # 1. 保持长宽比的缩放 (letterbox) h, w img.shape[:2] scale min(self.input_size[1] / h, self.input_size[0] / w) new_h, new_w int(h * scale), int(w * scale) resized_img cv2.resize(img, (new_w, new_h)) # 2. 创建画布并填充 canvas np.full((self.input_size[1], self.input_size[0], 3), 114, dtypenp.uint8) top (self.input_size[1] - new_h) // 2 left (self.input_size[0] - new_w) // 2 canvas[top:topnew_h, left:leftnew_w] resized_img # 3. 转换通道顺序 HWC - CHW, BGR - RGB, 归一化 img_data canvas.transpose((2, 0, 1))[::-1, ...] # BGR to RGB img_data np.ascontiguousarray(img_data) img_data img_data.astype(np.float32) img_data / 255.0 # 归一化到[0,1] return img_data, (scale, (left, top)), (h, w) def postprocess(self, outputs, preprocess_info, orig_shape): 后处理解码边界框应用NMS scale, (pad_left, pad_top) preprocess_info orig_h, orig_w orig_shape # 1. 将输出转换为检测框格式 [x1, y1, x2, y2, conf, cls] # 注意RKNN输出的格式需要根据你转换模型时的具体设置来解析。 # 这里假设输出是[1, 25200, 85]格式YOLOv5/v8常见854(xywh)1(conf)80(cls) # 实际中可能需要使用rknn.query_sdk_version()和rknn.query_io_info()来确认输出细节。 predictions np.squeeze(outputs[0]) # 简化批次维度 boxes [] # 遍历预测过滤低置信度并将坐标转换回原始图像尺寸 for pred in predictions: conf pred[4] if conf self.conf_thres: continue # 解码中心点和宽高 (相对于输入图像640x640) cx, cy, w, h pred[:4] # 转换到画布上的坐标去除填充 x1 (cx - w/2 - pad_left) / scale y1 (cy - h/2 - pad_top) / scale x2 (cx w/2 - pad_left) / scale y2 (cy h/2 - pad_top) / scale # 确保坐标在原始图像范围内 x1, y1, x2, y2 max(0, x1), max(0, y1), min(orig_w, x2), min(orig_h, y2) cls_id np.argmax(pred[5:]) boxes.append([x1, y1, x2, y2, conf, cls_id]) # 2. 应用非极大值抑制 (NMS) if len(boxes) 0: boxes np.array(boxes) indices cv2.dnn.NMSBoxes(boxes[:, :4].tolist(), boxes[:, 4].tolist(), self.conf_thres, self.iou_thres) final_boxes boxes[indices.flatten()] return final_boxes return [] def detect(self, img): 主检测函数 # 预处理 img_data, preprocess_info, orig_shape self.preprocess(img) # 推理 outputs self.rknn.inference(inputs[img_data]) # 后处理 detections self.postprocess(outputs, preprocess_info, orig_shape) return detections def release(self): self.rknn.release() # 主循环示例 def main(): detector LicensePlateDetector(./license_plate_det.rknn) cap cv2.VideoCapture(0) # 打开摄像头 while True: ret, frame cap.read() if not ret: break detections detector.detect(frame) for box in detections: x1, y1, x2, y2, conf, cls_id map(int, box[:6]) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, fPlate {conf:.2f}, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 2) cv2.imshow(Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows() detector.release() if __name__ __main__: main()关键点解析Letterbox处理预处理必须与训练和转换时的设置完全一致。这里在缩放时保持了长宽比并添加了灰边同时记录了缩放比例和填充偏移量用于在后处理中将坐标精确映射回原始图像。输出解析这是最大的坑点。RKNN推理的输出张量布局shape和含义取决于原始模型和转换过程。务必使用rknn.query_io_info()在初始化后打印输入输出信息并对照ONNX模型的输出结构来编写解析代码。不同版本的YOLOv5, v8输出格式可能不同。性能优化NPU推理本身很快但图像预处理cv2.resize,np.transpose和后处理NMS在CPU上进行可能成为瓶颈。可以考虑使用多线程将摄像头捕获、预处理、推理、后处理、显示放在不同的线程中形成流水线提高整体帧率。错误处理增加对rknn.init_runtime和rknn.inference返回值的检查便于调试。5.3 集成字符识别OCR模块检测到车牌后下一步是识别上面的字符。这里有两种主流思路方案A轻量级深度学习模型。可以训练一个专门的车牌OCR模型如CRNNCTC同样转换为RKNN格式在NPU上运行。这需要另一个车牌字符数据集流程与检测类似实现端到端的识别。方案B传统图像处理轻量级分类。这是更常见的嵌入式方案车牌矫正根据检测框的四个角点如果检测模型能输出角点更好否则用矩形框进行透视变换将倾斜车牌矫正为正视图。字符分割利用车牌的底色蓝、黄、白等与字符颜色的强对比度进行二值化、形态学操作、轮廓查找将单个字符分割出来。字符识别对每个分割出的字符区域可以使用一个轻量级的分类网络如MobileNet识别为0-9、A-Z及省份简称同样部署在NPU上或者使用更传统的模板匹配方法适用于字体较规范的车牌。方案B对计算资源的要求更分散可能更容易在资源受限的设备上实现实时性。但方案A的精度上限和泛化能力可能更好。在实际项目中我通常会先尝试方案B如果分割效果不稳定如车牌污损、光照不均再考虑方案A。6. 实测调优与部署避坑指南把代码跑起来只是开始让它在真实环境中稳定、准确地工作才是真正的挑战。下面分享一些从实验室到现场部署过程中积累的经验和常见问题。6.1 性能瓶颈分析与优化在行空板上运行程序后首先用top或htop命令监控CPU和内存占用同时测量端到端的处理延迟从捕获一帧到输出结果。瓶颈可能在CPU如果发现NPU利用率不高但CPU某个核心满载瓶颈很可能在图像预处理cv2操作或后处理NMS循环。优化方法包括使用OpenCV的UMat透明GPU加速如果板子有GPU或尝试更高效的数组操作。检查是否在循环中重复创建大数组尝试复用内存。对于NMS可以尝试使用更快的实现或者调整置信度和IOU阈值减少需要处理的框数量。瓶颈可能在I/O如果摄像头捕获帧率很低检查摄像头驱动和cv2.VideoCapture的参数设置。使用MJPG编码格式通常比YUYV等原始格式捕获更快。模型本身如果NPU利用率高但帧率仍不达标考虑换用更小的YOLO模型如从m换到s或者将输入尺寸从640降低到480但需重新训练模型。量化精度从FP16切换到INT8也能带来显著的性能提升。6.2 常见问题与排查链路RKNN模型加载失败现象init_runtime或load_rknn返回错误。排查确认RKNN模型文件路径正确且权限可读。确认行空板上RKNN运行时库的版本与转换模型时使用的RKNN-Toolkit版本完全一致。版本不匹配是首要原因。检查模型是否针对当前行空板的芯片型号如RK3566进行了预编译。非预编译的通用模型兼容性更好。查看系统日志dmesg看是否有NPU驱动相关的错误。推理结果异常全是零、框乱飞现象模型能跑但输出的检测框坐标或置信度完全不对。排查首要怀疑对象预处理/后处理不匹配。这是最高发问题。请以“像素级精度”核对以下环节是否完全一致训练时的letterbox逻辑 vs. 部署代码中的letterbox逻辑。训练时的归一化方式除以255减均值除标准差vs. 部署代码中的归一化。终极调试方法在PC上用Python加载原始的PyTorch模型.pt对同一张测试图片分别用训练代码的预处理方式和部署代码的预处理方式处理然后推理对比两者的输出原始输出未经过NMS。如果差异巨大问题就在预处理。如果PC上PyTorch模型输出正常但RKNN模型输出异常问题就在模型转换量化失真或输出解析错误。检查输出解析代码。使用rknn.query_io_info()打印输出的名称、形状和数据类型确保你的解析代码能正确对应。精度严重下降量化导致现象浮点模型精度很好但量化INT8后模型漏检、误检增多。排查与解决量化数据集确保量化数据集具有代表性覆盖各种场景不同亮度、车型、角度。可以尝试增加量化图片数量如从100张到500张。量化算法在RKNN-Toolkit的build函数中尝试不同的quant_algorithm选项如normal或mmse。混合量化如果某些层对量化特别敏感可以尝试在转换配置中将这些层排除在量化之外保持FP16。这需要查阅RKNN-Toolkit文档看是否支持混合精度配置。直接使用FP16模型如果INT8精度损失无法接受可以退而求其次使用FP16精度的模型速度比INT8慢但比FP32快精度基本无损。内存不足现象程序运行一段时间后崩溃或rknn.init_runtime失败。排查行空板内存有限通常1GB或2GB。确保没有内存泄漏。在初始化RKNN运行时可以尝试指定更小的core_mask如果NPU有多核或减少模型输入的批次大小batch size。同时检查其他进程是否占用了过多内存。6.3 提升系统鲁棒性的工程化建议看门狗Watchdog编写一个简单的看门狗脚本监控主识别进程的心跳。如果进程卡死自动重启。这对于无人值守的现场设备至关重要。日志系统不要只用print。集成logging模块将程序运行状态、识别结果、错误信息记录到本地文件或远程服务器便于后期问题追溯。参数可配置化将置信度阈值、IOU阈值、摄像头索引、模型路径等参数写入配置文件如config.yaml避免硬编码方便现场调整。温度监控长时间高负载运行NPU和CPU可能会发热。可以添加温度读取逻辑当温度过高时主动降低推理频率如跳帧处理或触发报警。从模型训练、转换、部署到最后的调优每一步都需要耐心和细致的调试。这个基于行空板和YOLO的车牌识别装置麻雀虽小五脏俱全它涵盖了嵌入式AI应用从开发到落地的完整链条。当你看到摄像头实时框出车牌并且能在资源受限的设备上流畅运行时那种成就感是对所有折腾的最好回报。这个框架不仅适用于车牌识别稍加修改就能迁移到人脸识别、安全帽检测、工业瑕疵检测等众多边缘视觉场景中。
基于行空板与YOLO的嵌入式车牌识别:从模型训练到NPU部署全流程
1. 项目缘起为什么选择行空板做车牌识别最近在折腾一个智能停车场的边缘计算节点核心需求是在一个低功耗、低成本、体积小巧的设备上实现实时、准确的车牌识别。市面上常见的方案要么是直接用树莓派跑OpenCV的传统图像处理识别率在复杂光照和角度下容易拉胯要么就是上Jetson Nano这类带GPU的板子性能是强了但功耗和成本也跟着上去了对于需要多点部署的场景来说预算压力不小。就在我纠结选型的时候行空板进入了视野。这块板子集成了高性能的AI协处理器NPU主打的就是在嵌入式端高效运行YOLO这类目标检测模型。这正好切中了我的痛点既需要YOLO算法带来的高精度和鲁棒性又受限于边缘设备的算力和功耗。用行空板来跑Yolo做车牌识别听起来就像是为这个场景量身定制的方案。它不像纯CPU方案如树莓派那样吃力不讨好也不像GPU方案如Jetson那样“大炮打蚊子”在性能、功耗和成本之间找到了一个不错的平衡点。这个项目的目标很明确打造一个软硬件一体的车牌识别装置。硬件上以行空板为核心搭配一个普通的USB摄像头或CSI摄像头软件上完成YOLO模型的训练、优化、部署并编写一个能够实时捕获视频流、进行推理、并输出车牌号码的Python应用。整个过程会涉及到模型选型、数据集处理、训练技巧、模型转换ONNX等、以及在行空板NPU上的部署优化是一套完整的嵌入式AI应用开发流程。无论你是想学习如何在嵌入式设备上部署YOLO还是正在为某个物联网项目寻找轻量级视觉解决方案相信这个实践都能给你带来不少启发。2. 核心组件解析行空板与YOLO的强强联合要理解这个方案为什么可行得先拆开看看手里的“牌”行空板的硬件特性和YOLO算法的软件特性。2.1 行空板为边缘AI而生的计算平台行空板并不是一个通用型的单板计算机它的设计初衷非常聚焦——边缘AI推理。其核心优势在于那颗专为神经网络计算优化的NPU神经网络处理单元。与树莓派的Broadcom CPU或Jetson的GPU不同NPU采用了针对矩阵乘加运算等AI计算核心操作的特殊硬件架构在执行YOLO这类卷积神经网络时能效比每瓦特性能远高于通用处理器。这意味着在识别一张图片中的车牌时行空板NPU可以在更短的时间内、消耗更少的电量完成计算。对于需要7x24小时不间断运行、且可能由电池或太阳能供电的停车场监控点来说低功耗是刚性需求。此外行空板通常也集成了足够的外设接口如USB、CSI摄像头接口、GPIO等方便连接摄像头、补光灯、继电器控制道闸等外围设备形成一个完整的嵌入式系统。选择行空板而非树莓派跑YOLO根本原因在于算力瓶颈。在树莓派上即使使用经过高度优化的TensorFlow Lite或PyTorch Mobile用YOLOv5或v8处理一帧高清图像也可能需要数百毫秒甚至上秒级很难达到“实时”例如10 FPS的要求。而行空板的NPU针对YOLO模型经过特定驱动和运行时优化后完全有可能将单帧推理时间压缩到几十毫秒以内为后续的字符识别留出充足时间。2.2 YOLO算法为何是车牌检测的首选YOLOYou Only Look Once系列算法是当前目标检测领域的标杆之一其“单阶段”one-stage的设计理念将目标定位和分类在一个网络前向传播中完成速度上具有天然优势。对于车牌识别这个任务可以分解为两个子任务1) 车牌定位检测2) 字符识别OCR。在这个项目中我们主要利用YOLO完成第一个也是最关键的任务从复杂的车辆图像中快速、准确地框出车牌的位置。相比于传统的基于边缘检测、颜色分割的车牌定位方法YOLO方案的优势是压倒性的高鲁棒性对光照变化逆光、夜间、角度倾斜、部分遮挡等情况有更好的适应性。端到端训练特征提取和检测框回归都由网络自动学习省去了繁琐的手工特征设计。速度快配合专用硬件如NPU能满足实时性要求。目前YOLOv5和YOLOv8是社区最活跃、最易用的两个版本。v5以其极致的工程化和丰富的文档著称部署生态非常成熟v8则集成了检测、分割、分类等多种任务并且官方提供了更先进的训练技巧和模型结构。对于车牌检测这个相对标准的任务两者都是优秀的选择。我个人更倾向于从YOLOv5入手因为其社区资源特别是关于模型转换和量化对于行空板这类特定平台的支持可能更成熟一些。2.3 技术栈选型与工作流程整个项目的技术栈可以概括为“训练在云端推理在边缘”。模型训练端在一台拥有GPU的电脑或云服务器上使用PyTorch框架和YOLO官方代码库进行模型训练。我们需要准备一个高质量的车牌检测数据集。模型转换端将训练好的PyTorch模型.pt文件转换为行空板NPU支持的格式。这通常需要经过ONNX开放神经网络交换格式作为中间桥梁再通过厂商提供的专用转换工具如RKNN-Toolkit之于瑞芯微芯片转换成最终的部署模型如.rknn文件。这个步骤是打通训练和部署的关键也是最容易出错的环节。边缘推理端在行空板上使用Python调用厂商提供的NPU推理运行时库RKNN Runtime加载转换好的模型并编写图像采集、预处理、推理、后处理解码检测框以及字符识别可接另一个轻量级OCR模型或传统算法的完整应用逻辑。这个流程中每一个环节都有其技术细节和“坑点”接下来我们会逐一深入。3. 从零开始构建与训练YOLO车牌检测模型模型是项目的核心一个好的模型是后续一切工作的基础。训练一个专用于车牌检测的YOLO模型并不像直接用COCO预训练模型那么简单需要针对性的数据处理和训练策略。3.1 数据集的准备与标注“垃圾进垃圾出”在机器学习领域是铁律。对于车牌检测数据集需要尽可能覆盖你的实际应用场景。数据来源可以从公开数据集中筛选如CCPD但更推荐自己采集。用摄像头在实际部署环境停车场入口、道路旁拍摄不同时段早、中、晚、不同天气、不同车型的车辆图片这样的数据最具代表性。初始数据量建议在500-1000张左右后续可以基于模型在测试中的bad case进行增量收集和标注。标注工具LabelImg是经典选择。这里需要注意一个关键设置标签保存路径和保存类型。在LabelImg中建议将“保存目录”设置为一个独立的文件夹如labels/与图片文件夹如images/平行。保存格式务必选择“YOLO”格式这样会为每张图片生成一个.txt文件其中每一行包含class_id x_center y_center width height坐标是归一化后的0-1之间。绝对不要用绝对坐标。标注技巧标注框应紧密贴合车牌的四边即使车牌有轻微倾斜也应使用矩形框框住整个车牌区域。对于一张图中有多个车牌的情况每个车牌都需要单独标注。类别通常就设一个如“license_plate”。3.2 YOLO模型训练的关键配置与技巧准备好数据集后按照YOLO官方仓库的说明组织目录结构通常创建dataset/images/train/,dataset/labels/train/,dataset/images/val/等。接下来是训练阶段的重头戏。模型选择与输入尺寸YOLOv5提供了n, s, m, l, x等不同大小的模型权衡速度和精度。对于车牌检测YOLOv5s或YOLOv5m通常是不错的起点。输入图像的尺寸img-size是一个重要参数。常见的误区是盲目使用高分辨率如1920x1080。YOLO网络内部会进行多次下采样直接喂入大图会极大增加计算量可能不会带来精度显著提升反而拖慢速度。建议将原始图片缩放到一个固定的正方形尺寸如640x640。YOLO训练代码会自动使用letterbox操作即保持原图长宽比进行缩放然后在上下或左右填充灰边将图像补成正方形。这能有效避免图像失真。在数据增强配置中可以开启mosaic和mixup来提升模型泛化能力。针对小目标的优化车牌在整张车辆图像中属于典型的小目标。YOLO在检测小目标时有时会出现检测框偏离目标甚至漏检的情况。为了解决这个问题可以采取以下措施修改模型结构适当减少下采样倍率。例如在YOLOv5的配置文件中可以将最深层的Detect层负责检测大目标的输入特征图来源调整一下或者增加一个更浅层的检测头但需要对应修改网络结构较复杂。数据增强多使用随机裁剪、缩放等增强让模型看到更多不同大小和位置的车牌。调整锚框Anchor使用k-means算法在自己的数据集上重新聚类生成锚框尺寸使其更贴合车牌的实际宽高比。YOLOv5/v8的训练脚本通常自带这个功能。损失函数权重可以尝试调高针对小目标检测的损失权重如box_loss但需谨慎避免破坏训练平衡。训练过程监控使用TensorBoard或WB等工具监控损失曲线和评估指标mAP0.5。确保训练损失平稳下降验证集mAP稳步上升。如果出现验证集指标震荡或下降可能是过拟合需要增加数据增强、使用早停Early Stopping或减小模型容量。注意训练完成后不要只盯着在验证集上的指标。一定要用一批从未参与训练和验证的“真实场景”测试图片进行可视化测试观察模型在复杂情况下的表现这是发现模型弱点的最好方法。4. 模型转换与优化让YOLO在行空板上飞起来训练得到一个.pt文件只是万里长征第一步接下来要让它能在行空板的NPU上高效运行。这个过程俗称“模型部署”是嵌入式AI项目中最具挑战性的环节之一。4.1 从PyTorch到ONNX打通框架壁垒ONNX是一个开放的模型表示格式它成为了从训练框架PyTorch, TensorFlow到终端推理引擎如RKNN之间的桥梁。YOLO官方代码通常提供了导出ONNX模型的脚本。导出时有几个关键参数opset_version建议使用12或13兼容性较好。dynamic_axes对于动态批处理或可变输入尺寸很有用。但为了NPU推理效率最大化更推荐固定输入尺寸。例如在导出时指定--img-size 640 640并设置dynamic_axesNone得到一个输入为[1, 3, 640, 640]的静态图模型。这能允许NPU编译器进行更深度的图优化。simplify务必使用onnx-simplifier工具对导出的ONNX模型进行简化。它会优化计算图结构移除冗余操作这对后续转换成功至关重要。命令示例针对YOLOv5python export.py --weights best.pt --img 640 --batch 1 --include onnx --opset 12 python -m onnxsim best.onnx best_sim.onnx4.2 深入NPU部署RKNN模型转换与调优行空板采用的通常是瑞芯微Rockchip或类似厂商的芯片其NPU开发套件一般为RKNN-Toolkit。我们需要在x86开发机上安装这个工具链将ONNX模型转换为.rknn格式。转换过程并非一键完成充满了配置和调优量化Quantization这是提升NPU推理速度最关键的一步。它将模型权重和激活值从浮点数FP32转换为低精度整数如INT8大幅减少计算量和内存占用。RKNN-Toolkit支持后训练量化PTQ。你需要准备一个“量化数据集”——一小部分100-200张来自训练集的代表性图片。量化质量直接决定模型精度损失程度。经验如果量化后精度下降严重可以尝试1) 增加量化数据集的数量和多样性2) 使用dataset.txt文件明确指定每张量化图片的路径3) 在转换配置中尝试不同的量化算法如normalmmse等。预编译Pre-compile在转换时开启预编译选项可以将模型针对目标芯片如RK3566进行编译优化生成更高效的指令。但预编译后的模型通常与芯片型号绑定移植性变差。输入输出配置在转换时必须明确指定模型的输入和输出节点名称、尺寸和数据类型。对于YOLO模型输出节点可能不止一个如三个检测头的输出需要仔细核对ONNX模型的输出名并在RKNN构建配置中正确设置。一个典型的RKNN转换Python脚本片段如下from rknn.api import RKNN rknn RKNN() # 配置 rknn.config(mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3566) # 加载ONNX ret rknn.load_onnx(modelbest_sim.onnx) if ret ! 0: print(Load model failed!) exit(ret) # 构建模型 ret rknn.build(do_quantizationTrue, dataset./quant_dataset.txt) if ret ! 0: print(Build model failed!) exit(ret) # 导出RKNN模型 ret rknn.export_rknn(./license_plate_det.rknn)这个过程可能反复多次需要根据转换日志和后续推理测试结果来调整参数。5. 边缘侧应用开发行空板上的实时推理程序模型转换成功后就可以在行空板上编写最终的应用程序了。这个程序需要完成视频流捕获、图像预处理、模型推理、结果后处理以及车牌字符识别可选的全流程。5.1 环境搭建与依赖安装行空板通常运行基于Linux的系统如Debian。首先需要通过SSH连接到板子。安装Python环境行空板可能已预装Python3确保版本符合要求如3.8/3.9。使用pip安装必要依赖opencv-python-headless用于图像处理、numpy、以及瑞芯微提供的rknn-toolkit-lite或rknpu运行时库。注意运行时库的版本必须与转换模型时使用的RKNN-Toolkit版本匹配否则可能无法加载模型。部署模型将转换好的.rknn模型文件拷贝到行空板的存储空间中。5.2 推理流程的完整实现以下是核心推理代码的逻辑框架重点在于处理NPU推理的特殊性。import cv2 import numpy as np from rknnlite.api import RKNNLite # 使用RKNN Lite运行时 class LicensePlateDetector: def __init__(self, rknn_model_path): self.rknn RKNNLite() ret self.rknn.load_rknn(rknn_model_path) if ret ! 0: raise Exception(fLoad RKNN model failed! Error code: {ret}) ret self.rknn.init_runtime(core_maskRKNNLite.NPU_CORE_0) # 指定NPU核心 if ret ! 0: raise Exception(fInit runtime failed! Error code: {ret}) self.input_size (640, 640) # 与模型输入一致 self.conf_thres 0.5 # 置信度阈值 self.iou_thres 0.45 # NMS的IOU阈值 def preprocess(self, img): 图像预处理缩放、归一化、转换通道 # 1. 保持长宽比的缩放 (letterbox) h, w img.shape[:2] scale min(self.input_size[1] / h, self.input_size[0] / w) new_h, new_w int(h * scale), int(w * scale) resized_img cv2.resize(img, (new_w, new_h)) # 2. 创建画布并填充 canvas np.full((self.input_size[1], self.input_size[0], 3), 114, dtypenp.uint8) top (self.input_size[1] - new_h) // 2 left (self.input_size[0] - new_w) // 2 canvas[top:topnew_h, left:leftnew_w] resized_img # 3. 转换通道顺序 HWC - CHW, BGR - RGB, 归一化 img_data canvas.transpose((2, 0, 1))[::-1, ...] # BGR to RGB img_data np.ascontiguousarray(img_data) img_data img_data.astype(np.float32) img_data / 255.0 # 归一化到[0,1] return img_data, (scale, (left, top)), (h, w) def postprocess(self, outputs, preprocess_info, orig_shape): 后处理解码边界框应用NMS scale, (pad_left, pad_top) preprocess_info orig_h, orig_w orig_shape # 1. 将输出转换为检测框格式 [x1, y1, x2, y2, conf, cls] # 注意RKNN输出的格式需要根据你转换模型时的具体设置来解析。 # 这里假设输出是[1, 25200, 85]格式YOLOv5/v8常见854(xywh)1(conf)80(cls) # 实际中可能需要使用rknn.query_sdk_version()和rknn.query_io_info()来确认输出细节。 predictions np.squeeze(outputs[0]) # 简化批次维度 boxes [] # 遍历预测过滤低置信度并将坐标转换回原始图像尺寸 for pred in predictions: conf pred[4] if conf self.conf_thres: continue # 解码中心点和宽高 (相对于输入图像640x640) cx, cy, w, h pred[:4] # 转换到画布上的坐标去除填充 x1 (cx - w/2 - pad_left) / scale y1 (cy - h/2 - pad_top) / scale x2 (cx w/2 - pad_left) / scale y2 (cy h/2 - pad_top) / scale # 确保坐标在原始图像范围内 x1, y1, x2, y2 max(0, x1), max(0, y1), min(orig_w, x2), min(orig_h, y2) cls_id np.argmax(pred[5:]) boxes.append([x1, y1, x2, y2, conf, cls_id]) # 2. 应用非极大值抑制 (NMS) if len(boxes) 0: boxes np.array(boxes) indices cv2.dnn.NMSBoxes(boxes[:, :4].tolist(), boxes[:, 4].tolist(), self.conf_thres, self.iou_thres) final_boxes boxes[indices.flatten()] return final_boxes return [] def detect(self, img): 主检测函数 # 预处理 img_data, preprocess_info, orig_shape self.preprocess(img) # 推理 outputs self.rknn.inference(inputs[img_data]) # 后处理 detections self.postprocess(outputs, preprocess_info, orig_shape) return detections def release(self): self.rknn.release() # 主循环示例 def main(): detector LicensePlateDetector(./license_plate_det.rknn) cap cv2.VideoCapture(0) # 打开摄像头 while True: ret, frame cap.read() if not ret: break detections detector.detect(frame) for box in detections: x1, y1, x2, y2, conf, cls_id map(int, box[:6]) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, fPlate {conf:.2f}, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 2) cv2.imshow(Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows() detector.release() if __name__ __main__: main()关键点解析Letterbox处理预处理必须与训练和转换时的设置完全一致。这里在缩放时保持了长宽比并添加了灰边同时记录了缩放比例和填充偏移量用于在后处理中将坐标精确映射回原始图像。输出解析这是最大的坑点。RKNN推理的输出张量布局shape和含义取决于原始模型和转换过程。务必使用rknn.query_io_info()在初始化后打印输入输出信息并对照ONNX模型的输出结构来编写解析代码。不同版本的YOLOv5, v8输出格式可能不同。性能优化NPU推理本身很快但图像预处理cv2.resize,np.transpose和后处理NMS在CPU上进行可能成为瓶颈。可以考虑使用多线程将摄像头捕获、预处理、推理、后处理、显示放在不同的线程中形成流水线提高整体帧率。错误处理增加对rknn.init_runtime和rknn.inference返回值的检查便于调试。5.3 集成字符识别OCR模块检测到车牌后下一步是识别上面的字符。这里有两种主流思路方案A轻量级深度学习模型。可以训练一个专门的车牌OCR模型如CRNNCTC同样转换为RKNN格式在NPU上运行。这需要另一个车牌字符数据集流程与检测类似实现端到端的识别。方案B传统图像处理轻量级分类。这是更常见的嵌入式方案车牌矫正根据检测框的四个角点如果检测模型能输出角点更好否则用矩形框进行透视变换将倾斜车牌矫正为正视图。字符分割利用车牌的底色蓝、黄、白等与字符颜色的强对比度进行二值化、形态学操作、轮廓查找将单个字符分割出来。字符识别对每个分割出的字符区域可以使用一个轻量级的分类网络如MobileNet识别为0-9、A-Z及省份简称同样部署在NPU上或者使用更传统的模板匹配方法适用于字体较规范的车牌。方案B对计算资源的要求更分散可能更容易在资源受限的设备上实现实时性。但方案A的精度上限和泛化能力可能更好。在实际项目中我通常会先尝试方案B如果分割效果不稳定如车牌污损、光照不均再考虑方案A。6. 实测调优与部署避坑指南把代码跑起来只是开始让它在真实环境中稳定、准确地工作才是真正的挑战。下面分享一些从实验室到现场部署过程中积累的经验和常见问题。6.1 性能瓶颈分析与优化在行空板上运行程序后首先用top或htop命令监控CPU和内存占用同时测量端到端的处理延迟从捕获一帧到输出结果。瓶颈可能在CPU如果发现NPU利用率不高但CPU某个核心满载瓶颈很可能在图像预处理cv2操作或后处理NMS循环。优化方法包括使用OpenCV的UMat透明GPU加速如果板子有GPU或尝试更高效的数组操作。检查是否在循环中重复创建大数组尝试复用内存。对于NMS可以尝试使用更快的实现或者调整置信度和IOU阈值减少需要处理的框数量。瓶颈可能在I/O如果摄像头捕获帧率很低检查摄像头驱动和cv2.VideoCapture的参数设置。使用MJPG编码格式通常比YUYV等原始格式捕获更快。模型本身如果NPU利用率高但帧率仍不达标考虑换用更小的YOLO模型如从m换到s或者将输入尺寸从640降低到480但需重新训练模型。量化精度从FP16切换到INT8也能带来显著的性能提升。6.2 常见问题与排查链路RKNN模型加载失败现象init_runtime或load_rknn返回错误。排查确认RKNN模型文件路径正确且权限可读。确认行空板上RKNN运行时库的版本与转换模型时使用的RKNN-Toolkit版本完全一致。版本不匹配是首要原因。检查模型是否针对当前行空板的芯片型号如RK3566进行了预编译。非预编译的通用模型兼容性更好。查看系统日志dmesg看是否有NPU驱动相关的错误。推理结果异常全是零、框乱飞现象模型能跑但输出的检测框坐标或置信度完全不对。排查首要怀疑对象预处理/后处理不匹配。这是最高发问题。请以“像素级精度”核对以下环节是否完全一致训练时的letterbox逻辑 vs. 部署代码中的letterbox逻辑。训练时的归一化方式除以255减均值除标准差vs. 部署代码中的归一化。终极调试方法在PC上用Python加载原始的PyTorch模型.pt对同一张测试图片分别用训练代码的预处理方式和部署代码的预处理方式处理然后推理对比两者的输出原始输出未经过NMS。如果差异巨大问题就在预处理。如果PC上PyTorch模型输出正常但RKNN模型输出异常问题就在模型转换量化失真或输出解析错误。检查输出解析代码。使用rknn.query_io_info()打印输出的名称、形状和数据类型确保你的解析代码能正确对应。精度严重下降量化导致现象浮点模型精度很好但量化INT8后模型漏检、误检增多。排查与解决量化数据集确保量化数据集具有代表性覆盖各种场景不同亮度、车型、角度。可以尝试增加量化图片数量如从100张到500张。量化算法在RKNN-Toolkit的build函数中尝试不同的quant_algorithm选项如normal或mmse。混合量化如果某些层对量化特别敏感可以尝试在转换配置中将这些层排除在量化之外保持FP16。这需要查阅RKNN-Toolkit文档看是否支持混合精度配置。直接使用FP16模型如果INT8精度损失无法接受可以退而求其次使用FP16精度的模型速度比INT8慢但比FP32快精度基本无损。内存不足现象程序运行一段时间后崩溃或rknn.init_runtime失败。排查行空板内存有限通常1GB或2GB。确保没有内存泄漏。在初始化RKNN运行时可以尝试指定更小的core_mask如果NPU有多核或减少模型输入的批次大小batch size。同时检查其他进程是否占用了过多内存。6.3 提升系统鲁棒性的工程化建议看门狗Watchdog编写一个简单的看门狗脚本监控主识别进程的心跳。如果进程卡死自动重启。这对于无人值守的现场设备至关重要。日志系统不要只用print。集成logging模块将程序运行状态、识别结果、错误信息记录到本地文件或远程服务器便于后期问题追溯。参数可配置化将置信度阈值、IOU阈值、摄像头索引、模型路径等参数写入配置文件如config.yaml避免硬编码方便现场调整。温度监控长时间高负载运行NPU和CPU可能会发热。可以添加温度读取逻辑当温度过高时主动降低推理频率如跳帧处理或触发报警。从模型训练、转换、部署到最后的调优每一步都需要耐心和细致的调试。这个基于行空板和YOLO的车牌识别装置麻雀虽小五脏俱全它涵盖了嵌入式AI应用从开发到落地的完整链条。当你看到摄像头实时框出车牌并且能在资源受限的设备上流畅运行时那种成就感是对所有折腾的最好回报。这个框架不仅适用于车牌识别稍加修改就能迁移到人脸识别、安全帽检测、工业瑕疵检测等众多边缘视觉场景中。