做机器视觉项目,尤其是各种竞赛、毕设和DIY产品原型,OpenMV识别颜色几乎是最常被问到的话题。很多人一上来就抄官方示例代码,烧进去发现要么识别不到,要么乱框一气,然后就开始怀疑板子坏了。其实OpenMV颜色识别的源码本身并不复杂,真正的门道全在阈值怎么取、参数怎么配、现场环境怎么处理。这篇文章我就把完整代码、每个参数的含义、调试工具的使用方法,以及我在实际项目里踩过的坑一并讲清楚,保证你从头到尾看明白。
1. 颜色识别项目,为什么大多数人都绕不开OpenMV
1.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到100;A代表从绿色到红色的分量,负值偏绿、正值偏红;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_threshold=200, area_threshold=200) 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, roi=Auto, x_stride=2, y_stride=2, invert=False, area_threshold=10, pixels_threshold=10, merge=False, margin=0, threshold_cb=None, merge_cb=None)。新手通常只关心前几个参数,但有几个坑必须提前知道。
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_threshold=100, area_threshold=100)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, bits=8, parity=None, stop=1) color_id = 1 # 1=红色, 2=绿色, 3=蓝色 while True: img = sensor.snapshot() blobs = img.find_blobs([red_threshold]) if blobs: largest = max(blobs, key=lambda b: b.pixels()) x = largest.cx() y = largest.cy() data = ustruct.pack("<bbhhb", 0xA5, color_id, x, y, 0x5A) uart.write(data)使用max(blobs, key=lambda 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], roi=roi, pixels_threshold=100, area_threshold=100) if blobs: largest = max(blobs, key=lambda b: b.pixels()) error = largest.cx() - img.width() // 2 print("ERROR:", error)这里有两个细节值得注意。一个是ROI只取画面下半部分,因为远处的线没有参考价值,而且弯道处远处的线容易误导控制逻辑。另一个是巡线对帧率非常敏感,推荐把分辨率降到QQVGA或更小,画面小一点处理速度快很多,控制也更流畅。也可以考虑用img.get_regression()做线性回归来直接得到线的倾斜角度和线性位置,这个方法在光线不好、线边缘模糊时往往比单纯找色块更稳。
最后再分享一点个人经验
OpenMV的颜色识别说起来就一个find_blobs函数,但真正把它用稳定,靠的不是学会这个API,而是肯花时间在现场反复测阈值、调参数。我做过的几个项目里,识别效果差的项目无一例外都是调完一次阈值就再也不管了,识别效果好的项目都是拿着实物在不同光线、不同角度下至少测了半小时以上。调阈值这件事没有捷径,但它也不会辜负你,一次细心调试换来的是很长时间的省心。你甚至可以专门写一个带串口打印的阈值测试脚本,把当前画面里所有色块的坐标和面积实时打出来,这样在现场就知道哪些是目标哪些是噪声。把这个基本功练扎实了,后续玩巡线、追踪、分拣都会轻松很多。