[MCU] 嘉楠K230AI开发板测评5---颜色识别、摄像头物体计数与巡线

dfjs   2024-11-3 21:28 楼主

嘉楠科K230AI开发板测评5--机器视觉篇

1.单一颜色识别

        预先设定颜色阈值,如红、绿、蓝,这样K230摄像头采集图像后就能自动识别了。

        CanMV集成了RGB565颜色块识别find_blobs函数(其位于image模块下),主要是基于LAB(L:亮度,取值0-100,表示从纯黑到纯白的变化;A代表从绿色到红色的范围,取值是-128--127;B代表从蓝色到黄色的范围,取值是-128--127)颜色模型,每个颜色都是用一组LAB阈值表示。

        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=None[, merge_cb=None]]]]]]]]]])函数查找图像中指定的色块,返回image.blog对象列表。

        thresholds: 必须是元组列表。 [(lo, hi), (lo, hi), ..., (lo, hi)] 定义你想追踪的颜色范围。 对于灰度图像,每个元组需要包含两个值 - 最小灰度值和最大灰度值。 仅考虑落在这些阈值之间的像素区域。 对于RGB565图像,每个元组需要有六个值(l_lo,l_hi,a_lo,a_hi,b_lo,b_hi) - 分别是LAB L,A和B通道的最小值和最大值;

        area_threshold: 若色块的边界框区域小于此参数值,则会被过滤掉;

        pixels_threshold: 若色块的像素数量小于此参数值,则会被过滤掉;

        merge: 若为True,则合并所有没有被过滤的色块;

        margin: 调整合并色块的边缘。

        blob.rect()函数返回一个矩形元组(x,y,w,h),如色块边界。可以通过索引[0-3]来获得这些值。

        blob.cx()返回色块(int)的中心x位置。可以通过索引[5]来获得这个值。

        blob.cy()返回色块(int)的中心y位置。可以通过索引[6]来获得这个值。

        代码编写流程如下:

image-20241103211629-1.png  

        参考代码:

'''
实验名称:单一颜色识别
实验平台:01Studio CanMV K230
教程:wiki.01studio.cc
'''

import time, os, sys

from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口

# 颜色识别阈值 (L Min, L Max, A Min, A Max, B Min, B Max) LAB模型
# 下面的阈值元组是用来识别 红、绿、蓝三种颜色,当然你也可以调整让识别变得更好。
thresholds = [(30, 100, 15, 127, 15, 127), # 红色阈值
              (30, 100, -64, -8, 50, 70), # 绿色阈值
              (0, 40, 0, 90, -128, -20)] # 蓝色阈值
              
try:

    sensor = Sensor() #构建摄像头对象
    sensor.reset() #复位和初始化摄像头
    sensor.set_framesize(width=800, height=480) #设置帧大小为LCD分辨率(800x480),默认通道0
    sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0

    Display.init(Display.ST7701, to_ide=True) #同时使用3.5寸mipi屏和IDE缓冲区显示图像,800x480分辨率
    #Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像

    MediaManager.init() #初始化media资源管理器

    sensor.run() #启动sensor

    clock = time.clock()

    while True:


        os.exitpoint() #检测IDE中断

        ################
        ## 这里编写代码 ##
        ################
        clock.tick()

        img = sensor.snapshot() #拍摄一张图片

        blobs = img.find_blobs([thresholds[0]]) # 0,1,2分别表示红,绿,蓝色。

        if blobs:

            for b in blobs: #画矩形和箭头表示
                tmp=img.draw_rectangle(b[0:4], thickness = 2)
                tmp=img.draw_cross(b[5], b[6], thickness = 2)

        img.draw_string_advanced(0, 0, 30, 'FPS: '+str("%.3f"%(clock.fps())), color = (255, 255, 255))

        Display.show_image(img) #显示图片

        print(clock.fps()) #打印FPS


###################
# IDE中断释放资源代码
###################
except KeyboardInterrupt as e:
    print("user stop: ", e)
except BaseException as e:
    print(f"Exception {e}")
finally:
    # sensor stop run
    if isinstance(sensor, Sensor):
        sensor.stop()
    # deinit display
    Display.deinit()
    os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
    time.sleep_ms(100)
    # release media buffer
    MediaManager.deinit()

        实验结果,如下图1,也可通过阈值编辑器来手动调节LAB的阈值范围,如下图2。

图片.png

  图片.png  

2.多种颜色识别

        基于单一颜色识别,加以修改,即可实现多种颜色识别。

        代码编写流程如下:

图片.png  

        核心代码如下,与单一颜色识别例程相比,修改的代码如下,在颜色识别前中加入了for循环,识别预设的3种颜色:

# 颜色识别阈值 (L Min, L Max, A Min, A Max, B Min, B Max) LAB模型
# 下面的阈值元组是用来识别 红、绿、蓝三种颜色,当然你也可以调整让识别变得更好。
thresholds = [(30, 100, 15, 127, 15, 127), # 红色阈值
              (30, 100, -64, -8, 50, 70), # 绿色阈值
              (0, 40, 0, 90, -128, -20)] # 蓝色阈值

colors1 = [(255,0,0), (0,255,0), (0,0,255)]
colors2 = ['RED', 'GREEN', 'BLUE']

..............


        img = sensor.snapshot() #拍摄一张图片
        for i in range(3):
            blobs = img.find_blobs([thresholds[i]]) # 0,1,2分别表示红,绿,蓝色。
            if blobs:

                for b in blobs: #画矩形、箭头和字符表示
                    tmp=img.draw_rectangle(b[0:4], thickness = 4, color = colors1[i])
                    tmp=img.draw_cross(b[5], b[6], thickness = 2)
                    tmp=img.draw_string_advanced(b[0], b[1]-35, 30, colors2[i],color = colors1[i])


        img.draw_string_advanced(0, 0, 30, 'FPS: '+str("%.3f"%(clock.fps())), color = (255, 255, 255))

        Display.show_image(img) #显示图片

        print(clock.fps()) #打印FPS

        实验结果如下,将每个颜色的圆形用矩形画出并表明颜色,与单一颜色识别例程相比,修改的代码如下,在颜色识别前中加入了for循环,识别预设的3种颜色:

图片.png  

3.物体计数(相同颜色)

        基于上一节颜色识别我们看到可以识别出色块的数量,来学习如何识别指定颜色的物体,计算其数量。

        针对不同颜色的物体我们如何获取它的阈值呢?

        先使用 摄像头代码采集物体图像,在IDE右上角缓冲区点击“禁用”将要识别的物体确认下来;点击 工具—机器视觉—阈值编辑器 。在弹出的对话框选择“帧缓冲区”。通过调整下方6个LAB值,使得物体颜色在右边为白色,其余背景为黑色。记录颜色的LAB值,在后面代码中使用,如下图。

图片.png  

        代码编写流程如下图:

图片.png  

        核心代码如下:

.............
thresholds = [(18, 72, -13, 31, 18, 83)] #黄色跳线帽阈值
.............

        img = sensor.snapshot()
        blobs = img.find_blobs([thresholds[0]])

        if blobs: #画框显示
            for b in blobs:
                tmp=img.draw_rectangle(b[0:4])
                tmp=img.draw_cross(b[5], b[6])

        #显示计算信息
        img.draw_string_advanced(0, 0, 30, 'FPS: '+str("%.3f"%(clock.fps()))+'       Num: '
                                 +str(len(blobs)), color = (255, 255, 255))

        Display.show_image(img)

        print(clock.fps()) #打印FPS
.............

        实验结果如下图,在阈值准确的情况下,统计出跳线帽的数量:

图片.png  

4.机器人巡线(实线)

        机器人巡线依然基于颜色识别,根据摄像头采集到的图像直线与中心偏离的位置计算出偏离角度。

        对画面是有一定要求的,也就是摄像头采集图像一定要出现唯一1条连续的黑色直线。程序通过对画面切割成三部分,计算每个部分黑色线的中心点X坐标,然后采用加权平均算法估算出直线的偏离位置。通常情况下越靠近底部的地方离摄像头越近,顶部表示远方线段。因此底部的图形权重高。

        假设摄像头当前画面的像素是例程的QQVGA分辨率:160(宽)X120(高),左上角坐标为(0,0),然后当前出现直线坐标为(80,120)至(160,0)偏右的直线。上中下三个部分的权重分别为0.1、0.3、0.7(底部图像靠近机器人,权重大,权重总和可以不是1),我们来计算一下其中心值,如下图:

图片.png  

        上图中Y轴的中点坐标就是60,X坐标加权平均值计算如下:

X=(80*0.7+120*0.3+160*0.1)/(0.7+0.3+0.1)=98

        那么直线偏离坐标可以认为是(98,60),图中绿色“+”位置。那么利用反正切函数可以求出偏离角度:a = atan((98-80)/60)=16.7°,机器人相当于实线的位置往左偏了,所以加一个负号,即 -16.7°;偏离角度就是这么计算出来的。得到偏离角度后就可以自己编程去调整小车或者机器人的运动状态,直到0°为没有偏离。

代码编写思路如下:

图片.png  

        参考代码如下:

'''
实验名称:机器人巡线(实线)
实验平台:01Studio CanMV K230
教程:wiki.01studio.cc

# 黑色灰度线巡线跟踪示例
#
#做一个跟随机器人的机器人需要很多的努力。这个示例脚本
#演示了如何做机器视觉部分的线跟随机器人。你
#可以使用该脚本的输出来驱动一个差分驱动机器人
#跟着一条线走。这个脚本只生成一个表示的旋转值(偏离角度)
#你的机器人向左或向右。
#
# 为了让本示例正常工作,你应该将摄像头对准一条直线(实线)
#并将摄像头调整到水平面45度位置。请保证画面内只有1条直线。
'''

import time, os, sys, math

from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口

# 追踪黑线。使用 [(128, 255)] 追踪白线.
GRAYSCALE_THRESHOLD = [(0, 64)]

# 下面是一个roi【区域】元组列表。每个 roi 用 (x, y, w, h)表示的矩形。

'''
#采样图像QQVGA 160*120,列表把roi把图像分成3个矩形,越靠近的摄像头视野(通常为图像下方)的矩形权重越大。
ROIS = [ # [ROI, weight]
        (0, 100, 160, 20, 0.7), # 可以根据不同机器人情况进行调整。
        (0,  50, 160, 20, 0.3),
        (0,   0, 160, 20, 0.1)
       ]
'''
#采样图像为QVGA 320*240,列表把roi把图像分成3个矩形,越靠近的摄像头视野(通常为图像下方)的矩形权重越大。
ROIS = [ # [ROI, weight]
        (0, 200, 320, 40, 0.7), # 可以根据不同机器人情况进行调整。
        (0,  100, 320, 40, 0.3),
        (0,   0, 320, 40, 0.1)
       ]

# 计算以上3个矩形的权值【weight】的和,和不需要一定为1.
weight_sum = 0
for r in ROIS: weight_sum += r[4] # r[4] 为矩形权重值.

try:

    sensor = Sensor(width=1280, height=960) #构建摄像头对象,将摄像头长宽设置为4:3
    sensor.reset() #复位和初始化摄像头
    sensor.set_framesize(width=320, height=240) #设置帧大小,默认通道0
    sensor.set_pixformat(Sensor.GRAYSCALE) #设置输出图像格式,默认通道0

    Display.init(Display.ST7701, to_ide=True) #同时使用3.5寸mipi屏和IDE缓冲区显示图像,800x480分辨率
    #Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像

    MediaManager.init() #初始化media资源管理器

    sensor.run() #启动sensor

    clock = time.clock()

    while True:


        os.exitpoint() #检测IDE中断

        ################
        ## 这里编写代码 ##
        ################
        clock.tick()

        img = sensor.snapshot() #拍摄一张图片

        centroid_sum = 0

        for r in ROIS:
            blobs = img.find_blobs(GRAYSCALE_THRESHOLD, roi=r[0:4], merge=True) # r[0:4] 是上面定义的roi元组.

            if blobs:
                # Find the blob with the most pixels.
                largest_blob = max(blobs, key=lambda b: b.pixels())

                # Draw a rect around the blob.
                img.draw_rectangle(largest_blob.rect())
                img.draw_cross(largest_blob.cx(),
                               largest_blob.cy())

                centroid_sum += largest_blob.cx() * r[4] # r[4] 是每个roi的权重值.

        center_pos = (centroid_sum / weight_sum) # 确定直线的中心.

        # 将直线中心位置转换成角度,便于机器人处理.
        deflection_angle = 0

        # 使用反正切函数计算直线中心偏离角度。可以自行画图理解
        #权重X坐标落在图像左半部分记作正偏,落在右边部分记为负偏,所以计算结果加负号。

        #deflection_angle = -math.atan((center_pos-80)/60) #采用图像为QQVGA 160*120时候使用

        deflection_angle = -math.atan((center_pos-160)/120) #采用图像为QVGA 320*240时候使用

        # 将偏离值转换成偏离角度.
        deflection_angle = math.degrees(deflection_angle)

        # 计算偏离角度后可以控制机器人进行调整.
        print("Turn Angle: %f" % deflection_angle)

        # LCD显示偏移角度,scale参数可以改变字体大小
        img.draw_string_advanced(2,2,20, str('%.1f' % deflection_angle), color=(255,255,255))

        #Display.show_image(img) #显示图片

        #显示图片,仅用于LCD居中方式显示
        Display.show_image(img, x=round((800-sensor.width())/2),y=round((480-sensor.height())/2))

        print(clock.fps()) #打印FPS

###################
# IDE中断释放资源代码
###################
except KeyboardInterrupt as e:
    print("user stop: ", e)
except BaseException as e:
    print(f"Exception {e}")
finally:
    # sensor stop run
    if isinstance(sensor, Sensor):
        sensor.stop()
    # deinit display
    Display.deinit()
    os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
    time.sleep_ms(100)
    # release media buffer
    MediaManager.deinit()

        实验结果如下,以手机做黑色直线用,分别观察摄像头采集到没偏移、左偏和右偏各个直线的实验结果,可以看出效果良好。手机偏移角度分别为接近0°,负数,正数。获取到的偏移角度可以通过串口发送给其他外设或者主控。

图片.png

  图片.png  

图片.png  

本帖最后由 dfjs 于 2024-11-3 21:29 编辑

回复评论

暂无评论,赶紧抢沙发吧
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复