引言:在计算机视觉和图像处理领域,颜色识别和色块追踪是实现目标检测、物体追踪、智能交互等应用的基础技术之一。通过分析图像中颜色的分布,我们可以识别并跟踪特定的物体或区域,进而为智能系统提供精确的视觉反馈。在嵌入式系统中,图像处理通常受限于硬件资源,因此需要高效且精确的算法来实现实时识别和追踪。
本文将以 K230 CanMV 开发平台为例,深入探讨如何利用其强大的图像处理能力,通过颜色识别技术实现对特定颜色物体的色块追踪。我们将介绍颜色识别函数find_blobs
的原理与用法,详细讲解如何使用该函数进行图像中的色块追踪,最后提供一个实际案例,帮助读者在实际项目中运用这一技术。
目录
一、颜色识别(find_blobs)概述
find_blobs
是 OpenMV 的图像处理函数,用于在图像中寻找并识别“斑点”(blobs)。这些斑点是指图像中具有相似颜色或亮度的区域。这个函数通常用于视觉检测和识别应用中,例如物体跟踪、颜色识别等。
CanMV支持OpenMV算法,同样可以使用find_blobs
二、函数讲解
blobs = img.find_blobs([thresholds], area_threshold=area_threshold, pixels_threshold=pixels_threshold, merge=True, margin=0)
-
thresholds: 这是一个包含颜色范围的列表,用于定义要查找的斑点的颜色范围。通常是一个包含两个或三个元素的元组。例如,
(100, 200, -64, 127, -128, 127)
表示 HSV 颜色空间中的范围,其中第一个和第二个值是 Hue,第三和第四值是 Saturation,最后两个值是 Value。 -
area_threshold: 斑点的面积阈值。只有面积大于这个值的斑点才会被返回。默认值是 0。
-
pixels_threshold: 斑点的像素数阈值。只有包含的像素数大于这个值的斑点才会被返回。默认值是 0。
-
merge: 是否合并相邻的斑点。设为
True
时,相邻的斑点会被合并成一个大斑点;设为False
时,斑点不会合并。默认值是True
。 -
margin: 用于合并斑点时的边距。设置为一个正整数,表示合并斑点时的最大距离。默认值是 0。
返回值:
find_blobs
函数返回一个包含斑点信息的列表。每个斑点都是一个 Blob
对象,通常包含以下属性:
-
cx
和cy
:斑点的中心坐标。 -
x
和y
:斑点的左上角坐标。 -
w
和h
:斑点的宽度和高度。 -
area
:斑点的面积(像素数)。
三、图像色块追踪方法介绍
image 模块为 Image 对象提供了 find_blobs()方法,用于查找图像中的所有色块,find_blobs() 方法如下所示:
image.find_blobs(thresholds, invert=False, roi, x_stride=2, y_stride=1,
area_threshold=10, pixels_threshold=10, merge=False, margin=0, threshold_cb,
merge_cb)
find_blobs()
方法在图像处理中用于根据指定的颜色阈值查找图像中的色块,并返回符合条件的 image.blob
对象列表。色块通常是指在图像中某一特定颜色范围内的连续像素区域。该方法支持对不同颜色空间和图像类型进行处理,允许在一定条件下筛选、合并或过滤色块。
方法参数解释
-
thresholds
该参数指定了颜色的阈值,用于定义哪些颜色区域需要被识别。它是一个元组列表,针对不同的图像和颜色空间,具体形式有所不同:- 灰度图像:对于灰度图像,
thresholds
应该是一个包含两个数值的元组(min_gray_value, max_gray_value)
,表示允许的灰度值范围。 - RGB565 图像:对于 RGB565 格式的图像,
thresholds
应该是包含六个值的元组(min_L, max_L, min_A, max_A, min_B, max_B)
,这六个值分别表示在 LAB 色彩空间下的 L、A 和 B 三个通道的最小值和最大值。LAB 色彩空间具有较好的色差感知效果,适用于在不同光照条件下进行色彩追踪。
例如,若要追踪红色和蓝色的色块,可以在阈值元组列表中分别传入对应的颜色范围。
- 灰度图像:对于灰度图像,
-
invert
该参数控制是否对颜色阈值进行反转操作。如果设置为True
,则追踪的不是在指定阈值内的色块,而是阈值之外的色块。默认为False
,即只关注在阈值范围内的色块。 -
roi (Region of Interest)
指定图像中感兴趣的区域。它用于限制图像处理的范围,减少计算量。若未指定该参数,则处理整个图像。 -
x_stride 和 y_stride
这些参数用于定义在图像处理中每次跳过多少个像素。若色块较大,增加这些步长可以提高效率,减少计算量。例如,x_stride=2
会让每次在 X 轴方向上跳过 2 个像素,y_stride=2
会在 Y 轴方向上跳过 2 个像素。 -
area_threshold
该参数用于过滤掉那些边界框区域小于指定值的色块。设定较大的area_threshold
可以去除一些过小的色块,以减小检测的噪声。 -
pixels_threshold
用于过滤掉那些像素数量少于指定值的色块。该参数通常用于避免误识别一些微小的、无意义的色块。 -
merge
该参数控制是否对相互重叠的色块进行合并。如果设置为True
,则会将边缘矩形相互交错且没有被过滤的色块合并成一个色块。若设置为False
,则不合并重叠的色块。 -
margin
该参数用于增大或减小检测到的色块边界矩形的大小,通常用于修正边界矩形的形状,确保色块的完整性。 -
threshold_cb (Threshold callback function)
这是一个回调函数,作用是在颜色阈值过滤过程中对被过滤的色块进行进一步筛选。该函数会接收到一个image.blob
对象,返回True
表示保留该色块,返回False
则表示过滤掉该色块。 -
merge_cb (Merge callback function)
这是一个回调函数,用于决定是否将两个即将合并的色块合并成一个。该函数会接收到两个image.blob
对象,返回True
表示合并色块,返回False
则禁止合并。
方法返回值
find_blobs()
方法返回一个包含 image.blob
对象的列表,每个 image.blob
对象表示一个在图像中找到的色块。每个色块包含以下信息:
- 色块的边界框位置和大小
- 色块的质心坐标
- 色块内像素的颜色值信息
import image
img = image.Image(size=(320, 240))
threshold = (8, 63, 8, 49, -77, -36)
blobs = img.find_blobs([threshold], False, (0, 0, img.width(), img.height()),
x_stride=2, y_stride=1, area_threshold=10, pixels_threshold=10, merge=True,
margin=10)
for b in blobs:
img.draw_rectangle(b.rect(), color=(255, 0, 0))
关于颜色阈值的设定,CanMV IDE 软件提供了“阈值编辑器”工具,可以方便地获取到想 要的阈值。通过依次点击 CanMV IDE 软件上方工具栏中的“工具”→“机器视觉”→“阈值编 辑器”,即可打开“阈值编辑器”工具,打开时会提示需要导入一张源图像,可以直接使用“帧 缓冲区”窗口中的图像,也可以从本地文件系统中选取,如下图所示:
四、示例模版
这里只列举一个单独的颜色追踪例子,具体demo还请查看固件自带虚拟U盘中的例程
- 初始化:导入库,设置摄像头和显示参数,初始化传感器和媒体管理器。
- 循环获取图像:通过传感器不断获取图像帧。
- 色块检测:通过
find_blobs
函数检测符合阈值条件的色块,并根据颜色编码进一步筛选。 - 绘制标识:对符合条件的色块进行绘制,如矩形框、十字标记、旋转角度等。
- 显示图像:将处理后的图像显示在屏幕上。
- 性能监控:输出帧率,监控程序性能。
- 清理资源:程序退出时清理传感器和显示资源。
# 单色代码跟踪示例
#
# 这个示例展示了使用 CanMV 相机进行单色代码跟踪。
#
# 颜色代码是由两种或更多颜色组成的斑点。下面的示例将
# 仅跟踪包含以下颜色的彩色物体。
import time, os, gc, sys, math
from media.sensor import *
from media.display import *
from media.media import *
DETECT_WIDTH = 640
DETECT_HEIGHT = 480
# 颜色跟踪阈值 (L Min, L Max, A Min, A Max, B Min, B Max)
# 以下阈值通常用于跟踪红色/绿色物体。您可能需要调整它们...
thresholds = [(12, 100, -47, 14, -1, 58), # 通用红色阈值 -> 索引为 0,所以代码 == (1 << 0)
(30, 100, -64, -8, -32, 32)] # 通用绿色阈值 -> 索引为 1,所以代码 == (1 << 1)
# 当 "merge=True" 时,代码会被“或”在一起以进行 "find_blobs"
# 只有那些像素数量超过 "pixel_threshold" 且面积超过 "area_threshold" 的斑点
# 才会被下面的 "find_blobs" 返回。如果更改相机分辨率,请更改 "pixel_threshold" 和 "area_threshold"。
# 必须设置 "merge=True" 以合并重叠的颜色斑点以形成颜色代码。
sensor = None
try:
# 使用默认配置构造一个Sensor对象
sensor = Sensor(width = DETECT_WIDTH, height = DETECT_HEIGHT)
# sensor复位
sensor.reset()
# 设置水平镜像
# sensor.set_hmirror(False)
# 设置垂直翻转
# sensor.set_vflip(False)
# 设置通道 0 输出大小
sensor.set_framesize(width = DETECT_WIDTH, height = DETECT_HEIGHT)
# 设置通道 0 输出格式
sensor.set_pixformat(Sensor.RGB565)
# 设置显示,如果您选择的屏幕无法点亮,请参考API文档中的K230_CanMV_Display模块API手册自行配置,下面给出四种显示方式
# 使用 HDMI 作为显示输出,设置为 VGA
# Display.init(Display.LT9611, width = 640, height = 480, to_ide = True)
# 使用 HDMI 作为显示输出,设置为 1080P
# Display.init(Display.LT9611, width = 1920, height = 1080, to_ide = True)
# 使用 LCD 作为显示输出
# Display.init(Display.ST7701, to_ide = True)
# 使用 IDE 作为输出
Display.init(Display.VIRT, width = DETECT_WIDTH, height = DETECT_HEIGHT, fps = 100)
# 初始化媒体管理器
MediaManager.init()
# sensor开始运行
sensor.run()
fps = time.clock()
while True:
fps.tick()
# 检查是否应该退出
os.exitpoint()
img = sensor.snapshot()
for blob in img.find_blobs(thresholds, pixels_threshold=100, area_threshold=100, merge=True):
if blob.code() == 3: # r/g 代码 == (1 << 1) | (1 << 0)
# 这些值依赖于斑点不是圆形的 - 否则它们会不稳定
# if blob.elongation() > 0.5:
# img.draw_edges(blob.min_corners(), color=(255,0,0))
# img.draw_line(blob.major_axis_line(), color=(0,255,0))
# img.draw_line(blob.minor_axis_line(), color=(0,0,255))
# 这些值在任何时候都是稳定的
img.draw_rectangle([v for v in blob.rect()])
img.draw_cross(blob.cx(), blob.cy())
# 注意 - 斑点的旋转是唯一的,仅限于 0-180
img.draw_keypoints([(blob.cx(), blob.cy(), int(math.degrees(blob.rotation())))], size=20)
# 将结果绘制到屏幕上
Display.show_image(img)
gc.collect()
print(fps.fps())
except KeyboardInterrupt as e:
print(f"user stop")
except BaseException as e:
print(f"Exception '{e}'")
finally:
# sensor停止运行
if isinstance(sensor, Sensor):
sensor.stop()
# 销毁display
Display.deinit()
os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
time.sleep_ms(100)
# 释放媒体缓冲区
MediaManager.deinit()
这段代码是一个基于色块跟踪的示例,使用 CanMV Cam
摄像头进行目标跟踪。它通过设定颜色阈值来识别图像中的特定颜色区域(色块),并结合色块的属性(如位置、旋转、长宽比等)进行进一步处理。下面将详细解析每个部分的作用和代码流程。
- time:用于处理时间相关的操作,例如计算帧率(fps)。
- os:用于操作系统相关的功能,例如退出点检查。
- gc:垃圾回收库,用于管理内存。
- sys:提供对 Python 运行时环境的访问。
- math:提供数学函数,特别是
math.degrees()
用于角度转换。
接下来的导入是涉及到图像采集、显示和媒体管理的自定义模块:
- media.sensor:提供摄像头传感器相关的功能。
- media.display:处理显示输出的相关功能。
- media.media:管理媒体相关的资源。
- 红色阈值:第一个元组
(12, 100, -47, 14, -1, 58)
用于识别红色区域(在 LAB 色彩空间中设置 L, A, B 三个通道的最小值和最大值)。 - 绿色阈值:第二个元组
(30, 100, -64, -8, -32, 32)
用于识别绿色区域。
每个阈值元组代表 LAB 色彩空间中三个通道的范围,配合 find_blobs()
函数可以找出图像中符合这些颜色范围的色块。
初始化了传感器,设置图像的宽度和高度,并指定图像格式为 RGB565
。这意味着每个像素会以 16 位存储,每个通道(红色、绿色、蓝色)分别使用 5 位、6 位和 5 位。
显示被初始化为虚拟显示 (Display.VIRT
),并设置分辨率为 640x480,帧率为 100。这里的显示操作是虚拟显示,适合开发和调试时在 IDE 中查看输出。
通过 MediaManager.init()
初始化媒体管理器,接着启动传感器运行。这意味着图像采集开始实时进行。
fps.tick()
用于记录帧时间,计算当前帧率。os.exitpoint()
检查是否需要退出程序。sensor.snapshot()
捕获当前图像帧。
find_blobs()
:基于之前定义的thresholds
参数,查找符合条件的色块。此方法返回一个包含色块的列表。pixels_threshold=100
:色块的像素数量必须大于 100。area_threshold=100
:色块的面积必须大于 100。merge=True
:合并重叠的色块,适用于多个颜色组合。
blob.code() == 3
:在本示例中,色块的代码为3
,即同时包含红色和绿色,这通过将1 << 0
和1 << 1
位运算或得到的结果。
色块处理:
blob.rect()
:获取色块的边界矩形,并绘制出来。blob.cx(), blob.cy()
:获取色块的质心坐标,并在图像上绘制十字。blob.rotation()
:获取色块的旋转角度,并绘制该角度的键点。
Display.show_image(img)
:将处理后的图像显示在屏幕上。gc.collect()
:进行垃圾回收,释放不再使用的内存。
通过 fps.fps()
输出当前帧率,用于调试和性能监控。
- 捕获
KeyboardInterrupt
异常(用户手动停止)以及其他基础异常,并打印出异常信息。 - 在
finally
块中停止传感器,反初始化显示设备,释放资源并确保应用退出前能进入睡眠状态。
五、实际案例
这段代码是一个基于色块跟踪的示例,旨在通过摄像头捕捉图像并检测特定颜色(红色)的物体。代码利用了 find_blobs
函数来检测图像中的红色区域,并对符合条件的色块进行标记和显示。
1. 导入必要的库
import time, os, gc, sys, math
from media.sensor import *
from media.display import *
from media.media import *
- time:用于处理时间相关的功能,如计算帧率。
- os:提供与操作系统交互的功能,例如检查退出点。
- gc:垃圾回收库,用于管理内存,避免内存泄漏。
- sys:提供对Python运行时环境的访问,通常用于调试或获取程序的系统信息。
- math:提供数学函数,这里使用
math.degrees()
来将弧度转换为度。
接下来的导入是与硬件和显示相关的自定义模块:
- media.sensor:提供与摄像头传感器相关的功能。
- media.display:处理图像显示的功能。
- media.media:用于初始化和管理媒体资源,如缓冲区。
2. 设置摄像头分辨率和颜色阈值
DETECT_WIDTH = 640
DETECT_HEIGHT = 480
# 只跟踪红色物体的阈值 (L Min, L Max, A Min, A Max, B Min, B Max)
red_thresholds = [(30, 68, 9, 127, -12, 73)] # 通用红色阈值 -> 索引为 0,所以代码 == (1 << 0)
-
DETECT_WIDTH
和DETECT_HEIGHT
设置图像的分辨率为 640x480,这是大多数视觉应用的标准分辨率。 -
红色阈值 (
red_thresholds
): 这个阈值使用的是 LAB 色彩空间中的三个通道 L, A, B 的范围。该阈值定义了一个色块的颜色范围,用于识别红色物体。- L (亮度) 最小值 30,最大值 68;
- A (绿色到红色的色差) 最小值 9,最大值 127;
- B (蓝色到黄色的色差) 最小值 -12,最大值 73。
红色的色块会被识别并标记为代码
1
(因为索引是0
,所以在find_blobs()
中,1 << 0
表示红色)。
3. 初始化摄像头和显示器
sensor = None
try:
# 使用默认配置构造一个Sensor对象
sensor = Sensor(width=DETECT_WIDTH, height=DETECT_HEIGHT)
# sensor复位
sensor.reset()
# 设置通道 0 输出大小
sensor.set_framesize(width=DETECT_WIDTH, height=DETECT_HEIGHT)
# 设置通道 0 输出格式
sensor.set_pixformat(Sensor.RGB565)
- 创建一个传感器对象
sensor
,并指定其分辨率为 640x480。 - 调用
sensor.reset()
来重置摄像头设置。 sensor.set_framesize()
设置图像输出的分辨率。sensor.set_pixformat(Sensor.RGB565)
设置输出图像格式为RGB565
,每个像素占用 16 位存储(5 位红色,6 位绿色,5 位蓝色)。
4. 设置显示
# 设置显示
Display.init(Display.VIRT, width=DETECT_WIDTH, height=DETECT_HEIGHT, fps=100)
这里使用虚拟显示 (Display.VIRT
) 初始化显示屏,并设置分辨率为 640x480,帧率为 100 帧每秒。
5. 初始化媒体管理器和启动传感器
# 初始化媒体管理器
MediaManager.init()
# sensor开始运行
sensor.run()
MediaManager.init()
初始化媒体管理器,管理摄像头捕捉的图像和相关的缓存。sensor.run()
启动摄像头的图像捕捉过程。
6. 主循环:图像捕捉、色块检测和显示
fps = time.clock()
while True:
fps.tick()
# 检查是否应该退出
os.exitpoint()
img = sensor.snapshot()
fps.tick()
:记录并更新帧时间,用于计算实时帧率。os.exitpoint()
:检查是否应退出程序。sensor.snapshot()
:捕获一帧图像。
7. 查找红色物体
for blob in img.find_blobs(red_thresholds, pixels_threshold=100, area_threshold=100, merge=True):
if blob.code() == 1: # 代码为 1 表示仅红色
# 绘制矩形框
img.draw_rectangle([v for v in blob.rect()])
# 绘制十字准星
img.draw_cross(blob.cx(), blob.cy())
# 绘制旋转角度
img.draw_keypoints([(blob.cx(), blob.cy(), int(math.degrees(blob.rotation())))], size=20)
-
find_blobs(red_thresholds, pixels_threshold=100, area_threshold=100, merge=True)
:red_thresholds
:提供红色物体的颜色范围。pixels_threshold=100
:只有像素数量超过 100 的色块才会被返回。area_threshold=100
:只有面积大于 100 的色块才会被返回。merge=True
:如果设置为True
,会合并相互重叠的色块。
-
if blob.code() == 1
:检查该色块是否是红色,红色色块的代码为1
,即1 << 0
。
绘制操作:
blob.rect()
:获取色块的矩形边界并绘制。blob.cx(), blob.cy()
:获取色块的质心坐标,并绘制十字准星。blob.rotation()
:获取色块的旋转角度,使用math.degrees()
将其从弧度转换为度数,并绘制旋转角度的标记。