嵌入式视觉实战:智能送药小车图像识别系统设计与避坑指南 1. 项目背景与核心挑战2021年全国大学生电子设计竞赛的F题“智能送药小车”可以说是在当年所有赛题中对视觉识别与运动控制结合要求最高、综合性最强的一道题。题目要求小车在模拟的医院病房环境中根据任务要求自主识别病房号、识别并抓取指定药品并最终送达指定床位。整个任务链条中视觉部分扮演了“眼睛”和“大脑”的双重角色它不仅要“看”得清识别目标还要“想”得对做出决策。我们团队当时选择了这道题就是想在视觉与控制的深度融合上挑战一下自己。现在回过头来看视觉部分的设计与实现是整个项目成败的绝对关键。它不像循迹或者避障有相对成熟的传感器方案和算法库可以快速上手。病房号识别、药品识别、颜色区分、位置定位每一个环节都需要从零开始搭建视觉处理流水线并且要保证在比赛现场复杂多变的光照条件下依然有极高的鲁棒性和实时性。这不仅仅是调用一个OpenCV函数那么简单它涉及到图像预处理、特征提取、模型选择、参数调优、误判处理等一系列工程化问题。这篇文章我就以一个亲历者的身份详细拆解我们当时在视觉部分遇到的每一个坑、做出的每一个选择以及那些在赛后复盘时才恍然大悟的经验教训。无论你是未来准备参赛的学弟学妹还是对嵌入式视觉应用感兴趣的开发者希望这些“踩坑实录”和“实战心得”能给你带来一些实实在在的帮助。2. 视觉系统整体架构设计与选型思考做项目最忌讳一上来就埋头写代码。对于电赛这种时间紧、任务重的比赛前期的架构设计直接决定了后期是“事半功倍”还是“事倍功半”。我们的视觉系统架构经历了从“想当然”到“实战化”的多次迭代。2.1 硬件平台树莓派4B 官方摄像头模组硬件选型是第一个决策点。当时主流的方案有几种STM32H7系列OV系列摄像头进行底层图像处理、Jetson Nano等边缘计算设备、以及树莓派。我们最终选择了树莓派4B4GB内存版搭配官方摄像头模组CSI接口。为什么是树莓派核心原因在于开发效率和生态。STM32方案虽然功耗和实时性有优势但我们需要实现的病房号识别数字、药品识别形状颜色以及后续可能用到的二维码识别如果全靠单片机写底层算法开发周期会非常漫长且调试困难。树莓派运行完整的Linux系统可以直接使用Python和OpenCV这样成熟的计算机视觉库还有丰富的AI模型框架如TensorFlow Lite支持极大地降低了算法实现的门槛。官方CSI摄像头保证了图像采集的稳定性和较低的延迟这对于运动中的小车至关重要。虽然Jetson Nano的AI算力更强但其功耗、散热以及相对复杂的驱动配置在电赛紧凑的备赛时间里成为了一个不确定因素。树莓派“开箱即用”的特性让我们能把宝贵的时间集中在算法本身而不是环境搭建上。2.2 软件流水线从采集到决策的六步流程我们的视觉处理被设计成一个清晰的流水线每一步的输出都是下一步的输入这样便于模块化调试和问题定位。整个流程如下图像采集通过OpenCV的VideoCapture接口从CSI摄像头以30FPS的速率读取图像帧。这里第一个坑就来了分辨率与帧率的权衡。高分辨率如1920x1080有利于识别远处的目标但处理一帧的时间会大大增加可能导致系统卡顿。我们最终将分辨率设定为640x480这是一个在识别精度和 processing 速度之间比较好的平衡点。ROI感兴趣区域裁剪小车摄像头是固定俯仰角安装的我们通过实际测量和标定预先确定了病房号区域、药品摆放区域在图像中的大致位置。每一帧图像我们先根据小车的实时位置由编码器反馈估算和预设地图裁剪出对应的ROI而不是在全图中进行搜索。这能有效减少不必要的计算量是提升实时性的关键一步。图像预处理这是提升后续识别鲁棒性的“魔法步骤”。主要包括灰度化将彩色图转为灰度图减少计算量用于数字和形状识别。高斯滤波消除图像噪声。光照补偿这是重中之重。比赛现场的光照条件无法预测可能存在顶灯、窗户自然光等混合光源。我们采用了自适应直方图均衡化CLAHE来增强图像的对比度特别是阴影和高光区域的细节。实测证明这一步能极大缓解因光照不均导致的二值化失败问题。二值化对于数字和形状识别需要将图像转为黑白。我们使用了**大津法Otsu‘s Method**进行自动阈值分割它可以根据图像灰度分布自动计算最佳阈值比固定阈值适应性更强。特征提取与识别这是核心算法模块针对不同任务采用不同策略。病房号识别采用轮廓查找 模板匹配的组合方案。先通过findContours找到所有闭合轮廓根据轮廓面积、宽高比、位置等几何特征筛选出可能的数字区域。然后与我们事先制作好的0-9十个数字的模板进行归一化互相关NCC匹配。为什么不直接用OCR库如Tesseract因为现场环境下的数字可能变形、有污渍OCR库的误识别率在初期测试中偏高且依赖语言包不够轻量。我们自制的模板匹配方案针对赛题特定的数字字体进行了优化速度更快准确率更高。药品识别分为形状识别和颜色识别两步。形状识别同样使用轮廓分析通过计算轮廓的Hu矩不变性或者近似多边形的边数来区分圆形药瓶、矩形药盒等。颜色识别则需要在原始RGB或HSV色彩空间中进行。我们选择了HSV空间因为它能将颜色的亮度Value与色调Hue、饱和度Saturation分离开对光照变化相对不敏感。通过设定不同药品颜色如红、蓝、绿在H通道上的阈值范围来判定药品颜色。坐标转换与定位识别出目标后我们得到的是它在图像像素坐标系下的位置x, y。小车需要知道目标在真实世界坐标系单位厘米中的位置才能导航过去。这就需要相机标定。我们使用OpenCV的calibrateCamera函数利用棋盘格标定板预先获取了相机的内参焦距、主点和畸变系数。然后通过已知的目标实际尺寸如药瓶直径、相机安装高度和俯仰角结合针孔相机模型可以估算出目标相对于小车的距离和方位角。这个环节的精度直接影响了小车最终停靠和抓取的准确性。决策与通信视觉处理模块将识别结果如“目标病房302 药品红色圆形 位于小车前方30cm 左偏10°”封装成一个结构体通过串口UART或者网络Socket发送给小车的主控STM32。主控根据这些信息进行路径规划和运动控制。通信协议的设计要简单、可靠我们使用了固定的帧头、帧尾和校验和防止数据传输错误。这个架构看起来清晰但每一个环节在实现时都充满了“陷阱”接下来我就分模块深入聊聊那些让我们掉进去又爬出来的“深坑”。3. 病房号识别从轮廓到数字的“破译”之路病房号识别是任务的起点如果第一步就认错了房间后面的一切都是徒劳。我们在这个环节投入的调试时间最长。3.1 轮廓查找的“噪声战争”使用cv2.findContours查找数字轮廓时最大的敌人不是数字本身而是图像中各种意想不到的“噪声轮廓”。墙面的纹理、门框的阴影、甚至图像传感器本身的噪点都可能被检测为轮廓。我们的应对策略是一套组合过滤拳法面积过滤计算轮廓面积cv2.contourArea(contour)。根据先验知识设定一个最小面积min_area和最大面积max_area。太小的肯定是噪声太大的可能是整个门板。宽高比过滤数字通常是瘦高的。计算轮廓外接矩形的宽高比aspect_ratio width / height。对于单个数字我们设定0.2 aspect_ratio 0.6。这个范围需要根据实际字体调整。轮廓层级与位置过滤数字“8”或“0”中间可能有洞会产生内外两层轮廓父子关系。我们利用轮廓的层级信息hierarchy只处理最外层的轮廓父轮廓。同时多个数字如“302”应该是水平排列的它们的Y坐标纵坐标应该大致相同。我们可以对筛选出的轮廓按Y坐标进行聚类把同一行的轮廓归为一组。# 伪代码示例轮廓筛选 def filter_digit_contours(contours, hierarchy): digit_candidates [] for i, cnt in enumerate(contours): area cv2.contourArea(cnt) x, y, w, h cv2.boundingRect(cnt) aspect_ratio w / h # 过滤条件 if (min_area area max_area and 0.2 aspect_ratio 0.6 and hierarchy[0][i][3] -1): # 没有父轮廓是最外层轮廓 digit_candidates.append((cnt, x, y, w, h)) # 按Y坐标聚类假设同一行数字Y坐标差异不超过阈值 digit_candidates.sort(keylambda c: c[2]) # 按y排序 # ... 聚类算法将Y坐标相近的轮廓分为一组 return grouped_digits3.2 模板匹配的细节魔鬼筛选出候选轮廓后需要与模板进行匹配。我们事先在均匀光照下拍摄了0-9十个数字的高清图片手动裁剪、缩放为统一大小如20x40像素并二值化作为模板库。匹配时关键点在于归一化和相似度度量将候选轮廓区域裁剪出来并缩放到与模板相同的尺寸。使用cv2.matchTemplate函数匹配方法我们选择了cv2.TM_CCOEFF_NORMED归一化相关系数匹配。它返回一个相似度得分范围在[-1, 1]1表示完全匹配。对于每个候选区域与十个模板分别匹配取最高得分作为匹配结果。但这里有个致命陷阱如果得分普遍很低比如最高分才0.4说明当前区域可能根本不是数字或者预处理失败。我们必须设置一个置信度阈值如0.65低于此阈值的结果直接丢弃视为识别失败。血泪教训这个置信度阈值不能拍脑袋决定必须在各种光照条件下强光、弱光、侧光进行大量测试统计正确识别和错误识别的得分分布找到一个能最大限度区分二者的阈值。我们最初设的0.7在侧光环境下漏识了很多后来调到0.6并在预处理中强化了CLAHE才稳定下来。3.3 数字序列的拼接与校验识别出单个数字后需要将它们组合成房间号。我们根据轮廓的X坐标横坐标进行排序得到从左到右的数字序列。但问题又来了数字“1”的轮廓可能很窄容易被面积过滤误杀数字“11”可能被识别为两个“1”。我们的解决方案是引入上下文校验已知病房号是三位数。如果我们只识别出两个轮廓且它们的位置非常接近则考虑可能是数字“1”被拆散或合并了进行特殊处理。识别完成后将结果与一张预设的“有效病房号列表”进行比对。如果识别结果不在列表中则本次识别无效小车需要稍微移动位置重新识别。这相当于一个简单的“语法检查”避免了明显的荒谬输出。4. 药品识别在色彩与形状的迷宫中穿行药品识别需要同时判断形状圆形/方形和颜色红/蓝/绿等。这比单纯的数字识别更复杂因为颜色受光照影响极大形状也可能因视角而变形。4.1 形状识别的“不变性”追求我们使用轮廓的Hu矩来识别形状。Hu矩是一组对图像平移、旋转、缩放变化保持不变的矩特征非常适合用于形状匹配。import cv2 import numpy as np def get_shape_type(contour): # 计算Hu矩 moments cv2.moments(contour) hu_moments cv2.HuMoments(moments) # 取对数使值更易于比较 hu_moments -np.sign(hu_moments) * np.log10(np.abs(hu_moments)) # 与标准形状的Hu矩进行比对需预先计算好标准圆形和方形的Hu矩 # 计算差异差异最小的即为匹配形状 # ...但在实际中我们发现对于规则程度一般的药瓶和药盒直接用Hu矩匹配有时不够稳定。我们结合了多种几何特征圆形度circularity 4 * pi * area / (perimeter^2)。完美圆形的值为1正方形约为0.785。可以设定一个阈值如0.7来判断是否为圆形。矩形拟合使用cv2.minAreaRect获取最小外接矩形计算其面积与轮廓面积的比值。一个填充饱满的矩形这个比值会接近1。顶点数使用cv2.approxPolyDP对轮廓进行多边形逼近然后统计顶点数。圆形逼近后顶点数较多通常8而矩形是4个顶点。综合以上几个特征通过决策树或简单的加权打分可以更鲁棒地区分形状。4.2 颜色识别的“HSV空间生存指南”颜色识别是整个视觉系统中最“玄学”的部分。RGB空间下颜色三个通道耦合严重光照一变值就全变了。HSV空间是我们的救星。操作步骤将包含药品的ROI图像从BGR转换到HSV空间hsv cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)。针对每一种目标颜色定义其在HSV空间中的下限和上限阈值。例如对于红色在OpenCV的HSV范围中H通道是0-180# 红色有两个范围因为HSV色环中红色在0°和180°附近 lower_red1 np.array([0, 70, 50]) upper_red1 np.array([10, 255, 255]) lower_red2 np.array([170, 70, 50]) upper_red2 np.array([180, 255, 255])使用cv2.inRange函数创建掩膜mask在阈值范围内的像素点为白色255否则为黑色0。计算掩膜中白色像素的面积如果面积大于某个阈值则认为检测到了该颜色。核心挑战与调参经验H色调通道这是判断颜色的主要依据相对稳定。但不同摄像头的色彩响应有差异必须在你的摄像头和比赛现场可能的光照下重新标定。我们是在一个可调光的台灯下用色卡反复调整找到最稳定的范围。S饱和度和 V明度通道这两个通道是“守护神”。S通道下限如70可以过滤掉接近灰色的浅色避免将白色反光误判为某种颜色。V通道下限如50可以过滤掉阴影区域。这两个值设得太高会漏掉深色或淡色的目标设得太低又会引入大量噪声。我们的经验是在保证能识别出目标的前提下尽可能提高S和V的下限。光照突变现场可能有窗户。我们做了一个动态白平衡的预处理在图像中找一块应该是白色的区域如病房门板计算其RGB均值然后对整个图像进行一个颜色增益调整使其白色区域恢复为中性白。这能有效缓解色偏。颜色交叠有些颜色在边界处容易混淆如某些橙色和红色。除了精细调整阈值我们在决策逻辑上加了优先级如果一个区域同时满足两种颜色的阈值则计算两种掩膜与原始轮廓区域的交集比例取比例高的那种颜色。同时结合形状信息比如只有圆形药瓶才有红色也能帮助排除歧义。5. 系统集成与现场调试的“终极考验”算法模块单独测试都挺好一旦集成到小车上和运动控制系统联动各种意想不到的问题就爆发了。5.1 图像采集与运动模糊的对抗小车在运动时摄像头拍摄的图像会产生运动模糊导致轮廓提取和特征识别失败。我们的解决方案是硬件层面尽量将摄像头安装稳固减少振动。可以考虑增加简单的机械防抖结构。软件层面触发式采集。我们不是连续处理每一帧图像而是让主控在控制小车到达预定识别点如病房门前后发送一个指令给树莓派树莓派才启动一次高频率的连续采集比如连续采5帧然后对这5帧图像分别识别采用“投票法”或“中值法”确定最终结果。在小车高速运动过程中视觉模块处于低功耗的待机状态。这大大减少了无效计算也避免了运动模糊帧的干扰。5.2 串口通信的可靠性保障树莓派视觉与STM32控制之间通过串口通信。通信不稳定会导致小车“失明”或“发疯”。波特率选择115200或更高保证数据及时传输。协议设计我们自定义了简单的帧结构[0xAA, 0x55, data_len, cmd_type, data..., checksum]。0xAA, 0x55作为帧头checksum是对前面所有字节的累加和取低8位。STM32端只有收到完整且校验正确的帧才进行解析。错误处理与超时重发树莓派发送数据后等待STM32的ACK应答。如果超时未收到ACK则重发最多重试3次。同样STM32如果收到校验错误的帧会发送NAK请求重发。数据缓冲在树莓派端图像识别和串口发送分属不同线程。识别结果先放入一个队列由专门的发送线程从队列中取出并发送。这样即使某次识别耗时稍长也不会阻塞整个通信流程。5.3 现场灯光适应性调整即使我们做了CLAHE和动态白平衡到了比赛现场灯光环境依然可能超出预期。我们准备了最后一招参数微调接口。我们在树莓派上运行了一个简单的Flask网页服务器手机连接树莓派的热点后可以通过浏览器访问一个控制页面。页面上有滑动条可以实时调整二值化阈值、HSV颜色阈值、置信度阈值等关键参数。这样在比赛前的调试时间里我们可以根据现场光线快速微调系统参数达到最佳状态。这个“后门”在关键时刻起到了决定性作用。6. 复盘总结与可复用的经验清单回顾整个视觉部分的开发从最初的迷茫到最后的稳定运行是一个不断遇到问题、分析问题、解决问题的过程。以下是一些我认为最具普适性的经验希望能帮你少走弯路预处理决定上限算法决定下限再先进的识别算法如果输入的是质量很差的图像效果也不会好。一定要在图像预处理去噪、增强、校正上花足功夫。CLAHE和大津法二值化是性价比极高的组合。没有“银弹”算法只有“组合拳”策略不要指望用一个神奇的算法解决所有问题。数字识别用模板匹配几何过滤颜色识别用HSV阈值面积过滤形状识别用Hu矩几何特征。多特征融合、多方法投票能极大提升系统的鲁棒性。阈值不是魔法数字而是统计结果所有算法中的阈值面积、宽高比、置信度、HSV范围都不应该靠猜。尽可能模拟各种环境收集大量正负样本通过统计分布来确定阈值并在代码中将这些阈值定义为可轻松修改的配置参数或常量。仿真与实车测试必须交替进行在电脑上用录制好的视频调试算法效率很高但那是“理想环境”。一定要尽早把算法放到小车上进行实测试你会发现运动模糊、振动、视角变化、实时性要求等一大堆新问题。仿真和实车测试循环进行才能打磨出真正可用的系统。设计可调试的系统在代码中留出丰富的调试信息输出比如在图像上绘制识别出的轮廓、标注识别结果、打印关键变量的值。甚至可以像我们一样做一个简单的Web界面来实时调整参数和查看图像。调试效率直接决定了开发进度。通信协议要简单且健壮嵌入式系统间的通信可靠性远比带宽重要。帧头、长度、校验和这三要素必不可少。要有超时重传和错误处理机制。时间管理就是风险管理电赛时间有限必须优先实现核心功能做出一个能跑通的“最小可行产品”MVP。比如先保证在一种稳定光照下能识别再去考虑复杂的自适应光照。先保证静止状态下能识别再去处理运动模糊。功能完善和性能优化可以放在后面但系统必须尽早集成并跑起来。智能送药小车的视觉部分是一个典型的嵌入式机器视觉应用案例。它没有用到很高深的深度学习模型但将传统的图像处理技术与嵌入式系统的实时性、可靠性要求紧密结合其中涉及的工程思维、问题分解能力和调试技巧对我后续的学习和工作产生了深远的影响。那段在实验室里调参数调到深夜看着小车终于准确识别出药品并抓取成功的瞬间所有的疲惫都化为了成就感。希望这份详细的总结能为你点亮前行路上的一盏小灯。