
做机器视觉项目尤其是各种竞赛、毕设和DIY产品原型OpenMV识别颜色几乎是最常被问到的话题。很多人一上来就抄官方示例代码烧进去发现要么识别不到要么乱框一气然后就开始怀疑板子坏了。其实OpenMV颜色识别的源码本身并不复杂真正的门道全在阈值怎么取、参数怎么配、现场环境怎么处理。这篇文章我就把完整代码、每个参数的含义、调试工具的使用方法以及我在实际项目里踩过的坑一并讲清楚保证你从头到尾看明白。1. 颜色识别项目为什么大多数人都绕不开OpenMV1.1 OpenMV在颜色识别场景里的定位OpenMV本质上是一块集成了摄像头、处理器和一堆外设接口的嵌入式开发板它跑的是MicroPython核心库里有现成的图像处理函数所以不需要自己去实现复杂的视觉算法。对于颜色识别这类任务方案其实不少但OpenMV几乎是性价比和上手速度最平衡的选择。拿它和树莓派加USB摄像头比树莓派性能强、能做更复杂的深度学习但整个系统需要额外解决供电、系统启动时间、Python环境、摄像头驱动等一系列问题。拿它和纯单片机方案比单片机从头去写颜色识别算法光是色彩空间转换和像素遍历就够折腾几个星期。OpenMV把底层全部封装好了你只需调库函数传参数这在快速原型验证、教学演示、中小型自动化项目里非常实用。我接触过不少实际项目比如机器人巡线、物料分拣、颜色小球追踪基本都是一块OpenMV加几个串口引脚就搞定了整个视觉前端主控那边只管接收识别结果。这种工作方式的好处在于视觉逻辑和运动控制逻辑完全解耦调摄像头算法不需要反复动底盘代码出错也容易定位。1.2 颜色识别为什么不用RGB而用LAB很多新手看到OpenMV里的阈值是一组六个数字比如(0, 100, -128, 127, -128, 127)根本不知道这是什么东西。这里必须先说清楚OpenMV里颜色识别默认使用LAB色彩空间而不是我们平时熟知的RGB。RGB的三个分量分别是红、绿、蓝它跟设备强相关同一块红色在不同亮度、不同色温的灯光下RGB数值波动非常大。LAB色彩空间则把颜色拆成了三部分L代表亮度范围0到100A代表从绿色到红色的分量负值偏绿、正值偏红B代表从蓝色到黄色的分量负值偏蓝、正值偏黄。LAB的设计目标就是让颜色的描述更接近人眼感知并且把亮度和颜色信息分开。在OpenMV里识别的六个数字就是L、A、B各自的最小值和最大值顺序是(L_min, L_max, A_min, A_max, B_min, B_max)。由于LAB把亮度和色度分开你在调阈值时通常可以只微调A和B的范围而不需要像RGB那样三个分量都要随着环境变化反复改。这也是OpenMV官方默认用它做颜色识别的原因。2. 搭环境这一步就淘汰了一半初学者2.1 IDE安装与固件更新跑OpenMV代码必须在官方IDE里进行IDE的名字就叫OpenMV IDE目前版本迭代到了4.x。不要随便在第三方网站上找下载包直接去OpenMV官网下载对应自己系统的版本就行。Windows、macOS、Linux都有配套安装包。装好IDE之后第一步是连接板子。用USB线把OpenMV接到电脑上IDE右下角的连接按钮会变成可点击状态点击后会显示连接成功。如果是第一次使用大概率会提示固件版本过旧这时候IDE会弹出固件升级窗口。升级过程很简单按住板子上BOOT按钮再插USB线进入DFU模式IDE就能自动烧写最新固件。很多朋友遇到连不上板子的问题往往不是线坏了而是没装串口驱动Windows下提示未知设备时去设备管理器看一下装上FTDI或ST官方驱动就能解决。2.2 连接失败与串口识别的典型错误连接失败是出现频率最高的新手问题我这里列几种高发情况。一种是最gb的供电问题有些廉价USB线只走电源不走数据线换根带数据传输的线立刻好。另一种是端口被占用如果电脑上开了多个串口调试助手占用了OpenMV的COM口IDE就会连不上关掉所有占用程序再试。连接成功之后建议先跑一个最简单的点亮摄像头程序确认图像能正常出画面import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time 2000) while True: img sensor.snapshot()这段代码的作用是初始化摄像头、设定像素格式为RGB565、画面尺寸为QVGA然后持续抓取画面。只要IDE的帧缓冲区里能看到实时画面说明环境搭建基本成功了。凡是走到这一步的人后面调颜色识别基本都不会再有环境层面的麻烦。3. 源码逐行拆解让OpenMV知道红色长什么样3.1 官方示例代码的完整逻辑颜色识别最经典的一份源码长这样import sensor, image, time # 初始化摄像头 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time 2000) sensor.set_auto_whitebal(False) # 红色阈值 (L_min, L_max, A_min, A_max, B_min, B_max) red_threshold (30, 100, 15, 127, 10, 60) clock time.clock() while True: clock.tick() img sensor.snapshot() blobs img.find_blobs([red_threshold], pixels_threshold200, area_threshold200) for blob in blobs: img.draw_rectangle(blob.rect()) img.draw_cross(blob.cx(), blob.cy()) print(FPS: , clock.fps())这段代码看起来短实际上已经把颜色识别的主干逻辑全涵盖了。先初始化摄像头再定义一组颜色阈值然后在循环里抓图、调用find_blobs查找色块、用draw_rectangle和draw_cross在画面上画框和十字最后打印帧率。理解了这个框架后续所有复杂功能都是在它上面做增补。3.2 find_blobs函数的关键参数解析find_blobs是OpenMV颜色识别最核心的函数它的完整签名是find_blobs(thresholds, roiAuto, x_stride2, y_stride2, invertFalse, area_threshold10, pixels_threshold10, mergeFalse, margin0, threshold_cbNone, merge_cbNone)。新手通常只关心前几个参数但有几个坑必须提前知道。thresholds必须是一个列表里面可以放一组或多组六元数阈值。如果放了多组函数会在每组阈值下分别找色块。pixels_threshold是像素数量下限一个色块的总像素数低于这个值会被忽略。它的作用是滤掉画面里的噪点和小色块。area_threshold是色块外接矩形面积的下限类似地用来过滤过小的零碎区域。merge决定是否合并相邻的色块。如果True相邻且颜色接近的色块会被合并成一个大块如果False每个独立区域都单独返回。roi是感兴趣区域传入一个元组(x, y, w, h)让查找只在指定区域进行。在我的实际使用经验里pixels_threshold和area_threshold是必须调的两个参数。画面里如果出现大量噪点把pixels_threshold调高一些如果色块边缘不连续导致同一个目标被拆成多块把area_threshold调低、merge设为True通常能改善。3.3 从blob对象里能拿到的信息find_blobs返回的是一个blob对象列表每个blob对象封装了查找到的色块的全部几何信息常用属性包括属性含义rect()色块外接矩形的(x, y, w, h)cx()色块中心点x坐标cy()色块中心点y坐标pixels()色块所占像素总数area()色块外接矩形面积rotation()色块主轴旋转角度code()这个色块匹配的是哪组阈值其中cx和cy用得最多机器人追踪小球、巡线纠偏都靠这两个值计算目标的方向偏差。rotation在巡线里特别有用它可以告诉你线的方向是水平的还是倾斜的。code这个属性在做多颜色识别时很有用它能区分当前色块匹配的是第几组阈值从而知道这是什么颜色。4. 阈值调试整个颜色识别里最考验耐心的环节4.1 用阈值编辑器快速取色手动去猜那六个数字的数值范围效率极低我不推荐任何人这么做。OpenMV IDE自带一个非常好用的工具Tools菜单下的Threshold Editor点了会弹出阈值编辑器窗口。用这个工具的流程是先把摄像头对准目标颜色让画面稳定出现在IDE帧缓冲区里然后打开阈值编辑器在左侧预览画面里用鼠标框选出目标区域右边的直方图和下面的阈值滑条会自动调整到匹配结果。六个滑条分别对应L、A、B的最小值和最大值编辑器底部会把当前生效的阈值以元组格式展示出来直接复制粘贴到代码里就行。实际操作中你会发现框选不同区域时阈值会有明显变化所以框选时尽量把目标颜色的高光和阴影部分都覆盖进去这样得到的阈值会更抗光照变化。如果目标颜色在画面里有明暗渐变不要只框最亮的一小块那会导致识别很不稳定。4.2 LAB三个通道到底该怎么理解在手动微调阈值时理解LAB三个通道的意义能帮你快速判断该动哪个滑条。L通道管亮度从黑到白。如果你的目标颜色在不同光照下忽亮忽暗就需要把L的范围放宽比如从默认的中间范围扩大到二三十到九十多。但放宽L的代价是更容易把深色和浅色背景一起圈进来所以L范围要配合A和B一起收紧。A通道是从绿到红负值代表绿正值代表红。识别红色目标时A的最小值通常是主导条件一般设在20以上才能排掉粉色、紫色等接近色的干扰。识别绿色目标时A的最大值要设为负值比如-10以下。B通道是从蓝到黄负值代表蓝正值代表黄。识别蓝色目标不仅要A为负B也要负得明显识别黄色目标则要求B的正值够大。调整时记住一个原则先锁定目标颜色在A、B通道的特征方向再去管L通道的宽容度。4.3 阈值过宽与过窄的取舍阈值调试的本质就是一个过拟合与欠拟合的博弈。阈值过窄目标稍微暗一点或者被光照偏一点就找不到阈值过宽背景里乱七八糟的东西全被圈进来导致误识别率飙升。我的经验是先在实验室光线稳定的环境里把阈值调到刚好能框住目标再把A、B范围各放宽10到20然后把目标物体放到画面不同位置、不同角度测试观察有没有漏检或误检。如果放到阴影下就找不到说明L的下限太高了如果发现背景里的相近颜色被框进来说明A或B的界限放得太宽。这个来回调整的过程没法跳过只能靠实测数据说话。另外有一点很多人没注意find_blobs支持传入多组阈值这是做红和绿同时识别的基础。你可以分别给红色和绿色各定义一组阈值放在同一个列表里传给函数。但要注意这样做时返回的blob对象并不会自动告诉你它属于哪组阈值需要通过前面说的code()属性或者对每组阈值分别调用find_blobs来判断具体做法后面进阶部分会讲。5. 从调通到稳定实战中必然遇到的几个坑5.1 自动曝光与白平衡必须关掉这是我在所有OpenMV项目里都会反复强调的一件事。默认情况下OpenMV摄像头会自动调节曝光和白平衡来适应环境亮度这在拍照时是好功能但对固定光源下的颜色识别却是灾难。自动曝光会在画面亮度变化时自动调整传感器的曝光时间同一个目标物体的颜色数值会跟着漂移导致你辛辛苦苦调的阈值时而有效时而失效。自动白平衡更麻烦它会把画面里偏红的色调往白色方向拉让你要识别的红色在算法眼里变淡甚至变色。正确的做法是在初始化阶段就把自动白平衡关掉并根据场景决定是否锁定曝光sensor.set_auto_whitebal(False) sensor.set_auto_exposure(False)如果现场光线充足且稳定建议直接关闭自动曝光如果现场光源会波动那就保留自动曝光但要重新在真实光线下校准阈值。此外关掉自动白平衡后画面颜色可能会偏色这很正常因为此时画面反映的是真实场景的原始色调你要识别的目标颜色反而更稳定。5.2 光线变化的补偿思路固定光源下关掉自动参数基本就够用了。但如果你的设备要在不同时间段、不同灯光环境下工作比如一台沿着场地巡线的机器人上午和下午都要跑光线差异会相当大。光线补偿的思路有三个层级。第一个层级是改善物理环境给目标区域增加遮光罩或补光灯让光照尽量稳定。第二个层级是优化阈值策略用前面提到的放宽A、B范围的思路牺牲一点误检率来换取更高的召回率。第三个层级是在代码里做动态校准比如开机时先让摄像头对准一个已知颜色区域自动计算出当前环境下的阈值偏移量再把这个偏移量应用到预设阈值上。这个方案效果最好但实现复杂度也最高普通项目用前两个层级已经能覆盖大部分问题。另外还有一个容易被忽视的技巧识别深色背景上的亮色目标时可以在代码里用img.binary()把图像二值化把所有符合条件的像素置为白色、其余置为黑色然后再找色块。二值化后图像信息量大大降低处理速度更快误检也更少。但二值化会丢失目标的原始颜色只适合判断有没有和在哪的场景不适合需要区分多个颜色的项目。5.3 同类色干扰与ROI裁剪实战调通基础识别后最常见的翻车现场是目标红色圆球旁边出现了一个红色纸箱或者地面上有一块红色地砖。find_blobs可不懂你的项目语义它只按颜色找凡是在阈值范围内的都会返回。对付这种问题最直接的手段是裁剪ROI区域。如果你的相机安装角度固定目标只会在画面某个区域出现那在find_blobs里传入roi参数把搜索范围限定在目标可能出现的区域就能完全避开区域外的干扰物。代码写法很简单blobs img.find_blobs([red_threshold], roi(80, 40, 160, 120), pixels_threshold100, area_threshold100)ROI裁切还有个额外好处减少了需要扫描的像素范围查找速度会快不少。另外一个辅助思路是同时设置pixels_threshold和area_threshold把小块的误检区域过滤掉如果干扰物和目标大小接近那就只能依靠ROI或引入其他传感器信息了。6. 进一步扩展多颜色识别、串口通信与巡线6.1 一帧画面识别多种颜色的正确姿势实际项目里经常需要在同一帧画面中同时识别多种颜色比如分拣机器人要区分红、绿、蓝三种物料。实现方式有两种第一种是把多组阈值放进同一个列表调用find_blobs第二种是分别对每组阈值调用一次find_blobs。第一种方式代码简单但返回的blob不容易区分颜色必须靠blob.code()去判断匹配的是哪组阈值。第二种方式逻辑清晰每组的识别结果天然分好类代价是计算量翻倍。如果你的OpenMV性能吃紧、帧率掉得厉害就用第一种如果帧率无所谓、代码可读性优先就用第二种。我个人倾向于第二种调试时一目了然red_threshold (30, 100, 15, 127, 10, 60) green_threshold (30, 90, -50, -10, 0, 50) blue_threshold (20, 80, -20, 20, -60, -10) red_blobs img.find_blobs([red_threshold]) green_blobs img.find_blobs([green_threshold]) blue_blobs img.find_blobs([blue_threshold])再强调一次多颜色识别时optimum的阈值需要分别校准不要图省事用一组阈值跑所有颜色。校准方法跟单颜色完全一样逐个颜色过一遍Threshold Editor就行。6.2 与STM32等主控的串口通信实现OpenMV在很多项目里不是最终主控它只是视觉前端识别结果要通过串口发给STM32等主控去执行动作。串口通信的核心是把识别到的颜色和坐标打包成主控能解析的帧格式。最简单的协议可以这样设计数据帧长度固定帧头用0xA5第二个字节代表颜色ID第三和第四个字节代表目标中心点x坐标的高低位第五个字节代表目标中心点y坐标帧尾用0x5A。OpenMV端发送代码如下import ustruct from pyb import UART uart UART(3, 115200) uart.init(115200, bits8, parityNone, stop1) color_id 1 # 1红色, 2绿色, 3蓝色 while True: img sensor.snapshot() blobs img.find_blobs([red_threshold]) if blobs: largest max(blobs, keylambda b: b.pixels()) x largest.cx() y largest.cy() data ustruct.pack(bbhhb, 0xA5, color_id, x, y, 0x5A) uart.write(data)使用max(blobs, keylambda b: b.pixels())的目的是在多结果时只取面积最大的那个目标这是多目标追踪里常用的一个简化策略。153600波特率通常足够传坐标帧主控侧只要按帧头帧尾解析即可。串口通信的几个坑基本都集中在波特率不匹配、电压域不匹配和共地问题记得OpenMV的UART电平是3.3V如果主控是5V的TTL电平需要加电平转换模块。6.3 巡线项目里对颜色识别的特殊要求巡线是OpenMV颜色识别最经典的应用之一网上能搜到大量相关源码。巡线任务的核心逻辑其实特别简单找到画面里的线然后计算线中心点相对画面中心的偏差把偏差发给底盘去纠偏。常规写法是用find_blobs找到线色块然后取blob.cx()与画面中心的差值作为左右轮速差控制的输入roi (0, int(img.height() * 0.5), img.width(), int(img.height() * 0.5)) blobs img.find_blobs([line_threshold], roiroi, pixels_threshold100, area_threshold100) if blobs: largest max(blobs, keylambda b: b.pixels()) error largest.cx() - img.width() // 2 print(ERROR:, error)这里有两个细节值得注意。一个是ROI只取画面下半部分因为远处的线没有参考价值而且弯道处远处的线容易误导控制逻辑。另一个是巡线对帧率非常敏感推荐把分辨率降到QQVGA或更小画面小一点处理速度快很多控制也更流畅。也可以考虑用img.get_regression()做线性回归来直接得到线的倾斜角度和线性位置这个方法在光线不好、线边缘模糊时往往比单纯找色块更稳。最后再分享一点个人经验OpenMV的颜色识别说起来就一个find_blobs函数但真正把它用稳定靠的不是学会这个API而是肯花时间在现场反复测阈值、调参数。我做过的几个项目里识别效果差的项目无一例外都是调完一次阈值就再也不管了识别效果好的项目都是拿着实物在不同光线、不同角度下至少测了半小时以上。调阈值这件事没有捷径但它也不会辜负你一次细心调试换来的是很长时间的省心。你甚至可以专门写一个带串口打印的阈值测试脚本把当前画面里所有色块的坐标和面积实时打出来这样在现场就知道哪些是目标哪些是噪声。把这个基本功练扎实了后续玩巡线、追踪、分拣都会轻松很多。