
简介基于OpenMV视觉的运输小车设计源码是一套完整的智能运输小车实现方案面向嵌入式开发、机器视觉与智能物流方向的爱好者与学生。系统以C语言为主结合Python与MATLAB覆盖路径识别、障碍物检测、运动控制等关键环节适合用于课程设计、毕业设计或竞赛原型开发。资源包共206个文件包含36个头文件、34个C源文件、3个Python脚本及大量调试配置与编译中间文件压缩包大小27.52MB目录结构清晰便于按模块学习。已有315人学习下载。文件涵盖stm32f10x系列驱动、OpenMV视觉处理脚本、系统接线与启动脚本等读者可借此理解视觉模块与STM32底层控制的配合方式快速搭建并验证运输小车功能节省从零移植与调试的时间。1. 当视觉小车不满足于循迹OpenMV给运输场景带来了什么传统运输小车大多靠灰度传感器循迹或红外避障一旦地面反光、货物遮挡标线小车就会像无头苍蝇一样乱跑。而基于OpenMV视觉的运输小车把摄像头当成眼睛先看到目标物是什么颜色、什么形状、在什么位置再决定抓取和运送的路线。这套方案的工程意义在于识别逻辑从有没有线升级为是什么物体这让小车能完成自动分拣、定点投递、颜色分类这类真实仓储场景的任务。现在市面上主流的做法是用MicroPython在OpenMV上跑视觉算法再通过串口把识别结果发给STM32或Arduino控制板执行运动。最核心的问题从来不是摄像头怎么拍照而是目标物体在不同光照下怎么稳定识别、识别结果怎么可靠地传给下位机。这篇文章会把硬件选型、图像处理代码、通信协议、参数调优这些环节全部串起来讲清楚每一段代码都是可以抄下来直接改的。2. 运输小车视觉系统的硬件选型与架构2.1 为什么选OpenMV而不是树莓派或手机摄像头做运输小车视觉系统时首先要回答的问题是嵌入式设备上做视觉用哪块板子树莓派性能确实更强但它跑Linux系统上电启动要几十秒而且依赖Python生态里OpenCV等库的编译安装供电和体积对小车底盘来说都是负担。OpenMV的定位恰好卡在中间——它是一块自带摄像头接口、LCD接口、LED补光灯的微控制器跑Micropython固件主打在MCU上能跑的轻量视觉算法。对运输小车这个场景来说OpenMV有三个无法替代的优势一是帧率和功耗的平衡OpenMV Cam H7 Plus在320x240分辨率下能跑到50FPS功耗不超过200mA用18650电池就能直接喂饱二是固件内置了find_blobs、find_apriltags这些现成算法省掉自己在MCU上写特征提取的功夫三是IDE调试体验好OpenMV IDE里能看到实时画面、直方图和阈值调整滑块这让现场的工程师能快速看清算法到底看到了什么。如果你的项目需要识别复杂物体或者跑深度学习模型那OpenMV确实力不从心但运输小车场景里的颜色分拣、二维码识别、形状匹配OpenMV足够了。2.2 小车底盘的完整电气链路运输小车整套系统至少包含这几部分OpenMV摄像头模块负责感知主控MCU负责决策电机驱动板负责执行再加上电源管理模块给各器件供电。常见搭配是OpenMV Cam H7 Plus STM32F103C8T6 TB6612FNG电机驱动 4路红外对管做辅助避障。红外避障模块在这里起到的是兜底作用。视觉识别受光线干扰时小车至少不会直接撞墙。典型接线方式是OpenMV的UART3TX对应P1、RX对应P0采用跳线帽设置连接到STM32的USART2STM32的PB8/PB9接TB6612的AIN1/AIN2PB10/PB11接BIN1/BIN2控制左右电机。电源用两节18650串联得到7.4V一端接降压模块到5V给OpenMV另一端接到TB6612的VM引脚逻辑电源VCC也接5V。这样整套系统的电压轨是清晰分层的不会出现电机启停把摄像头复位的问题。STM32F103C8T6 TB6612FNG OpenMV Cam H7 Plus ------------- --------- ---------------- PB8 - AIN1 AO1 - 左电机 PB9 - AIN2 AO2 - 左电机- PB10 - BIN1 BO1 - 右电机 PB11 - BIN2 BO2 - 右电机- PA2/USART2_TX - OpenMV UART3_TX (P1) PA3/USART2_RX - OpenMV UART3_RX (P0)提示OpenMV的串口电平是3.3VSTM32也是3.3V逻辑可以直连。千万别把OpenMV引脚直接接到5V单片机的TX/RX上会让芯片永久烧毁。如果下位机是Arduino Uno这类5V设备中间一定要加电平转换模块。2.3 镜头、补光灯与安装位置对识别的影响硬件里最容易被忽视的是镜头选择和安装角度。OpenMV标配的是标准2.8mm焦距镜头视场角约70度这个参数决定了小车能看到多宽的输送带区域。如果你的运输场景里目标物离车头比较近10-30厘米2.8mm就够用如果目标物体积小、距离远建议换上4.5mm以上的长焦镜头代价是视场角变窄识别范围缩小。总之先测距离再选镜头不要图省事一直用默认镜头。补光灯也不是标配就万事大吉。OpenMV板上有一颗白光LED用于照明但它照射范围有限而且会导致高光反光反而让颜色阈值漂移。实际项目中建议在摄像头旁边装一盏扩散角60度左右的匀光LED灯板用独立开关控制——白天环境光足够就关掉傍晚或光线暗再开。至于安装位置摄像头离地高度建议在8-15厘米之间俯视角度30-45度。角度太平会把远处和近处的物体压缩在同一画面里阈值就不容易稳定了。3. OpenMV图像处理的三个核心算法与源码实现3.1 颜色识别从RGB阈值到LAB色彩空间OpenMV的颜色识别之所以要用LAB色彩空间是因为LAB把亮度和色度分开存放在不同通道上。RGB三个通道互相干扰明显——同一个红色物体光照强一点RGB三个值同时变大你就很难判断它还是不是红色。LAB中L通道管亮度A通道管红绿B通道管黄蓝我们只需要锁定A和B的取值范围亮度变化就不会对颜色判定产生决定性影响。在OpenMV IDE里打开工具-机器视觉-阈值编辑器选择帧缓冲区里的图片拖动LAB阈值滑块直到目标物体在画面中被完整地用红色蒙版框选出来。眼睛看到的结果直接可以导出成一组阈值粘贴到代码里。下面这段是最基础的颜色识别代码用于检测画面中的红色物体import sensor, image, time, math from pyb import UART # 初始化传感器 sensor.reset() sensor.set_pixformat(sensor.RGB565) # RGB565格式色彩信息完整 sensor.set_framesize(sensor.QVGA) # 320x240分辨率速度和精度兼顾 sensor.skip_frames(30) # 跳过前30帧让自动曝光收敛 sensor.set_auto_whitebal(False) # 关闭白平衡防止颜色抖动 sensor.set_auto_exposure(False) # 关闭自动曝光固定曝光值 sensor.set_auto_gain(False) # 关闭自动增益稳定亮度 # 红色目标物体的LAB阈值从阈值编辑器导入 red_threshold (30, 100, 15, 127, 15, 127) uart UART(3, 115200, timeout_char1000) # 串口3波特率115200 def find_target(): img sensor.snapshot() blobs img.find_blobs([red_threshold], pixels_threshold200, area_threshold200, mergeTrue) if blobs: largest_blob max(blobs, keylambda b: b.pixels()) return largest_blob return None while True: blob find_target() if blob: x_center blob.cx() # 目标中心x坐标 area blob.pixels() # 目标面积 # 将识别结果通过串口发送格式X坐标,面积\n uart.write((x:%d,a:%d\n % (x_center, area)).encode()) time.sleep(10) # 每个循环间隔10ms让出CPU给图像采集代码逻辑并不复杂先固定摄像头所有自动参数然后每一帧去寻找符合红色阈值的色块取最大的一块作为目标。这里有几个值得注意的参数pixels_threshold200是过滤掉噪点的最小像素数如果画面里出现了很多小碎块就调大这个值比如500mergeTrue表示相邻色块会合并成一个整体处理被遮挡的物体时很有用area字段代表色块面积这个值在后续判断物体距离远近时很有用——物体靠近摄像头面积自然变大。3.2 AprilTag识别运输场景里精度最高的定位方案颜色识别有个天然缺陷一旦环境里有多个相同颜色的物体你分不清哪个才是要抓的那个。更可靠的方式是给每个目标物贴一个AprilTag二维码它携带唯一的ID编号相当于给物体做了身份标记。AprilTag甚至可以计算出目标在三维空间中的相对位置这正是运输小车做精准对接时需要的。OpenMV对AprilTag的底层支持已经封得很完善了你不需要管它内部是怎么做四边形检测和编码解码的只要保证画面里Tag足够大、足够清晰。为了让系统识别更稳定建议把Tag的实际边长size字段写进find_apriltags的参数里这样就能直接计算出到目标的距离用于后续的抓取时机判断。import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.GRAYSCALE) # AprilTag识别用灰度图就够速度更快 sensor.set_framesize(sensor.QQVGA) # 160x120分辨率跑得更快 sensor.skip_frames(30) sensor.set_auto_gain(False) # 必须关闭增益否则Tag识别率降低 uart UART(3, 115200, timeout_char1000) TAG_SIZE_CM 5 # AprilTag的实际边长单位厘米 TAG_FAMILIES image.APRILTAG_TAG36H11 # 使用TAG36H11家族支持587个独立ID while True: img sensor.snapshot() tags img.find_apriltags(familiesTAG_FAMILIES, fx945.0, fy945.0, cx80.0, cy60.0) for tag in tags: # tag.rotation是旋转角度tag.id是Tag编号 # 计算目标相对摄像头的三维坐标 x_trans tag.x_translation() * TAG_SIZE_CM y_trans tag.y_translation() * TAG_SIZE_CM z_trans tag.z_translation() * TAG_SIZE_CM # 输出数据格式id,x平移,y平移,z平移,旋转角 msg id:%d,x:%.1f,y:%.1f,z:%.1f,r:%.1f\n % ( tag.id, x_trans, y_trans, z_trans, math.degrees(tag.rotation())) uart.write(msg.encode()) # 在图像中画出Tag的边框方便现场观察 img.draw_rectangle(tag.rect(), color(255, 0, 0)) img.draw_cross(tag.cx(), tag.cy(), color(0, 255, 0)) time.sleep(10)fx和fy是摄像头焦距的像素表示分别对应x和y方向。它们的作用是把图像坐标映射成空间坐标默认值是从OpenMV官方标定数据中拿到的。如果你换了不同焦距的镜头这两个值就不再准确需要用棋盘格做一次相机标定得到新的fx、fy、cx、cy。cx和cy是光心在图像中的坐标正常情况下就是画面中心点对应QQVGA分辨率下的(80, 60)。注意tag.x_translation()返回的是归一化单位对应焦距为1时的坐标所以要乘以Tag的真实边长才能得到物理世界中的厘米数值。3.3 形状识别不依赖外部标记的补充方案有些场景不允许给货物贴AprilTag比如原始外包装的纸箱、编织袋这种情况下可以用形状匹配作为补充方案。OpenMV支持find_circles()找圆形和find_rects()找矩形它们都是基于图像边缘梯度和霍夫变换原理实现的。import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(30) sensor.set_auto_whitebal(False) while True: img sensor.snapshot() # 检测圆形threshold越大圆越完美才会被接受 circles img.find_circles(threshold3500, x_margin10, y_margin10, r_margin10, r_min20, r_max80) for c in circles: # c是圆的参数对象c.x()和c.y()是圆心坐标 img.draw_circle(c.x(), c.y(), c.r(), color(255, 0, 0)) print(circle at x%d, y%d, radius%d % (c.x(), c.y(), c.r())) # 检测矩形不限制旋转角度 rects img.find_rects(threshold10000) for r in rects: img.draw_rectangle(r.rect(), color(0, 255, 0)) # r.magnitude()返回置信度数值越高代表越像标准矩形 print(rect corners: %s % str(r.corners())) time.sleep(50)find_circles的threshold参数表示累加器阈值类似霍夫变换里的投票数数值越大找到的圆越标准但也越容易漏检。实际调试时如果真实圆形的边缘因为光照产生残缺可以把threshold从3500降低到2000左右。find_rects返回的结果包含四个角点坐标corners可以通过角点之间的几何关系计算出矩形的旋转角度和中心点然后用这些参数来引导小车对准目标。4. 视觉信息如何驱动运输小车运动4.1 OpenMV与STM32的串口通信协议设计视觉处理的结果必须可靠地传到运动控制层。首选方案是UART串口因为它实现简单、协议透明不牵扯I2C地址配置或SPI片选逻辑。涉及通信协议设计核心原则是上行发状态、下行发指令每一步都要有明确的帧头、载荷和校验。以下代码是OpenMV端的串口就绪判断和消息发送封装。实际项目中不建议把裸字符串直接发给STM32因为字符串解析在C语言里要写一堆坑人的分隔逻辑。更稳的做法是定义一个有固定帧头的字节格式比如帧头是0xA5后面依次跟上目标的坐标高低字节和状态码最后加一个校验字节。from pyb import UART import sensor, image, time, struct sensor.reset() sensor.set_pixformat(sensor.GRAYSCALE) sensor.set_framesize(sensor.QQVGA) sensor.skip_frames(30) uart UART(3, 115200, timeout_char500) # 定义通信协议 # [0]帧头 0xA5 # [1]识别状态 0x01找到目标 0x00无目标 # [2]目标X坐标 低字节 # [3]目标X坐标 高字节 # [4]目标Y坐标 低字节 # [5]目标Y坐标 高字节 # [6]校验和 (0xA5状态X低X高Y低Y高) 0xFF FRAME_HEADER 0xA5 CHECKSUM_MASK 0xFF def send_target_status(found, x_coord, y_coord): x_lo x_coord 0xFF x_hi (x_coord 8) 0xFF y_lo y_coord 0xFF y_hi (y_coord 8) 0xFF checksum (FRAME_HEADER found x_lo x_hi y_lo y_hi) CHECKSUM_MASK # 按顺序打包成字节流发送struct.pack保证小端字节序 frame struct.pack(BBBHHB, FRAME_HEADER, found, 0, x_coord, y_coord, checksum) uart.write(frame)提示struct.pack(BBBHHB)里的表示小端字节序B是无符号字节H是无符号短整型占2字节。用struct打包而不是手动拼接字节字符串可以把可读性和可维护性都提上去下位机解析时只要按同样的格式解包就行。在运输小车的实际使用场景里坐标的分辨率值得思考。QQVGA分辨率是160x120用16位表示坐标已经绰绰有余将来如果换成VGA分辨率也不用改协议。发送频率不用太激进10Hz足够——小车运动控制的响应时间是毫秒级视觉帧率是瓶颈10Hz的更新率已经能保证80ms以内的视觉延迟这比大多数底盘机械结构的响应时间还快。太频繁的串口发送反而会占用MCU的宝贵时间挤占图像处理资源。4.2 STM32端的数据解析与运动决策STM32端的重点不是串口接收本身而是怎么保证数据不丢失、不粘包、不因误码导致小车乱撞。经典的解决方案是使用状态机解析。只要接收缓冲区里读到0xA5帧头就进入载荷接收状态确认收够6个字节后再做校验和比对。校验通过才把坐标值更新到全局变量里否则全部丢弃并回到等待帧头状态。// STM32端串口接收状态机 typedef enum { FRAME_IDLE, // 空闲状态等待帧头 FRAME_DATA, // 已收到帧头接收载荷 FRAME_CHECK // 接收完成校验数据 } FrameState; uint8_t rx_buffer[7]; // 7字节缓冲区帧头6字节载荷 uint8_t state FRAME_IDLE; uint8_t rx_index 0; // 串口中断处理函数USART2_IRQHandler中调用 void process_rx_byte(uint8_t data) { if (state FRAME_IDLE) { if (data 0xA5) { rx_buffer[0] data; rx_index 1; state FRAME_DATA; } } else if (state FRAME_DATA) { rx_buffer[rx_index] data; if (rx_index 7) { state FRAME_CHECK; } } else if (state FRAME_CHECK) { // 计算校验和这里rx_buffer[6]是收到的最末字节 uint8_t sum 0; for (int i 0; i 6; i) { sum rx_buffer[i]; } if (sum rx_buffer[6]) { // 校验通过解包坐标 int16_t x rx_buffer[2] | (rx_buffer[3] 8); int16_t y rx_buffer[4] | (rx_buffer[5] 8); update_tracking_target(x, y, rx_buffer[1]); } state FRAME_IDLE; rx_index 0; } }在运动决策上要引入一个关键概念死区。摄像头画面中心只有160像素宽如果把画面中心点当作目标对准的参考那中心点左右±5像素范围内的偏移应该视为已对准否则小车会一直左右微调、电机嗡嗡响而车子不走。把死区设计成可配置的宏现场调试时可以根据底盘的惯性大小调整。底盘惯性大死区就加大到±10像素底盘轻灵死区缩小到±3像素。运动策略还应该加上目标丢失保护。如果连续500ms都没有从串口收到有效目标坐标说明视觉算法跟丢或者目标移出视野。此时小车必须按预先定义的安全策略动作立即刹车停在原地不要盲目前进。很多搬运意外发生在自动模式下突然失去目标小车不知道是该继续走还是停下来最后撞上了货架。4.3 PID控制在输送过程中的作用运输小车的运动控制核心通常是PID控制器它的作用不是让小车本身跑得多精准而是让小车在负载变化比如装上货物后重量增加的情况下还能匀速前进。如果不控制速度空载时电机转速忽高忽低摄像头拍摄到的画面就会产生运动模糊直接拖累视觉识别。闭环控制在STM32里最朴素也最有效的形式是增量式PID。轮子上的霍尔编码器测得实际速度目标速度由视觉算法给出误差经过比例、积分、微分三个环节计算后输出PWM占空比的增量。增量式的优点是计算过程中不累加历史误差的绝对值不会出现积分饱和失控。// 增量式PID的核心计算调用周期建议5ms typedef struct { float Kp; // 比例系数 float Ki; // 积分系数 float Kd; // 微分系数 float target; // 目标速度 float last_error; // 上一次误差 float integral; // 积分累加值 }PID_Handle; int32_t pid_update(PID_Handle *pid, float measure) { float error pid-target - measure; pid-integral error; float delta pid-Kp * (error - pid-last_error) pid-Ki * error pid-Kd * (error - 2*pid-last_error pid-last_last_error); // 这里使用了二阶差分近似微分抗噪声能力更好 pid-last_last_error pid-last_error; pid-last_error error; return (int32_t)delta; }提示实际整定PID参数时不要一上来就三个参数一起调。遵循先P后I再D的顺序把Ki和Kd设为0慢慢加大Kp直到车子开始轻微震荡此时取Kp的50%作为设定值然后加入Ki消除稳态误差最后用极小值的Kd抑制超调。没经验就用试凑法经验值范围是Kp在3~15之间Ki在0.1~0.5之间Kd在0.01~0.1之间具体看你的电机齿比和负载。5. 让视觉小车在实际光线下稳定运行的三个进阶技巧运输小车从实验室走向收货区路面最大的拦路虎是光照。同一种颜色在日光灯下、窗户边、阴影里的LAB值可以差出两个数量级。我们在调OpenMV的色块识别时常常陷入一个误区不断调阈值让当前画面变得完美但换一个位置就失灵了。解决思路不是调阈值而是改处理流程。先亮出第一个技巧最有效的办法是关闭自动曝光、自动白平衡、自动增益这会让画面在局部光照变化时保持稳定但并发的问题是全黑环境下画面会彻底黑掉。所以不要直接关闭它们而是用scene_detection()或者定时读取画面的平均亮度做一个粗判断再决定是否切换到LED补光灯。在OpenMV里可以检测画面的L通道均值如果低于某个值就打开补光灯。但不要全部依赖灯光合理做法是补光灯只做夜间补偿正常光照下保持关闭否则会出现灯照区域亮、边缘暗的强对比识别结果更差。第二个进阶技巧是建立一套视野中的区域映射。让OpenMV识别出的视野坐标和小车的实际物理坐标的对应关系通过四角标定法计算出来。方法是在小车的工作台面上放一张A4纸用四个角点作为标定参照把图像坐标映射到世界坐标。这比直接用比例系数缩放要准得多因为摄像头是斜着安装的同样距离的图像误差在远处会放大好几倍。标定完成后OpenMV输出的坐标就是世界坐标系的厘米值下位机可以直接把它作为目标位置。第三个技巧是针对运输小车的实际流程把视觉任务拆成寻找阶段和对准阶段。寻找阶段只用低分辨率灰度图把帧率拉到最快尽快发现目标区域发现目标后切换成高分辨率彩色识别来做精确对准。OpenMV的set_framesize()是可以在运行时反复调用的但要注意切换后需要skip_frames(5)让感光元件稳定下来否则切换后的前几帧画面会花屏。这个动态分辨率切换的方案比一直跑高分辨率模式要快40%以上而且在大范围运输场景里更加贴合需求——移动过程中不需要精细识别停下来抓取时才需要高精度。最后建议在串口通信里加一个看门狗式的握手逻辑OpenMV每隔1秒主动发送一个心跳包STM32在超过2秒没有收到任何有效数据时自动切换成安全模式低速行驶并鸣笛提醒。这个改动只需20行代码却能够把视觉模块死机和底盘乱跑彻底隔离开让小车在真正进入仓储运行前就拦住一大堆隐性故障。本文还有配套的精品资源点击获取