一、机器视觉硬件支持

语言:micropython MicroPython是 Python 3 语言的精简实现 ,包括Python标准库的一部分,经过优化可在微控制器和受限环境中运行。

Pasted image 20250731170931

首先是导库: Pasted image 20250731171104

  • sensor库:sensor是摄像头,sensor库负责摄像头的底层驱动。
  • image库:image库里包含了一些处理图像的函数
  • time库:时间库,用于获取时间
  • lcd库:lcd屏幕底层驱动的相关库

Pasted image 20250731171321 初始化LCD 传了一个参数freq(频率),指定驱动LDC的时钟频率,这里是15MHz。 invert参数:控制LCD屏是否反色

反色如下图所示,左边是invert=True,右边是False。 Pasted image 20250731171542

sensor.reset():初始化摄像头,同时摄像头开始运行

sensor.set_pixformat(sensor.RGB565):用于设置像素格式,设置摄像头为RGB565格式,除了RGB565还有GRAYSCALE(灰度模式) sensor.GRAYSCALE: 灰度,每个像素8bit。 sensor.RGB565: 彩色,每个像素16bit。

  • GRAYSCALE灰度模式图像是黑白的,每个像素8bit,色差从最黑:0到最白28,也就是255。
  • RGB565的16bit的存储方式: Pasted image 20250731172227 一个彩色图像由RGB三个分量组成,一个RGB565的每一个像素点数据位2Byte,即16位,那么从名字上就可看出来这16位中,高5位为R分量,中间6位为G分量,低5位为B分量。 Pasted image 20250731172336

sensor.set_framesize(sensor.QVGA):分辨率设置。设置分辨率为QVGA,即320x240。VGA前面每加一个Q,图片就越小,处理速度就越快 - 分辨率:数字图像由一个个像素点组成,每个像素点只能显示一种颜色,大量像素点拼在一起组成数字图像 - 320X240:长度是320个像素点,宽度是240个像素点。 Pasted image 20250731173022

sensor.set_vflip(True) #垂直翻转
sensor.set_hmirror(False) #水平翻转
lcd.rotation(0)#lcd屏幕翻转

这三个函数是调节lcd屏的,第一次上电,lcd内显示的图像可能是倒着的,可能是镜像的,所以要用这些函数调整。

sensor.skip_frames(time = 2000) #跳过用于初始化的2000ms 摄像头刚启动时,图像质量还没稳定,所以跳过一些图像,time=2000表示跳过2000ms

clock = time.clock() #创建一个时钟以获得FPS 如注释说明

最后是while死循环:

while(True):
    clock.tick()                    #更新时钟
    img = sensor.snapshot()         #获取摄像头传来的一张图片
    lcd.display(img)                #把图片显示在LCD上
    print(clock.fps())              #将程序运行的帧率打出来

lcd.display()函数:在LCD屏上显示某张图像

print函数用串口输出字符串,无需添加串口相关的库,print直接用,而且每个print自带换行+回车。 根据print的功能,可以通过串口实现STM32与K210之间的通信

  • 拔下typec线,发现lcd屏实时显示的图像不见了 因为其实根本没有把代码烧录进去,烧录的只有刚开始的maixpy固件,而点绿色箭头之后只是通过串口向k210发送命令

如何保存到k210的flash:工具→将打开的脚本保存到开发板的boot.py


二、色块追踪

Pasted image 20250731174602

sensor.set_auto_gain(False) #颜色识别必须关闭自动增益,会影响颜色识别效果
sensor.set_auto_whitebal(False) #颜色识别必须关闭白平衡,会影响颜色识别效果,导致颜色的阈值发生改变

set_auto_gain(False):关闭自动增益,自动增益控制是指放大电路的增益自动地随信号强度而调整的自动控制方法,该功能自动调整增益会影响对颜色的判断,所以关掉。 set_auto_whitebal(False):关掉自动白平衡 白平衡:字面上的理解是白色的平衡。 白平衡是描述显示器中红、绿、蓝三基色混合生成后白色精确度的一项指标。 白平衡的数值越高,色调越暖;数值越低,色调越冷。

find_blobs函数

blobs = img.find_blobs([thresholds], roi=my_roi, x_stride=30, y_stride=30, invert=False, area_threshold=100,pixels_threshold=100,merge=False)

第一个参数是一个列表,因为只识别一种颜色,所以只有thresholds一个参数,可以添加多个,识别多个颜色。

roi=my_roi,前面写了my_roi = [0, 0, 320, 240] My_roi列表里这四个参数分别是:顶点x、顶点y、宽度w、高度h,单位是像素。 摄像头拍照得到的图像分辨率是QVGA,也就是320X240,也就是宽度320,高度240。 由于LCD使用时是竖着的,所以坐标系是这样建立的: Pasted image 20250731175700 ROI(region of interest),感兴趣区域。 也就是运行寻找色块算法的区域,只在roi中寻找,my_roi = [0, 0 ,320 ,240],正好是全屏,也就是整张图片都检测色块。 如果想只检测上半截图片,就可以my_roi = [0,0,160,240]。

x_stride 就是查找的色块的x方向上最小宽度的像素,默认为2,如果只想查找宽度30个像素以上的色块,那么就设置这个参数为30。

y_stride 就是查找的色块的y方向上最小宽度的像素,默认为1,如果只想查找宽度30个像素以上的色块,那么就设置这个参数为30。

invert 反转阈值,把阈值以外的颜色作为阈值进行查找,比如除了绿色的色块都找,有些工况会用得到。

area_threshold 面积阈值,如果色块被框起来的面积小于这个值,会被过滤掉 pixels_threshold 像素个数阈值,如果色块像素数量小于这个值,会被过滤掉

merge 合并,如果设置为True,那么合并所有重叠的blob为一个。 注意:这会合并所有的blob,无论是什么颜色的。如果想混淆多种颜色的blob,只需要分别调用不同颜色阈值的find_blobs。

这个函数执行一次,返回值是一个含有七个参数的列表: 分别是:色块框x坐标,色块框y坐标,色块框宽度w,色块框高度h,色块包含像素数m,色块框中心x坐标,色块框中心y坐标。

  if blobs:
    for i in blobs:
    img.draw_rectangle(i[0:4])
	img.draw_cross(i[5],i[6])
	print("(%d,%d)"%(i[5],i[6]))

然后通过函数返回值,在原图像上画个框,再画个十字准星,把色块标注出来,最后在lcd上显示加工过的图像。

接下来对要测的色块进行阈值标定 首先在K210上运行“2.色块追踪.py”,然后用摄像头照要识别的色块 保持这个色块出现在摄像机图像里,然后点击禁用 左上角工具栏:工具、机器视觉、阈值编辑器。 选择帧缓冲区。将右边调到尽量显示左边色块完整的轮廓, 调整得差不多之后,复制下面的LAB阈值,然后粘贴到开头。

Lab颜色空间(Lab color space) L代表亮度,取值在0-100之间,0是黑色,100是白色。 a代表从绿色到红色的分量,取值-128至127,正数偏红,负数偏绿。 b代表从蓝色到黄色的分量,取值-128至127,正数偏黄,负数偏蓝。


三、K210与STM32通信

 print("(%d,%d)"%(i[5],i[6]))

没有STM32单独测试K210串口发送是否正常时,首先k210连接USB线,与maixpy连通之后禁用右上角的帧缓冲区: Pasted image 20250801113153

然后打开左下角的串行终端:就能快速看到发送的数据了。 Pasted image 20250801113211

四、视觉循迹

  1. 数字图像直方图均衡化运算
  2. OTSU(最大类间方差)二值化及底层C代码
  3. 图像使用线性回归算法
THRESHOLD = (0, 100, -128, 127, -6, -105)
import sensor, image, time,lcd
 
lcd.init(freq=15000000,invert=False)
lcd.rotation(0)#lcd屏幕翻转
sensor.reset()
sensor.set_vflip(True)
sensor.set_hmirror(False)
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QQQVGA) # 80x60
sensor.set_auto_gain(False) 
sensor.set_auto_whitebal(False) 
sensor.skip_frames(time = 2000)
clock = time.clock()
 
while(True):
    clock.tick()
    img = sensor.snapshot()
    img.binary([THRESHOLD])
    img=img.erode(1)
    img=img.dilate(1)
    line = img.get_regression([(100,255)], robust = True)
    if (line):
        rho_err = abs(line.rho())-img.width()/2
        if line.theta()>90:
            theta_err = line.theta()-180
        else:
            theta_err = line.theta()
        img.draw_line(line.line(), color = 127)
        print("(%.2f,%.2f)"%(rho_err,theta_err))
    lcd.display(img)

sensor.set_auto_gain(False) sensor.set_auto_whitebal(False) 由于彩色巡线也要通过阈值识别颜色,凡是识别颜色的程序都要关闭自动增益和白平衡。 由于循迹的帧率要求很高,所以把图像缩小到了QQQVGA,80X60的大小。

img.binary([THRESHOLD]) Binary(二进制的 adj.),这个函数的意思是将当前图像二值化

二值化,详细一点叫“黑白二值化”,也就是把一张彩图或者灰度图经过二值化处理之后,图片中只有黑和白两种颜色组成 Pasted image 20250802112907 二值化的算法必定要先给一个阈值(threshold)。比如彩色图,给出某个颜色的LAB阈值,二值化处理中属于阈值内的像素都变成白色,不属于阈值内的都变成黑色。 彩色巡线程序二值化思路是:二值化之后,蓝色都变成白色,不是蓝色全变成黑色。(其中蓝色的LAB阈值由阈值编辑器调出来) 执行完img.binary([THRESHOLD])后,图像会被按照蓝色和非蓝色二值化分割。

img=img.erode(1) img=img.dilate(1) 去除噪点用,是OpenCV的库函数,用到了卷积操作

line = img.get_regression([(100,255)], robust = True) Regression(回归) linear regression (线性回归) 这个函数要实现的功能是将二值化后的蓝色像素点全部参与线性回归计算,计算出回归直线。

线性回归用到了最小二乘法: Pasted image 20250802113420 Pasted image 20250802113436

line = img.get_regression([(100,255)], robust = True)
第一个参数[(100,255)],由于img现在已经是二值化之后的图像了,所有像素的灰度值只有0和255两种情况,想选中所有白色的像素点进行线性回归,也就是选中所有灰度值为255的像素。但是这个参数必须是一个范围,不能只输入一个255,所以[(100,255)]里的100是随便取的,取254也可以,只要不取0就行(因为0是黑色)。

第二个参数robust = True,若 robust 为True,则使用Theil-Sen线性回归算法,它计算图像中所有阈值像素的斜率的中位数。默认开启即可。

    if (line):
        rho_err = abs(line.rho())-img.width()/2
        if line.theta()>90:
            theta_err = line.theta()-180
        else:
            theta_err = line.theta()
        img.draw_line(line.line(), color = 127)
        print("(%.2f,%.2f)"%(rho_err,theta_err))
    lcd.display(img)

如果拟合成功了,有line对象: line.rho() 可视为直线与最下方水平线的交点值,-img.width()/2经过运算后得到横向偏移量的误差值rho_err。测试得rho_err的范围是(-35,35),如下图。 Pasted image 20250802113836 前面色块分析可以把模块和屏幕竖着放,但是并没有找到可以将图像旋转90°的函数,由于get_regression函数的局限性,只能横着放模块了。

line.theta()表示直线偏斜的角度,经过if算法后得到角度误差值theta_err。theta_err范围是(-90,90),如下图: Pasted image 20250802115102 后计算得到的角度误差值和横向偏移量,两个数据通过print函数从串口发出,发给stm32用于PID循迹。

注意在STM32的相关PID循迹中,要写两套PID,一个矫正角度偏移,一个矫正横向偏移量,然后两个pid的结果按一定的分配比例相加,做出综合的判断。

  • 深色背景浅色赛道循迹 Pasted image 20250802115218 智能车竞赛的赛道就是深色背景(蓝色)和浅色赛道(白色)。

首先智能车摄像头并不能像k210一样python编程,只能当做传感器,传回数组形式的原始图像,在单片机中自己写C代码来处理图像。

  • 第一步:二值化 灰度图中,二值化的阈值相比彩色图更简单,灰度图像素点的阈值是0-255,二值化的阈值是0-255中的一个数。例如100,灰度值小于100的变成白色,大于100的变成黑色。

    随着环境变化,阈值并不能保证时刻是最佳阈值。尤其是智能车,时刻要保持最好的阈值进行二值化,把赛道和背景尽可能清晰的分开。

    如何寻找阈值:OTSU算法(大津法)(最大类间方差算法)

    用这个算法可以通过分析传来的灰度图像,分析当前图像的最佳阈值并进行二值化。

来自chatGPT的解释: 类间方差(between-class variance)是一种用于衡量图像分割或模式识别中类别间差异的统计指标。它主要用于评估不同类别之间的分离程度,反映了不同类别之间的平均差异程度。

在图像分割任务中,类间方差常用于确定最佳阈值或分割边界。通过最大化类间方差,我们可以寻找最佳的分割阈值,将图像分成两个或多个类别,使得类别间的差异最大化。

当存在明显的前景和背景区域,并且它们之间的差异较大时,类间方差通常会显示出峰值。在这种情况下,通过调整阈值或分割边界,可以将图像分成两个类别,使得类别间的差异最大化,从而产生峰值。

所以,基于最大类间方差反应的物理量,OTSU算法的整体思路是: 遍历0-255的灰度值,直到找到灰度值a,使得[0 , a-1]与[a, 255]两部分灰度的最大类间方差处于峰值时,取当前的a作为最佳阈值进行二值化分割。

OTSU算法(大津法)

FJW:图传—阈值计算—二值化,压缩,y轴反转—搜图扫线—边界,拐点等图像特征—元素处理

期望值是变量输出值的平均数,数学期望的计算公式:

期望的性质

设图像最佳阈值为T,T将图像分为目标和背景。其中目标点数占总图像比例为w0,平均灰度值为u0;背景点数占图像比例为w1,平均灰度值为u1 则,

图像的总平均灰度值为:
类间方差公式为:
注意与普通方差的公式不一样。

  • OTSU算法的C语言实现。代码可移植到智能车单片机内直接使用。
#define GrayScale
  256
//大津法,算阈值
uint8 otsuThreshold(uint8 *image, uint16 col, uint16 row)
{
    uint16 width = col;
    uint16 height = row;
    int pixelCount[GrayScale];//灰度计数数组:灰度i出现的次数
    float pixelPro[GrayScale];//概率 
    int i, j, pixelSum = width * height;//整张图片的像素总数
    uint8 threshold = 0;    //最终要输出的阈值
    uint8* data = image;    //指向图像数据的指针
    for (i = 0; i < GrayScale; i++) //这个循环用于初始化数组
    { 
      pixelCount[i] = 0;    
      pixelPro[i] = 0;
    }
 
    uint32 gray_sum = 0;    //初始化参数
    //统计灰度级中每个像素在整幅图像中的个数
    for (i = 0; i < height; i++)    //这个二维数组负责从上到下,从左到右
    {
        for (j = 0; j < width; j++)
        { 
            pixelCount[(int)data[i * width + j]]++;  //将像素值作为灰度计数数组的下标   
            gray_sum += (int)data[i * width + j];  //灰度值总和gray_sum
        }
    }
 
    //计算每个灰度值在整幅图像中的比例pixelPro
    for (i = 0; i < GrayScale; i++)
    { 
        pixelPro[i] = (float)pixelCount[i] / pixelSum;
    }
 
    //遍历灰度级[0,255]
    float w0, w1, u0tmp, u1tmp, u0, u1, u, deltaTmp, deltaMax = 0;
    w0 = w1 = u0tmp = u1tmp = u0 = u1 = u = deltaTmp = 0;
    for (j = 0; j < GrayScale; j++)//0-255从头到尾遍历
    {
        w0 +=pixelPro[j];  //背景部分每个灰度值的像素点所占比例之和 即背景部分的比例,即灰度值为0~j的像素占总的比例
        u0tmp += j * pixelPro[j];  //背景部分 每个灰度值的点的比例 *灰度值(背景部分的期望)
        w1 = 1 - w0;       //j+1~255部分所占的比例
        u1tmp = gray_sum / pixelSum - u0tmp;///(赛道部分的期望)整张图像的平均灰度值-背景的期望=赛道的期望
        u0 = u0tmp / w0;    //背景平均灰度(期望/比例=对应灰度值)
        u1 = u1tmp / w1;    //前景平均灰度(期望/比例=对应灰度值)
        u = u0tmp + u1tmp;  //全局平均灰度
        deltaTmp = w0 * pow((u0 - u), 2) + w1 * pow((u1 - u), 2);//类间方差
        if (deltaTmp > deltaMax) 
        {//求出目前最大的类间方差
            deltaMax = deltaTmp;
            threshold = j;
        }
        if (deltaTmp < deltaMax) 
        {//一旦当前的类间方差开始有下行趋势,说明当前阈值即为方差峰值,停止检测
            break;
        }
    }
    return threshold;
}
  • maixpy的代码
import sensor, image, time,lcd
 
lcd.init(freq=15000000,invert=False)
lcd.rotation(0)#lcd屏幕翻转
sensor.reset()
sensor.set_vflip(True)
sensor.set_hmirror(False)
sensor.set_pixformat(sensor.GRAYSCALE)
sensor.set_framesize(sensor.QQQVGA) # 80x60 (4,800 pixels)
sensor.skip_frames(time = 2000)
clock = time.clock()
 
while(True):
    clock.tick()
    img = sensor.snapshot()
    histogram = img.get_histogram()
    Thresholds = histogram.get_threshold()
    v = Thresholds.value()
 
    img.binary([(0,v)],invert=False)#反着来,把暗的部分变成白色
    img=img.erode(1)
    img=img.dilate(1)
    line = img.get_regression([(v,255)], robust = True)   #robust为True则使用Theil-Sen线性回归算法,它计算图像中所有阈值像素的斜率的中位数。
    if (line):
        rho_err = abs(line.rho())-img.width()/2
        if line.theta()>90:
            theta_err = line.theta()-180
        else:
            theta_err = line.theta()
        img.draw_line(line.line(), color = 127)
        print("(%.2f,%.2f)"%(rho_err,theta_err))
 
    lcd.display(img)

与彩色巡线代码的差异:

首先,传入的是灰度图像sensor.set_pixformat(sensor.GRAYSCALE),而不是彩色图。 多了histogram = img.get_histogram(),histogram(直方图),这个方法的作用是进行数字图像直方图均衡化运算

均衡化前后的效果差异: Pasted image 20250802162402 原本灰不溜秋的图片,处理之后变得层次分明。直方图均衡化运算对于灰度图像的作用是让图像的灰度分布均匀,如下图所示,避免图像中大部分的像素灰度值集中在某个值形成峰值,灰度分布图平缓起来之后,图像的对比度就会更加清晰,也就更方便二值化。如下图: Pasted image 20250802162515

Thresholds = histogram.get_threshold() v = Thresholds.value() get_threshold()函数就是大津法。v就是返回的最佳阈值。

二值化:img.binary([(0,v)],invert=False) 二值化的时候传入的元组只有两个值,因为灰度图像只需要一段灰度值,选择的灰度值是[0 , v],给暗的像素变成白色,亮的像素是黑色,这样做是因为循的是黑色赛道,白色背景,所以要反过来,灵活变通即可。

line = img.get_regression([(v,255)], robust = True)   #robust为True则使用Theil-Sen线性回归算法,它计算图像中所有阈值像素的斜率的中位数。

线性回归,给白色像素线性回归,也就是黑线部分线性回归


五、深度学习体验: yolov2数字识别、人脸识别、垃圾分类等(物体检测)

解决一大部分机器视觉问题,比如:

  1. 基础人脸识别,即能分别出是人脸,但是不知道是谁的脸。
  2. 垃圾分类,识别垃圾的品种。
  3. 识别水果,香蕉葡萄西瓜等、识别动物、植物、交通工具等各种各样的东西。
  4. 一些应用,安全帽检测,口罩检测,这次的数字识别。

视觉识别技术:基于深度学习的物体检测

思维导图: Pasted image 20250802164301


六、AprilTag视觉测距

实验现象:使用摄像头进行测距,只需要把这个像二维码一样的东西打印出来贴在被测距的目标位置上。

Pasted image 20250814120654 maixpy上方菜单栏→工具→机器视觉→AprilTag生成器→H11家族

原理:Pasted image 20250814120805 通过小孔成像,可以测量得到照片的像素来算出看到物体的距离,可以分析得到一张图片的x,y,z方向的偏移和x,y,z轴的旋转角度——AprilTag的厉害之处。

首先需要得到所用镜头的焦距,以及感光面的长宽。 通过找客服查手册,所用的maixbit开发板上的镜头的焦距是4.0mm,感光元件型号为ov5642,查阅ov5642的数据手册得到感光面的长宽如image area所示: Pasted image 20250814121127

看代码:

import sensor, image, time, math
 
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QQVGA)
sensor.skip_frames(30)
sensor.set_auto_gain(False)
sensor.set_auto_whitebal(False)
sensor.set_vflip(True) #垂直翻转
sensor.set_hmirror(False) #水平翻转
clock = time.clock()
 
 
# c_x 是图像的x中心位置
# c_y 是图像的y中心位置
 
f_x = (4.0 / 3.6736) * 160 # 默认值
f_y = (4.0 / 2.7384) * 120 # 默认值
c_x = 160 * 0.5 # 默认值(image.w * 0.5)
c_y = 120 * 0.5 # 默认值(image.h * 0.5)
dis_gain = 1.0
 
def degrees(radians):
    return (180 * radians) / math.pi
 
while(True):
    clock.tick()
    img = sensor.snapshot()
    for tag in img.find_apriltags(fx=f_x, fy=f_y, cx=c_x, cy=c_y): # 默认为TAG36H11
        img.draw_rectangle(tag.rect(), color = (255, 0, 0))
        img.draw_cross(tag.cx(), tag.cy(), color = (0, 255, 0))
        print_args = (tag.x_translation(), tag.y_translation(), tag.z_translation(), \
            degrees(tag.x_rotation()), degrees(tag.y_rotation()), degrees(tag.z_rotation()))
        # 位置的单位是未知的,旋转的单位是角度
        dist=dis_gain*math.sqrt(tag.x_translation()*tag.x_translation()+\
        tag.y_translation()*tag.y_translation()+tag.z_translation()*tag.z_translation())
        print("%f"%dist)

图像分辨率为QQVGA,为160x120。

f_x,f_y的参数中,4.0是焦距,3.6736和2.7384是感光元件的长宽,160和120是最终图像分辨率。

使用find_apriltags函数找到所有的apriltag,这里只使用了一个apriltag。​

变量print_args里面储存了处理好的三个横向偏移量和三个角度偏移量,这里没有用到角度,但是角度值也是很有用的。​

dist变量为测量的最终距离,这个距离计算很简单:​

有一个增益系数dis_gain,这个增益系数与打印出来的apriltag的实际大小有关,测距前需要用尺子量一下真实距离,再对比测量距离,然后真实距离/测量距离即为这个增益值。​