01 LQ_MV编程简介
通过上一章学习,我们基本了解了Python编程以及LQMV固件相关的基本情况。接下来让我们在Python语言的基础上结合LQ_MV硬件模块了解MicroPython。它是专门为在单片机这类嵌入式微控制器上,实现的一款简洁稳定的Python语言解释器。 其在语法层面上与Python语言基本相同,不同的是MicroPython集成了很多特殊的软件包,这些软件包定义了一些与硬件相关的对象以及接口。包括有管脚、串口、PWM输出、ADC采集、IIC总线、SPI总线等等。通过这些特殊的硬件驱动包,我们就可以来控制单片机,继而完成我们的比赛任务。
对于整个比赛程序来说,大体上分为初始化与循环两部分。初始化部分顾名思义是用来设置最初状态的,这部分包括引用我们需要用到的软件包、对程序中用到的变量进行声明和定义、硬件模块进行初始化设置。循环部分由for或者while循环构成,这部分必须死循环。否则程序执行完后会自动停止。关于循环部分,我们要以时间片段来理解,一次循环理解成一个时间片段(就像视频是一帧一帧连续变化的),我们需要不停的进行循环刷新和赋值硬件相关的变量与对象才可以正常运行。
最后来说一下在车模控制中会碰到的特殊编程技术,那就是中断。中断可以看成是一个代码片段,需要执行的时候来执行一次,执行完后继续执行循环中的代码。中断可以由外部电平变化触发,也可以由内部定时器触发。
本章就来带大家一起来学习LQ_MV的一些基础外设,这些外设在实际项目中经常会用到,希望大家可以认真学习掌握,以便将来更好、更快的完成实际项目开发。
02 LQ_MV模块介绍
一、核心板介绍
1、核心板引脚
首先来看核心板,板子正面左右各两排引脚,具有丰富的外设组件。这些引脚通过核心板后面的FPC软排线连接到母板上,以此来控制母板,其管脚资源及所有功能详见下图。
2、LED灯
核心板上正面有一个LED灯,可以通过程序控制其颜色。
3、SWD下载口
右侧有一排SWD下载引脚,可以通过此引脚连接LQ_ArmLink-V9下载器进行固件的烧录。但我们一般不用此方式下载。
4、DFU下载方式
短接BOOT引脚与RST引脚之后再上电,可以使LQ_MV进入DFU下载模式,此时可以通过第一章第五节的方式进行烧写固件,一般采用此方式进行固件的更新。
5、Type-C连接
核心板下方的Type-C接口连接数据线到电脑USB后,可以在此电脑中找到该设备的内存磁盘。如果安装SD卡后即显示SD卡的空间,若没有安装SD卡则显示芯片中100K的缓存。
6、SD卡
核心板背面有SD卡的卡座,可以插入SD卡,在使用SD卡时需要注意以下几点。
- SD卡大小最大支持32GB。
- SD卡文件系统需要为FAT32,如果不是此格式需要初始化为FAT32格式。
7、外部供电
板子背面可以通过外部电源对其进行供电,最大电压不可超过5V,推荐使用5V电源进行供电,这里不是必须的,当核心板通过软排线连接到母板上时,给母板供电,母板会通过FPC软排线对核心板进行供电。
8、CAN通信
核心板上继承了CAN通信的收发器,此接口作为保留,暂时不可用,在后续拓展板上改引脚被当作普通IO来做拨码开关使用。
二、母版介绍
为了能够链接更多外设而设计的LQMV4拓展版,其具有更丰富的接口,可以通过对应的排线进行直插来连接被控设备,或输入设备,其基本接口展示如下图所示。
在板子的背面,还有三路编码器输入接口,这个三路编码器均支持,A/B输入读取或单个脉冲读取,即可作为能接AB相的以及带方向的编码器,也可接普通单脉冲计数型的编码器使用,甚至可以在其他管脚不够用时从当普通I/O,通过程序配置来作为其他外设连接使用。
其次,只看接口可能不能满足有时候需要查看电路连接的需求,因此在这里几给出拓展板的原理图,可作为板上器件电路了解使用,原理图PDF文件可以在视觉模块资料中的拓展板原理图文件夹中查看,当然LQMV4模块引脚定义原图和管脚资源分配表格也在该文件夹中。
03 LQ_MV4实验
这部分不需要全部学习,用到哪个功能前来学习哪个实验,不过需要注意有些实验是需要拓展板配合完成的,故建议一开始就连接拓展板,并在运行程序时连接电池同时打开拓展板上的电源开关,以下大部分实验需用到LQ_MV文件夹中的资源,请提前拷至 此电脑用户>文档>OpenMV文件夹中,以方便后续使用。
一、LED灯闪烁
1、硬件
LED灯对应的引脚如下。
- LED_RED - 红灯 对应pyb包中LED模块的LED(1)
- LED_GREEN - 绿灯 对应LED(2)
- LED_BLUE - 蓝灯 对应LED(3)
- LED_IR - 红外 对应LED(4)
2、示例
如图所示,关于LED组件例程种有三个测试文件(只有将LQ_MV复制解压到文档>OpenMV文件夹中,才可以在IDE中这样直接打开文档文件夹例程)。
每个测试文件测试的功能都不相同,分别打开这三个文件。
- RGB_LED.py
此文件测试LED灯的三种基本颜色,通过“on()”或“off()”来控制具体颜色的亮灭。期间通过500ms的延时来保持亮起或熄灭的状态。
- Infrared LED lights.py
此文件测试镜头两侧的红外灯。控制原理与LED相同。
- Multi-colored LED lights.py
代码分析
此文件测试RGB灯的颜色组合,红、绿、蓝三种颜色可以组合成7种不同颜色的灯光。程序种通过函数“Test_RGBLED()”来测试,此函数内部有一个for循环,将变量i赋值为0到7之间不同数据传入到“LED_Ctrlx()”函数中,由“LED_Ctrlx()”函数根据不同的数据控制具体蓝色的灯的亮灭。
以上三个文件都要进行测试以下,前两个主要学会如何适用LED灯,第三个主要复习三个知识点,for循环、if判断语句以及&运算。
来看具体代码:
for i in range(8):
LED_Ctrlx(i) # 调用LED控制函数
time.sleep_ms(500)
利用for和range可以构成编程中常见到的循环操作。将列表[0, 8)循环赋值给变量i,将i传入函数“LED_Ctrlx()”中进行运算。运算详见程序注释。每次循环延时500ms,延时的目的是保持LED灯状态。
# 判断传入参数 x 的二进制数据第二位是否为0
if (x & 2) == 0: # 二进制运算 (xxx & 010)
LEDG.off() # 如果为0,则关掉绿色灯
else: # 如果不为0,则打开绿色灯
LEDG.on()
if语句用来根据表达式,选择需要执行的语句是哪一条,对与上面代码来说,表达式对应的就是“(x & 2) == 0”。通过判断该算式的值,来执行语句,如果该值不为0则执行if下缩进的语句,如果为0则执行else下缩进的语句。
(x & 2) == 0
该表达式是真(不为0)还是假(为0)是与x的值变化相关的。x的范围是从0-7,那么不同的x值带入运算中可以得到什么样的结果,通过下面公式不难计算出。
通过上面表格我们可以很清楚的知道当x等于何值时,蓝灯会亮起;等于何值时,蓝灯会熄灭。
二、TFT显示屏
1、硬件
显示屏引脚与LQ_MV对应关系:实际上屏幕针脚与拓展板直插即可
2、示例
1、 在进行测试之前,我们需要将设备连接好,如下图所示确认连接无误后再打开电源开关,值得注意的是使用拓展板外设接口功能时需要给拓展板单独供电,图中使用统一的8.4V锂电池来供电(该电池额定工作电压为7.2V-8.4V,充满电约8.4V,7.2V即表示电池需要充电了,切记锂电池不要过放使用,否则容量将有所损失)。
2 、打开文档中的OpenMV文件夹,将下图目录中的图片复制到OpenMV接入电脑弹出的U盘根目录中。
3、 打开LQ_MV中的LCD测试程序
LCD测试分为两个文件,分别测试显图片以及显示摄像头实时图像。
4、 LCD显示图片
5、 LCD显示相机图像
具体代码参考例程,其现象如下图所示。
3、代码分析
引用头文件包含了我们所需要的方法函数。
- nsor:是感光元件也就是相机相关的库,
- image:是图像相关的库,这里其实没有用到,因为我们只是采集的图像,并没有对图像进行处理,所以用不到里面的方法。
- time:是时间相关的函数库,计算帧率、系统运行时间以及延时的方法是这里面的。
- display:是LCD显示屏的函数库,显示图像到屏幕。
在之前我们已经将图片复制到了我们的设备缓存里面,接下来只需要读取我们的图片,并将图片赋值给我们的变量。
pic=image.Image("/LQ_Logo.jpg")
双引号中是我们的图片所在的路径,如果图片放在文件夹中,则路径如下所示:
pic=image.Image("/folder/LQ_Logo.jpg")
需要注意的是“folder”必须为英文,如果是中文文件夹,则程序无法解析识别到路径进而报错。将图片读取出来,交由变量“pic”保存,然后调用“lcd”的“write”方法,将图片输出到屏幕上。显示图片代码中的最后的while循环只是为了不让程序停止,其内部不做任何代码处理(pass是空指令)。
显示图片
lcd.write(pic) # 通过LCD显示该图片.
对于显示摄像头拍摄的画面来说,仅仅是把图像换成了相机拍摄到的照片,所以我们需要设置相机采集的图片的大小。
首先需要初始化我们的摄像头
sensor.reset() #初始化摄像头
设置采集的图像颜色
sensor.set_pixformat(sensor.RGB565)#设置图像色彩格式,有RGB565色彩图和GRAYSCALE灰度图两种
设置采集图像的大小
sensor.set_framesize(sensor.LCD) # 设置图像大小 为LCD 或 QQVGA2 (128X160)
跳过前两秒的图像采集,目的是等待摄像头初始化完成。
sensor.skip_frames(time = 2000)
采集一帧数据,并赋值给img
img = sensor.snapshot()
显示图像
lcd.write(img) # 通过LCD显示获得的画面 display the image.
三、KEY按键与拨码开关
1、硬件
.
按键引脚与LQ_MV对应关系:
拨码开关与LQ_MV对应关系:
注意:也不排除器件焊接方向不同,所以1、 2位置可能不同。
这就是我们板子上面的按键,常见的按键有2脚按键和4脚按键。上图所示就是开发板上的4脚按键,当按键按下的时候,对角线的两个引脚会被接通。我们将按键的引脚一端接GND,其对角线的引脚连接到我们的芯片的引脚上,当按键按下的时候会把芯片的引脚接到GND上,这时候我们再来检测这个引脚的状态,会发现变成了0,也就是低电平。我们利用这个特性来通过按键控制我们程序执行不同的代码。
拨码开关与按键在程序上使用方法是一样的,不同的是拨码开关拨到NO一侧会持续使引脚拉低,当拨回off一侧后才会使引脚回复高电平。
2、示例
打开按键的示例“Button.py”,
import pyb,time
from pyb import LED, Timer,Pin,LED
LEDR = LED(1) #LED 红色
LEDG = LED(2) #LED 绿色
LEDB = LED(3) #LED 蓝色
from LQ_Module import key
K_0 = key(key_pin=Pin("P30"))
K_1 = key(key_pin=Pin("P31"))
K_2 = key(key_pin=Pin("P1"))
while(True):
#***********检测K0按键**********************
if(K_0.down()):
LEDR.on()
print("K0 按键按下")
else:
LEDR.off()
#***********检测K1按键**********************
if(K_1.down()):
LEDG.on()
print("K1 按键按下")
else:
LEDG.off()
#***********检测K2按键**********************
if(K_2.down()):
LEDB.on()
print("K2 按键按下")
else:
LEDB.off()
- 当K0按键按下后红灯亮起,并且串口打印“K0 按键按下”
- 当K1按键按下后绿灯亮起,并且串口打印“K1 按键按下”
- 当K2按键按下后蓝灯亮起,并且串口打印“K2 按键按下”
- 当两个按键同时按下时灯的颜色会有所变化,并且同时打印两个按键按下的信息。
打开示例“Switch.py”文件
"""
@Untitled - By: LQ008 - Thu May 23 2024
@文件说明:使用本测试例程之前请先将 ‘LQ_Module.py’ 文件复制存放到模块根目录
测试母板上的拨码开关,基本功能
"""
import pyb, time
from pyb import LED, Timer,Pin,LED
from LQ_Module import key
LEDR = LED(1) #LED 红色
LEDG = LED(2)
LEDB = LED(3)
#初始化两个拨码开关引脚
SW1 = key(key_pin=Pin("P33"))
SW2 = key(key_pin=Pin("P32"))
#定义变量用来表示拨码开关的状态
Switch_Status = 0
while(True):
#******检测拨码开关状态********
if((SW1.value() == 1) and (SW2.value() == 1)): #拨码1关 拨码2关
Switch_Status = 0
elif((SW1.value() == 0) and (SW2.value() == 1)): #拨码1开 拨码2关
Switch_Status = 1
elif((SW1.value() == 1) and (SW2.value() == 0)): #拨码1关 拨码2开
Switch_Status = 2
elif((SW1.value() == 0) and (SW2.value() == 0)): #拨码1开 拨码2开
Switch_Status = 3
#******拨码1关 拨码2关**********************
if(Switch_Status == 0):
print("拨码1关 拨码2关")
#******拨码1开 拨码2关**********************
if(Switch_Status == 1):
LEDR.on()
print("拨码1开 拨码2关")
#******拨码1关 拨码2开**********************
if(Switch_Status == 2):
LEDG.on()
print("拨码1关 拨码2开")
#******拨码1开 拨码2开**********************
if(Switch_Status == 3):
LEDB.on()
print("拨码1开 拨码2开")
time.sleep_ms(200)
LEDR.off()
LEDG.off()
LEDB.off()
上方示例中将两个拨码开关组合起来控制灯的亮灭,代码运行后不同的拨码开关组合会打印不同的信息,并且亮起不同颜色的LED灯。
3、代码分析
首先,需要引用“LQ_Module.y”中的key方法。
from LQ_Module import key
“LQ_Module.y”是专用于LQ_MV的库,里面针对LQ_MV开发板定义了一些功能,就比如本次引用了该库中的key函数。
K_0 = key(key_pin=Pin("P30"))
将按键0对应的引脚“P30”指定给了变量“K_0”,然后就可以通过变量“K_0”读取按键0的按下与抬起。
if(K_0.down()):
LEDR.on()
print("K0 按键按下")
else:
LEDR.off()
print("K0 按键松开")
再通过if语句进行判断来执行不同的代码。这里仅为按键的常规用法,为方便同学们更好理解按键,这里将自定义按键的示例给出,该程序位是于LQ_MV文件夹中独立测试例程中“Button_key.py”,这里单独拿出来自行参考:
import pyb,time
from pyb import LED, Timer,Pin,LED
LEDR = LED(1) #LED 红色
LEDG = LED(2) #LED 绿色
LEDB = LED(3) #LED 蓝色
#------------------------- 自定义按键扫描start -------------------------------------
#按键初始化,按键扫描,母版上K0,K1,K2分别对应,P30,P31,P1
K0 = Pin('P30', Pin.IN, Pin.PULL_UP) # 板上有上拉 电阻,默认为高电平,低电平触发
K1 = Pin('P31', Pin.IN, Pin.PULL_UP)
K2 = Pin('P1', Pin.IN, Pin.PULL_UP)
# 定义初始状态
K0_state = False #布尔类型值
K1_state = False
K2_state = False
def Key_Scan():
if not K0.value():
time.sleep_ms(20) # 等待一段时间以消除抖动
if not K0.value(): # 再次检查按键状态
global K0_state
K0_state = not K0_state # 切换状态
while not K0.value(): # 等待按键释放
pass
return K0_state
if not K1.value():
time.sleep_ms(20) # 等待一段时间以消除抖动
if not K1.value(): # 再次检查按键状态
global K1_state
K1_state = not K1_state # 切换状态
while not K1.value(): # 等待按键释放
pass
return K1_state
if not K2.value():
time.sleep_ms(20) # 等待一段时间以消除抖动
if not K2.value(): # 再次检查按键状态
global K2_state
K2_state = not K2_state # 切换状态
while not K2.value(): # 等待按键释放
pass
return K2_state
#------------------------- 自定义按键扫描end -------------------------------------
while(True):
Key_Scan() # 注意长按为阻塞式,松开触发
if(K0_state): # 判断按键状态
LEDR.toggle() # 如果按键K0被按下一次,LEDR 状态翻转一次
K0_state = 0 # 清除按键状态,准备下一次触发
if(K1_state):
LEDG.toggle()
K1_state = 0
if(K2_state):
LEDB.toggle()
K2_state = 0
运行程序后,按拓展板上的三个按键,分别控制三色LED的三种颜色亮灭的状态。
四、电机的使用
1、硬件
LQMV4拓展控制板与三路电机驱动板引脚连接对应关系:
电机与LQ8701驱动板之间的接线示意如图:
驱动与控制板的实物接线如下图:
其中驱动电机的电源供电取自控制板上电源接口的并联接线座,红色为电源正极,黑色为负极,再接线时一定要看接口正负极的丝印标识,以免接错导致上电发生意外短路烧坏器件。
电机驱动板上红色的电压输入端的输入范围为6V至28V直流电压,推荐依据电机额定电压确定此处输入电压,在本教程中福来三轮车将统一使用8.4V锂电池作为小车电源输入。
蓝色端子为驱动板电压输出端,直接连接直流电机两端,此处正反之分没有实际意义,主要看驱动控制,但为了统一方向尽量接成一致的,三个端子连接三个电机,虽说没有正反之分,但如果你使用例程发现原本想要其正转的轮子反转了(不排除电机焊接的电源线红黑线存在不一致),那么有两种方式来让轮子变为正转:
① 调换该电机的两根线接线线序;
② 在程序中改变PWM值的正负号。
这两种方式都是可以改变电机转动的方向,其核心是改变经过电机线圈电流的方向,而②中改pwm值的正负其实是通过电机驱动板的方向控制I/O的电平来实现的,驱动输出电流的方向,假设PWM1有信号,I/O1高电平时正转,那么PWM1不变的情况下I/O1低电平反转。
白色端口为驱动板的信号输入端口同时也是电机驱动控制芯片的工作电压3.3V的输入端,也就是控制端口。此处连接如上表所示,其中每个PWM与I/O控制一个电机的转速与转向。PWM控制转速,I/O控制转向。
PWM简介:
PWM全称是Pulse Width Modulation,也就是脉冲宽度调制,波形如上图所示。
它的两个参数,一个是频率,另一个是占空比。
-
频率:固定值为10KHz
-
占空比:通俗的来讲就是一个周期内高电平所占的比值。例如上图中的百分比就表示了该PWM信号的占空比,以下通过表格距离其电压关系。
红色端子电压,蓝色端子电压,PWM及I/O这四者有什么样的关系,请看下表。
2、示例
打开IDE左上角文件>文档文件夹>LQ_MV>4_Motor中的8701三路驱动示例。
这里共有三个测试脚本文件来测试电机,第一个是通过改变定时器值改变PWM脉宽的方式;第二个是以总脉宽的百分比设置输出,也就是直接给占空比,例如下面第二个示例中“motor2.run_percent=50”表示将motor2的输出占空比设为50%,那么再一个周期内,方波的高电平和低电平各占一半,体现在电压关系上为输出电压为输入电压的一半,其实第三个测试文件则包含无感无刷驱动的程序,在本节实验中不需要看第三个。
打开两个8701的例程打开两个例程运行后电机运行,电机会慢慢加速,加速到最大转速的60%切换成反转。
两个例程略有不同,不同之处在于他们的控制方式,“8701_three-way_drive.py”采用的是更加细腻的控制方式,其把100%占空比分成了24000份,可以更加精确的控制电机的转速。
3、代码分析
#初始化pwm 及控制io,针对例如LQ8701电机驱动板
motor1 = motor(timer=4, chl=1, freq=10000, pin_pwm="P7", pin_io="P22")
motor2 = motor(timer=4, chl=2, freq=10000, pin_pwm="P8", pin_io="P23")
motor3 = motor(timer=4, chl=3, freq=10000, pin_pwm="P9", pin_io="P24")
指定电机1的速度控制引脚为P7,使用的是定时器4的通道1,设置PWM信号频率为10KHz,指定电机1的方向控制引脚为P22。其他两个电机同理。
括号里面参数的由来,参考资料中的“引脚功能说明”与“管脚分配”,如下图:
motor2.run_percent(duty) #duty范围为[-100,100]
motor2.run(duty) #duty范围为[-24000,24000]
初始化部分两个例程都是一样的,区别在于设置PWM占空比时的方式不一样。
motor2.run 为设置脉宽值,其范围从-24000到24000,为负数的时候电机为反转,绝对值越大驱动输出给电机的电压越高,电机转速越高。
motor2.run_percent 为设置脉宽百分比,范围为-100到100。很多时候为了便于动态调整电机输出仅采用前者的控制方式,这样一来就有很大的动态调整范围,控制也越细腻。
五、霍尔编码器
1、硬件
.
霍尔编码器与母板接连接对应关系(实际物理线序参考下面的实物图片标注):
编码器的接口在视觉模块拓展板后面,霍尔电机包种有专用的排线。将编码器接口与电机上的接口用排线连接起来即可,器连接示意如下图(注意母板上接口与电机编码器接口线序不一致但排线已做处理,可直插使用,这里图片与表格线序不做对应):
这里需要注意的是,编码器霍尔输出A和B实质上没有区分意思,也就是说如果A,B既可以由程序定义哪个是A哪个是B,也可以在程序不变的情况下调换 A, B两根线的位置。最终达到能区分电机转向就可以。
2、示例
打开编码器的示例5_Enc_Read文件中的DRV8701_Encoder.py:
切换到“DRV8701_Encoder.py”文件,单击运行前需要确保电机接线正确,并确保电机的三个轮子离开地面(可以拿在手里或使用电池充电器盒子支撑物将小车撑起,可参考下图),以免程序运行时小车原地快转。
将例程中的占空比改小一点,例如下图中的40然后单击运行后电机会同步转起来,此时查看编码器读数。
因为三个电机安装按照前面的示意图接线,所以同一个PWM下,转向相同编码器读取的方向也相同,且数值都在70左右,如果你发线某个电机的数值差异大,可以给PWM输出增加补偿使得他们的编码器值接近,实际转速接近。
接下来切换到“Test_Encoder.py”文件,单击运行,打开串行终端,用手专用电机2的轮子,查看串行终端的数据。
3、代码分析
Test_Encoder.py代码如下:
import pyb,time
from pyb import Timer
from LQ_Module import Enc_AB, Encoder
#任选一个定时器并独占(其他地方不能再使用否则会冲突),若方向不对调换Enc_A,Enc_B位置即可
#初始化 定时器号,频率(多久读一次),管脚,适用于AB和带方向的编码器,以及单一脉冲的编码器
Enc1 = Enc_AB(Timer(14, freq=10), Enc_A="P27", Enc_B="P21") #带方向的只累计单一方向计数值
Enc2 = Enc_AB(Timer(13, freq=10), Enc_A="P28", Enc_B="P29")
Enc3 = Enc_AB(Timer(12, freq=10), Enc_A="P25", Enc_B="P26")
#--------------------------------------------------------------------------------------------
while True:
time.sleep_ms(30) # 延时多久都可以
print("ENC1=",Enc1.Get(),"; ENC2=",Enc2.Get(),"; ENC3=",Enc3.Get()) #USB串口打印计数值
主要看编码器初始化Enc_AB传入的参数。
Enc1 = Enc_AB(Timer(14, freq=10), Enc_A="P27", Enc_B="P21") #带方向的只累计单一方向计数值
首先是Timer:
Timer传入的第一个参数为定时器号,这里需要使用单独的定时器,不可同时用于电机舵机等。第二个参数为采样频率,采样频率直接影响编码器的大小以及实时性,采样频率越高电机相同转速下采集到的编码器值越小,实时性越好;与之对应的采样频率越低,采集到的编码器值越大,实时性越差。
Enc_A表示霍尔编码器的A项所接的引脚。
Enc_B表示霍尔编码器的B项所接的引脚。
编码器值读取
Enc1.Get() #编码器1读取值
Enc2.Get()# 编码器2读取值
Enc3.Get()# 编码器3读取值
DRV8701_Encoder.py代码为电机测试例程与编码器例程相融合而成,由电机驱动板驱动电机,可以设置不同的速度来观察编码器读数。
六、图像处理-色块寻找
1、运行示例程序
打开如下图所示例程:
可以将下面的程序复制然后把软件中的替换掉。程序没有改动,只是把注释换成了中文的。
# 单颜色组合识别例程## 这个例子显示了使用OpenMV的单色代码跟踪。##颜色代码是由两种或更多颜色组成的色块。下面的例子只会跟踪同时具有以下两种颜色的彩色物体。
import sensor, image, time, math
# 颜色跟踪阈值(L Min, L Max, A Min, A Max, B Min, B Max)# 下面的阈值一般跟踪红色/绿色的东西。你可以调整它们…
thresholds = [
(30, 100, 15, 127, 15, 127), # generic_red_thresholds -> index is 0 so code == (1 << 0)
(30, 100, -64, -8, -32, 32)
]
# generic_green_thresholds -> index is 1 so code == (1 << 1)
# 当“find_blobs”的“merge = True”时,code代码被组合在一起。
sensor.reset()#初始化摄像头,reset()是sensor模块里面的函数
sensor.set_pixformat(sensor.RGB565)#设置图像色彩格式,有RGB565色彩图和GRAYSCALE灰度图两种
sensor.set_framesize(sensor.QVGA)#设置图像像素大小
sensor.skip_frames(time = 2000)
sensor.set_auto_gain(False) # 颜色跟踪必须关闭自动增益
sensor.set_auto_whitebal(False) # 颜色跟踪必须关闭白平衡
clock = time.clock()
# 只有比“pixel_threshold”多的像素和多于“area_threshold”的区域才被# 下面的“find_blobs”返回。 如果更改相机分辨率,# 请更改“pixels_threshold”和“area_threshold”。 “merge = True”合并图像中所有重叠的色块。
while(True):
clock.tick()
img = sensor.snapshot()
for blob in img.find_blobs(thresholds, pixels_threshold=100, area_threshold=100, merge=True):
if blob.code() == 3: # r/g code == (1 << 1) | (1 << 0)
# These values depend on the blob not being circular - otherwise they will be shaky.
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(blob.rect())
img.draw_cross(blob.cx(), blob.cy())
# 注意-色块的旋转rotation是0-180内的唯一。
img.draw_keypoints([(blob.cx(), blob.cy(), int(math.degrees(blob.rotation())))], size=20)
print(clock.fps())
将代码替换完成后,我们运行这个代码。运行起来后我们用摄像头拍摄一些东西,可以在软件的帧缓冲区看到图像。此时我们停止运行后会有一帧数据保存在帧缓存区中。
2、调节识别阈值
我们用软件自带的工具“阈值编辑”器获取我们想要的阈值。点击后选择会有弹窗,这里我们选择帧缓冲区。
需要注意的时,在调解阈值时,我们需要先打开一个显示图像的示例,比如示例中的第一个“hello world.py”,然后将图像大小调节成与我们识别应用的程序中的大小一致,比如你识别程序中初始化图像大小为QQVGA2(128*160像素与LCD大小一致),那么就将打开的hello world程序中的图像大小改为相同大小,然后点击运行这个程序并将镜头对准识别的物体色块上。
打开“阈值编辑器”之后可以调节下面的滑块筛选我们需要的范围。右侧图像中白色部分表示我们需要的图像中的颜色。
简单了解图像的LAB格式表示方法。为何要用LAB而不用RGB是因为LAB有一个很好的特性:设备无关。也就是用LAB格式表示的颜色与使用的显示介质没有关系。
L、A、B都是实数,不过实际一般限定在一个整数范围内:
L越大,亮度越高。L为0时代表黑色,为100时代表白色。
A和B为0时都代表灰色。
A从负数变到正数,对应颜色从绿色变到红色。
B从负数变到正数,对应颜色从蓝色变到黄色。
观察上图我们可以看到当前LAB阈值筛选的颜色为红色,调节下面滑块,将想要筛选的颜色筛选出来。注意,这里由于不同环境光照不同,所以同一种物品的阈值会存在一定的差异。
调整完成后复制下方的阈值,然后关闭当前页面,比如想要获取当前环境下红色的阈值,那么调整完毕后如下图,复制当前阈值。
将代码此处的值替换为刚才获取的阈值。
再次进入“阈值编辑器”,点击重置滑块,再次手动滑动滑块,这次选择筛选蓝色。
筛选完成后复制阈值并关闭当前页面。
替换代码中第二段阈值。
替换完成后运行程序,并将镜头对准刚才筛选的两个物体。观察帧缓冲区。
帧缓冲区将刚才两个包含两个颜色的物体框起来。
如果只想获取一种颜色组成的物体,那么按照下面方式修改代码即可。
将帧缓冲区的图像显示到显示屏上。
根据第二节的内容进行修改。
首先添加LCD相关库,
第二步修改图像大小为LCD或者QQVGA2
然后添加lcd对象。
最后,将图像写到LCD屏幕中,注意要在画完方框后再将图像写入。不然显示屏上不会出现处理过后的图像。
显示结果如下图所示。
3、代码分析——重要
以下内容有关颜色识别处理相关函数说明,想要做好识别处理,必须熟悉重要的几个函数及其参数意义,这样才识别处理中才能更好地通过这些函数的运用和参数达到想要的图像处理效果。
循环之前的代码我们再显示屏章节讲过了,不过在初始化阶段新增加了两段摄像头初始化语句。这两句是必须加的。
sensor.set_auto_gain(False) # 颜色跟踪必须关闭自动增益
sensor.set_auto_whitebal(False) # 颜色跟踪必须关闭白平衡
下面我们主要看一下“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=None[,merge_cb=None[,x_hist_bins_max=0[,y_hist_bins_max=0]]]]]]]]]]]])
上面这个方法的定义我们不必弄明白什么意思,实际在使用的时候我们只关心需要传入什么参数,然后我们可以得到什么东西,得到的东西的含义是什么。
for blob in img.find_blobs(thresholds, pixels_threshold=100, area_threshold=100, merge=True): #例程中的代码
(1) 关于传入参数
thresholds必须是元组列表,意思就是必须要是一组用中括号括起来的数据,对于RGB565图像,每个元组需要有六个值(l_min,l_max,a_min,a_max,b_min,b_max)分别是LAB格式中L、A和B通道的最小值和最大值。也就是之前在“阈值编辑器”中获取到的阈值范围。
为方便使用,此功能将自动修复交换最小值和最大值。此外,如果元组大于六个值,则忽略其余值。相反,如果元组太短,则假定其余阈值处于最大范围。
除了thresholds是必须元素外,例程中还有其他值pixels_threshold与area_threshold以及merge。
若一个色块的边界框区域小于 area_threshold ,则会被过滤掉。
若一个色块的像素数小于 pixel_threshold ,则会被过滤掉。
merge 若为True,则合并所有没有被过滤掉的色块,这些色块的边界矩形互相交错重叠。
其他更多参数参考:打开后搜索find_blobs
https://book.openmv.cc/example/10-Color-Tracking/single-color-code-tracking.html
(2) 关于返回值
例程中将返回值赋值给了对象“blob”。该对象具有以下方法。
blob.corners()
返回对象的4个角的4(x,y)元组列表。从左上方开始按顺时针顺序返回角。
blob.min_corners()
返回包含4个角的4(x,y)元组的列表,该元组的边界大于该Blob的最小面积矩形的边界。与blob.corners()不同,最小面积矩形的角并不一定位于blob上。
blob.rect()
返回一个矩形元组(x, y, w, h),用于如色块边界框的image.draw_rectangle等其他的image方法。
blob.x()
返回色块的边界框的x坐标(int)。
您也可以通过索引 [0] 取得这个值,即blob[0]。
blob.y()
返回色块的边界框的y坐标(int)。
您也可以通过索引 [1] 取得这个值。
blob.w()
返回色块的边界框的w坐标(int)。
您也可以通过索引 [2] 取得这个值。
blob.h()
返回色块的边界框的h坐标(int)。
您也可以通过索引 [3] 取得这个值。
blob.pixels()
返回从属于色块(int)一部分的像素数量。
您也可以通过索引 [4] 取得这个值。
blob.cx()
返回色块(int)的中心x位置。
您也可以通过索引 [5] 取得这个值。
blob.cxf()
返回blob(浮点数)的质心x位置。
blob.cy()
返回色块(int)的中心y位置。
您也可以通过索引 [6] 取得这个值。
blob.cyf()
返回blob(浮点数)的质心y位置。
blob.rotation()
返回色块的旋转(单位:弧度)。如果色块类似铅笔或钢笔,那么这个值就是介于0-180之间的唯一值。如果这个色块圆的,那么这个值就没有效用。
您也可以通过索引 [7] 取得这个值。
blob.rotation_deg()
以度为单位返回blob的旋转角度。
blob.rotation_rad()
以弧度为单位返回blob的旋转度数。这个方法比 blob.rotation() 更具描述性。
blob.code()
返回一个32位的二进制数字,其中为每个颜色阈值设置一个位,这是色块的一部分。例如,如果您通过image.find_blobs来寻找三个颜色阈值,这个色块可以设置为0/1/2位。注意:除非以merge=True调用image.find_blobs,否则每个色块只能设置一位。那么颜色阈值不同的多个色块就可以合并在一起了。您也可以用这个方法以及多个阈值来实现颜色代码跟踪。
您也可以通过索引 [8] 取得这个值。
blob.count()
返回合并到此Blob中的Blob数。 只有您以 merge=True 调用 image.find_blobs 时,这个数字才不是1。
您也可以通过索引 [9] 取得这个值。
blob.perimeter()
返回该blob周长上的像素数。
blob.roundness()
返回0到1之间的值,表示对象的圆度。一个圆将是1。
blob.elongation()
返回一个介于0和1之间的值,该值表示对象的长度(不是圆形)。一条线将是1。
blob.area()
返回色块周围的边框面积(w * h)
blob.density()
返回这个色块的密度比。这是在色块边界框区域内的像素点的数量。 总的来说,较低的密度比意味着这个对象的锁定得不是很好。结果在0和1之间。
blob.extent()
是blob.density()的别名。
blob.compactness()
类似blob.density(),但是,使用blob的周长来衡量对象的密度,因此更准确。结果在0和1之间。
blob.solidity()
类似blob.density(),但是,使用旋转的最小面积矩形与边界矩形来衡量密度。结果在0和1之间。
blob.convexity()
返回一个0到1之间的值,表示对象的凸度。正方形是1。
blob.x_hist_bins()
返回blob中所有列的x轴直方图。Bin值在0和1之间缩放。
blob.y_hist_bins()
返回blob中所有行的y轴直方图。Bin值在0和1之间缩放。
blob.major_axis_line()
返回blob的主轴(这条线穿过最小面积矩形的最长边)的行元组(x1, y1, x2, y2),可以使用 image.draw_line() 来绘制它。
blob.minor_axis_line()
返回blob的次轴(这条线穿过最小面积矩形的最短边)的行元组(x1, y1, x2, y2),可以使用 image.draw_line() 来绘制它。
blob.enclosing_circle()
返回一个圆(包围blob的最小面积矩形的圆)元组(x, y, r),可以使用 image.draw_circle() 来绘制它。
blob.enclosed_ellipse()
返回一个椭圆(包围blob的最小面积矩形的椭圆)元组(x, y, rx, ry, rotation),可以使用image.draw_ellipse()来绘制它。
每个色块对象有一个代码值code,该值为一个位向量。
if blob.code() == 3: # r/g code == (1 << 1) | (1 << 0)
若您在image.find_blobs中输入两个颜色阈值,则第一个阈值代码为1,第二个代码为2(第三个代码为4,第四个代码为8,以此类推)。合并色块对所有的code使用逻辑或运算(|),以便您知道产生它们的颜色。这使得您可以追踪两个颜色,若您用两种颜色得到一个色块对象,则可能是一种颜色代码。这也就解释了为什么例程代码中判断是否等于3。因为例程中传入了两个颜色的阈值,判断两个颜色的阈值是否同时满足就需要判断返回的code是否为3。
通过上面的返回值,再来看代码。
while(True):
clock.tick()
img = sensor.snapshot()
for blob in img.find_blobs(thresholds, pixels_threshold=100, area_threshold=100, merge=True):
if blob.code() == 3: # r/g code == (1 << 1) | (1 << 0)
# These values depend on the blob not being circular - otherwise they will be shaky.
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(blob.rect())
img.draw_cross(blob.cx(), blob.cy())
# 注意-色块的旋转rotation是0-180内的唯一。
img.draw_keypoints([(blob.cx(), blob.cy(), int(math.degrees(blob.rotation())))], size=20)
lcd.write(img)
print(clock.fps())
上面代码翻译成白话文如下:
死循环:
获取时间
获取一帧图像
在这一帧图像中寻找阈值在thresholds中的色块,并且色块的边界框大小不小于100,像素大小不小于100,合并所有没有被过滤掉的色块。
如果上面两个颜色的色块都筛选出来了:
如果返回的色块长度大于0.5那么执行:
用红色框框出它的边界来,
用绿色线画出此色块的主轴,
用蓝色线画出此色块的次轴。
不管色块长度是否大于0.5都要用白色框画出矩形,与中心十字,还有具有指向偏移方向的圆。
不管是否有合适的色块,都要将图像显示到屏幕中。
打印帧率。
■ 相关文献链接: