ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenART智能视觉小车实战:从图像处理到AprilTag增强现实追踪

OpenART智能视觉小车实战:从图像处理到AprilTag增强现实追踪 做智能视觉小车这件事说难不难说容易也真容易让人崩溃。我第一次把OpenART摄像头装到小车底盘上时以为只要接好线、烧个巡线程序就能跑结果一开机屏幕全是花的电机一转画面就开始抖动标签识别时远时近——那一个星期我几乎把能踩的坑都踩了一遍。今天这篇就把我从零搭OpenART图像处理小车到实现增强现实标签追踪的完整经验写出来覆盖硬件选型、图像处理算法、AR定位逻辑以及官方文档里大概率不会告诉你的避坑细节。适合正准备入门视觉小车的爱好者也适合已经拼好车但程序总跑不通的初学者。1. 选型前先想清楚你的视觉小车到底要干什么1.1 先定功能再定硬件很多朋友一上来就问“用哪块板子好”这个顺序其实是反的。智能视觉小车的核心不是板子而是你希望它具备什么能力。同样是视觉小车巡线、颜色分拣、AprilTag定位、数字识别这几件事对硬件的需求差异很大。先说巡线这个需求最简单只需要灰度摄像头加基本的二值化处理几十帧的帧率就够用。如果要做颜色识别就需要彩色图像和稳定的颜色空间转换这时候对镜头色彩还原、白平衡设置就有要求。再往上走做增强现实标签追踪比如识别AprilTag后在小车屏幕上绘制虚拟路径或提示框那就不光要识别图案还得解算出标签相对摄像头的位置和角度对图像分辨率、处理器算力、镜头畸变控制都会更敏感。我的建议是先在纸上把功能列表写清楚再倒推硬件需求。我当时的目标很明确完成巡线、颜色识别、AprilTag增强现实追踪这三件事并且希望全程跑在嵌入式平台上不依赖电脑。基于这个目标选了以STM32H7为核心的OpenART平台配套MicroPython开发环境后续也不需要为换板子推翻重写。1.2 OpenART和树莓派、OpenMV比赢在哪如果只做一两个固定场景的视觉识别树莓派加USB摄像头也能实现网上这类教程非常多。但真放到小车上树莓派的启动速度、供电稳定性、图像采集延迟都是问题。小车是移动设备电池供电空间有限树莓派4B峰值功耗动不动就七八瓦还得配散热对底盘压力很大。OpenART这类嵌入式视觉平台的优势在于它把摄像头、处理器、外设接口集成在同一块板子上图像采集、处理、控制输出可以在几十毫秒内完成闭环。相比树莓派它的功耗只有几瓦甚至更低上电即可运行不需要等待操作系统启动。相比OpenMVOpenART在引脚资源、内存、扩展性上又更充裕适合带电机驱动、舵机、显示屏、串口模块等外设的场景。另外还有一个很实际的因素开发体验。OpenART支持在OpenMV IDE里用MicroPython写代码改一行、点一下运行、立刻看到效果这对调参和debug来说太重要了。C语言做嵌入式视觉不是不行但每改一个阈值都要重新编译烧录效率低到让人怀疑人生。对比项OpenART嵌入式视觉板树莓派方案OpenMV方案启动时间秒级上电即跑十几秒到几十秒秒级功耗低适合电池供电较高需散热低开发语言MicroPythonPython为主MicroPython图像处理延迟低硬件直连较高链路长低外设扩展引脚丰富依赖扩展板相对较少适合场景移动视觉、控制闭环服务机器人、复杂AI教学、轻量视觉2. 硬件搭建里的细节底盘、摄像头和电源2.1 差速底盘与电机驱动的连接逻辑常见的小车底盘分为差速驱动和舵机转向两种。视觉小车我强烈建议选差速驱动也就是左右各一个电机靠两个轮子的速度差实现转弯。差速驱动的好处是转向半径小可以实现原地旋转调巡线PID时也更灵活不会出现舵机转向那种“先减速再打方向”的顿挫感。电机驱动板的选择上L298N是很经典的方案但它体积大、压降也大电池电压低的时候容易让逻辑电路不稳定。我更推荐用DRV8833或TB6612这类MOS管驱动芯片体积小效率高还能通过PWM直接控制转速。接线时注意电机供电和逻辑供电最好分开驱动板的逻辑电源可以从OpenART的5V引脚取电机电源单独从电池取这样能有效避免电机启动时拉低主控电压。如果你用的是带编码器的电机建议把编码器信号接上。编码器能让你知道轮子实际转速和目标转速的差距是后面做PID闭环的基础。没有编码器也能跑但遇到地面摩擦不均匀或者电池电压下降时小车会莫名其妙地跑偏排查起来非常头疼。2.2 摄像头安装高度和角度比你想象的更重要摄像头装多高、朝什么角度是决定图像处理稳定性的关键因素。装得太低画面里全是近处地面前方信息太少小车转弯时根本来不及反应装得太高视角虽然远了但画面里会出现大量背景干扰二值化时很容易把远方的物体误判成目标。我的经验是摄像头离地10到15厘米俯角15到30度让画面里近处地面的线条占屏幕下方三分之一左右。这个角度下巡线时能同时看到近处的线、中距离的线以及远处的转折点小车可以提前判断方向变化。还有镜头选型。OpenART默认配的是标准广角镜头在室内小车场景下够用但广角镜头边缘畸变比较明显做AprilTag位姿解算时标签靠近画面边缘会出现距离测量不准的问题。如果条件允许可以换一个畸变更小的镜头或者在代码里做简单的去畸变处理。之前我懒得处理畸变结果小车在标签偏左时明明该左转却直行查了半天才发现是边缘像素坐标偏差太大。2.3 供电问题为什么小车总是无故重启如果只挑一个最影响智能视觉小车稳定性的因素我会选供电。电机启动瞬间的电流冲击非常大如果电池、驱动板、主控板共用一条电源路径主控很容易电压跌落然后复位。这个问题的典型表现是小车静止的时候一切正常一加油门屏幕闪一下程序从头跑。解决思路是分开供电电机用动力电池直接供主控和传感器通过稳压模块单独供并把两者的地线共地。共地这个操作很多新手会忽略不共地的话PWM信号在电机启停时会产生剧烈电平波动轻则图像抖动重则直接烧引脚。另外要注意电池的放电能力。我用过一组性能较差的18650电池组标称容量不小但内阻高大电流放电时电压掉得厉害。后来换成了放电倍率更高的锂电池问题迎刃而解。如果你的小车出现“电压显示3.7V但一动就掉到2.8V”的情况别急着调程序先检查电池是不是带不动。3. 图像处理流程从一帧画面到可执行的指令3.1 图像预处理为什么要灰度化和二值化摄像头采集到的原始图像数据量非常大QVGA分辨率的一帧RGB565图像就有约15万字节。如果在彩色图上直接做复杂运算嵌入式处理器的负载会很高。所以实际项目中预处理的第一步通常是灰度化把三通道的彩色图压缩成单通道灰度图数据量直接降到三分之一。灰度化之后是二值化也就是根据灰度值把像素分成两类目标和非目标。以巡线为例如果赛道是黑线白底设定一个阈值灰度值低于阈值的像素置为白色1其他置为黑色0这样图像就变成了只有黑白两个值的矩阵。二值化的意义在于它把问题从“判断一个像素像不像线”简化成了“这个像素是不是线”后续的寻找色块、计算偏移都基于这个纯净的输入。不过“先灰度再二值化”只是最基础的流程。当你做颜色识别时灰度化会把不同颜色的信息丢掉这时候更需要保留色彩的LAB颜色空间来处理后面会单独讲。3.2 巡线算法中心偏移量是怎么算出来的巡线的核心不是“看到线”而是“算出车身相对线的偏移量”。有了这个偏移量再把它映射成两个电机的速度差就完成了从视觉到控制的闭环。我在OpenART上实现巡线的思路分四步。第一步定义感兴趣区域ROI只处理画面下方三分之一区域减少远处干扰第二步在ROI内做二值化找出亮色线条第三步用find_blobs找出最大面积的色块色块中心点作为当前线的位置第四步计算色块中心横坐标与画面中心横坐标的差这个差值就是偏移量error。import sensor, image sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time2000) sensor.set_auto_exposure(False) sensor.set_auto_whitebal(False) GRAY_THRESHOLD (128, 255) # 白线黑底按实际场景调整 def get_error(img): # 只取画面下方1/3作为ROI roi (0, img.height() * 2 // 3, img.width(), img.height() // 3) blobs img.find_blobs([GRAY_THRESHOLD], roiroi) if not blobs: return None largest max(blobs, keylambda b: b.pixels()) center_x largest.cx() error center_x - img.width() // 2 return error while True: img sensor.snapshot() err get_error(img) if err is not None: # err为正说明线偏右为负说明线偏左 print(error:, err)这里有个容易被忽略的点ROI的坐标单位是像素而且坐标系原点是图像左上角。如果你把ROI定义错了二值化出来的线和画面里的线对不上调半天阈值也没用。建议在开发环境里先把ROI区域用矩形画出来确认框住的是想处理的区域再往下写。3.3 形态学处理膨胀与腐蚀的妙用二值化之后的图像往往带着很多噪声点尤其是地面有纹理、有反光时白色区域里会掺杂不少孤立的小点黑色背景里也会冒出一些碎块。这时候就要用到形态学操作最基础的就是膨胀和腐蚀。膨胀的效果是把白色区域向外扩张把附近细小的间隙填上腐蚀的效果是向内收缩把孤立的噪点吞掉。实际操作中我通常先做一次腐蚀去掉地面纹理产生的杂点再做一次膨胀把线条本身残缺的部分补回来。顺序不能乱如果先膨胀后腐蚀会把噪声也一起放大处理完反而更脏。OpenMV系列固件提供了一个好用的combined方法可以在二值化后直接调用img.erode(1)和img.dilate(1)其中参数表示执行次数。次数不要贪多一两遍就够了做多了线条会明显变粗偏移量计算会引入额外误差。如果你做的是赛道巡线尤其要注意这点——线变粗1个像素可能感知不出来但弯道里的误差累积起来车速一快就会冲出赛道。3.4 颜色识别LAB空间为什么更好用做颜色识别时RGB空间并不合适因为它对光照变化太敏感。同一个红色物体在亮光和阴影下RGB值能差出好几倍。LAB颜色空间把亮度信息单独放在L通道把颜色信息放在A和B通道这样即使亮度变化A和B的值也相对稳定识别率会高很多。在OpenART上用LAB做颜色识别思路和二值化很类似只不过阈值变成了六个数L的最小最大、A的最小最大、B的最小最大。这个六个值怎么确定不用靠猜开发环境里有阈值编辑器实时显示图像的同时可以直接框选目标区域自动计算阈值范围非常方便。颜色识别做完之后通常还会加两个过滤条件面积过滤和矩形度过滤。面积过滤可以排除小噪声矩形度过滤可以排除那些形状明显不对的区域。比如识别红色小球我会要求色块面积大于某个值而且外接矩形宽度和高度比例接近1比1。这两个条件可以挡掉大量误识别。4. 增强现实落地让小车认识标签并“看见”虚拟标线4.1 AprilTag为什么比普通色块更适合定位增强现实在智能小车上的应用最常见的形式就是让小车识别特定标签并根据标签的位置、方向决定下一步动作。这里我用的是AprilTag它不是简单的颜色块而是一套设计好的黑白方格编码图案图案内部带有校验信息可以通过解码得到标签的ID、位置、旋转角度甚至标签平面相对于摄像头的三维位姿。相比普通色块AprilTag最大的优势是抗干扰和唯一性。普通色块只要颜色接近就会被误判而AprilTag即使被部分遮挡、旋转、倾斜只要还在可识别范围内就能稳定解算。这让我想到自动驾驶里的视觉定位——本质上都是通过图像中的已知标记推算自身位置区别只是AprilTag是人为放置的标记而自动驾驶用的是车道线、路牌等自然标记。在OpenART上识别AprilTag不需要自己写复杂的特征提取算法固件里已经内置了find_apriltags接口。它的实现原理是基于四边形检测和编码解码先在图像里找候选的四边形轮廓再通过单应性矩阵把图案投影到标准坐标系最后比对编码库确认标签ID。4.2 在OpenART上跑通AR标签识别的完整步骤我的AR标签识别程序分为三步初始化摄像头参数、循环采集图像并查找标签、把标签信息可视化并输出。初始化时要特别注意两点一是关闭自动曝光和自动白平衡让图像亮度保持稳定二是把分辨率设为VGA或更高因为AprilTag的解码对细节要求比较高QVGA下远距离小标签可能完全识别不出来。识别主循环的核心代码如下import sensor, image sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.VGA) sensor.skip_frames(time2000) sensor.set_auto_exposure(False) sensor.set_auto_whitebal(False) while True: img sensor.snapshot() tags img.find_apriltags(familiesimage.TAG36H11) for tag in tags: img.draw_rectangle(tag.rect(), color(255, 0, 0)) img.draw_cross(tag.cx(), tag.cy(), color(0, 255, 0)) img.draw_string(tag.x(), tag.y(), ID:%d % tag.id(), color(255, 255, 0)) print(ID:, tag.id(), 距离估算:, tag.z_translation())这段代码运行后屏幕上每个被识别到的AprilTag都会被红色矩形框住中心画一个绿色十字旁边再标出ID。所谓的增强现实效果在这一步就已经初步出现了——你在屏幕里看到的标签周围多出来的框和文字就是视觉系统叠加出来的虚拟信息。4.3 把标签位姿变成小车的行为指令识别标签只是第一步真正让小车“听懂”标签需要把位姿信息映射成行为指令。比如我布置了一个简单场景小车看到ID为1的标签就左转看到ID为2的标签就右转看到ID为3的标签就停车并亮灯。这里有个关键点tag.z_translation()返回的是标签到摄像机的距离估计单位是毫米但这个数值会受镜头畸变和标签实际尺寸影响默认是按固定尺寸计算的。如果标签打印尺寸和默认值不一致一定要通过tag_size参数重新指定否则距离信息完全不可信。我就是被这个坑过打印的标签比默认尺寸小一半识别到的距离比实际远了将近一倍小车该减速的地方没减速直接撞上了障碍物。把位姿转行为指令时还可以结合增强现实的思想在图像上画出小车的预期行驶路径。我试过在识别到标签后根据标签的中心坐标和旋转角度在画面里画一条从画面底部延伸到标签位置的虚拟引导线。视觉效果很直观调试时也能一眼看出算法判断的方向是不是合理。5. 从零调试遇到的问题与排查技巧5.1 图像层反光、过曝、流动光线视觉小车在室内调试最大的敌人就是光线。同样的代码上午跑得好好的下午换个角度就失灵十有八九是光照变了。二值化阈值是固定的但地面反射、灯光阴影都会让画面灰度发生偏移。解决思路有几个按优先级排列。第一关闭摄像头的自动曝光和自动白平衡让画面参数固定下来避免摄像头自己在不同亮度间跳来跳去第二调整镜头角度尽量避开正上方灯光直射造成的反光区域第三选择在固定时间、固定灯光下调试把环境变量控制住先跑通再考虑复杂场景。如果阈值漂移问题很严重还可以用自适应阈值的方法比如把ROI内的灰度直方图取波谷作为分割点而不是用固定阈值。这个方法在OpenMV里有现成的接口可以用但要注意它对噪声更敏感需要配合形态学处理。5.2 控制层转向震荡与PID参数整定小车巡线最常见的失控表现是左右摇摆越摆越大最后冲出赛道这就是典型的转向过度。原因是偏移量到电机速度差的映射系数太大或者只做了比例控制没有阻尼。我的调参经验是先用P参数让小车能“大致跟着线走”此时允许小幅摇摆然后加一点D参数消除振荡最后加I参数消除稳态误差。调D参数时尤其要小心它是对误差变化率的响应设置过大会导致转向抖动电机咔咔作响听起来就像齿轮在打架。另一个容易踩的坑是偏速度差之后要限制电机的最大转速变化率也就是加速度限制。否则小车当前速度很快时检测到急弯突然给一个很大的反向速度差轻则车轮打滑重则驱动板过流保护。这个限制可以在代码里用简单的限幅实现每次PWM变化量不超过某个上限。5.3 系统层串口日志与模块化验证从零搭建系统最怕的是所有模块一起联调。我坚持一个原则每次只调一个变量。硬件通电后先用串口打印系统状态确认主控、摄像头、电机驱动都正常再烧一个最简单的亮灯程序确认GPIO映射正确然后单独跑图像采集确认画面不花不抖最后才组合成完整逻辑。串口打印是调试过程中最能救命的工具。不要在代码里到处写print却不带标签建议统一格式比如[LINE] error: 12[TAG] id:2 dist:340。这样出现问题时翻日志一眼就能定位是哪部分异常。如果嫌串口麻烦也可以直接把调试信息画到图像上我经常这样用因为图像上看到的数据比一串串数字直观得多。5.4 常见问题速查表现象可能原因排查方向上电没反应电源接反、稳压模块损坏先量电压再查接线电机一动主控重启电源路径共用、电池供电不足分开供电共地换高放电倍率电池画面花屏闪烁镜头排线接触不良、供电电流不够重新插紧排线给摄像头独立稳压二值化后线条断断续续阈值不合理、反光干扰关闭自动曝光调整阈值加形态学处理巡线时车身剧烈摇摆P值过大或D值缺失降低P适当增加D限制PWM变化率AprilTag识别距离偏短分辨率太低、标签尺寸参数错误提高分辨率在find_apriltags中指定tag_size颜色识别时好时坏白平衡未关、环境光变化关闭自动白平衡改用LAB颜色空间固定光照6. 让小车更聪明的三个扩展方向6.1 用状态机管理多任务当小车同时具备巡线、颜色识别、AR标签识别能力后代码会变得很复杂。如果全靠一堆if else互相嵌套迟早会把自己绕晕。我推荐引入状态机把一次任务拆分成多个状态比如“寻找标签”“朝标签行驶”“识别颜色”“执行动作”每个状态里只做一件事状态之间通过条件触发跳转。状态机的价值不只是让代码更整洁它能让你逐步测试整个任务流程。比如我可以先单独测试“寻找标签”状态确认没问题后再让它跳转到“朝标签行驶”。每次只验证一个转换出问题能立刻定位是为了哪个环节。6.2 视觉与上位机的通信协议设计如果下一步想做更复杂的逻辑比如把小车采集到的图像实时传回电脑做处理或者用手机APP控制小车就需要设计通信协议了。我用的方式是串口加JSON格式OpenART把识别结果打包成JSON字符串发给上位机上位机解析后再下发控制指令。设计协议时要注意一点传输频率不需要太高。图像处理帧率可能到了30帧每秒但真正需要发给上位机的可能只是每5帧里的一个结果或者只有状态变化时发一次。我的习惯是只在关键状态变化时上报数据否则串口会被刷爆反而影响控制指令下发。6.3 从视觉到控制PID参数整定的整体思路扩展方向说再多最终都绕不开视觉和控制两条腿走路。视觉负责“看”控制负责“动”。很多小车跑不好不是算法不行而是视觉输出到电机控制的这个桥梁没搭好。我最后的建议是把视觉部分和控制部分彻底解耦。视觉模块只负责输出目标位置、距离、方向这些语义化信息控制模块只负责接收这些信息转换成电机指令。调试时先给控制模块喂假数据比如固定说“目标在左侧”看它转不转、转多久再给视觉模块配一个实时显示画面确认它的输出值是否符合直觉。两边都正常了再合并调试。这样分开验证整个系统的可靠性会高很多也方便后续把视觉部分升级成更复杂的模型——比如如果你以后想用CNN做目标分类会发现图像处理为什么用CNN而不用普通前馈网络这个问题在嵌入式小车上尤其直观图像本身是局部相关的普通前馈网络把每个像素独立对待参数量爆炸不说还学不到空间特征CNN通过卷积核的局部感受野和共享权重既减少了计算量又天然适合处理图像对算力有限的嵌入式平台来说这种差异几乎是决定性的。我在整个项目里最大的体会是智能视觉小车的难点从来不是某一个具体功能而是把图像处理、增强现实定位、控制决策串成一条稳定链路的系统能力。每一步单独看都不算复杂但连起来之后任何一环的不稳定都会被放大。所以别急着追求一步到位先把一个功能调到足够稳再往后走。最后再分享一个小技巧所有阈值参数、PID参数都建议用一个配置文件或常量区集中管理并写上注释说明在什么环境下整定的。这样过了两周你回头调试时不用靠回忆去猜当时为什么设成这个值。
返回列表