获得了物体的坐标后,可以用它来完成一些有趣的事情,例如把物体当作“笔”在图像上绘制出图样。我们可以选择一种颜色的黏土,将其固定在任意棒状物(例如铅笔)的一端用 OpenCV 绘制小圆点为了让魔法棒实现画图的效果,我们需要学习用 OpenCV 进行图形的绘制。画小圆点可以利用 circle 函数来实现:圆心坐标和线的颜色必须用小括号括起;颜色按 BGR 的顺序指定,3 个参数依次为蓝色值、绿色值、红色值,范围为 0~255;线宽的参数为 -1 时表示圆被实心填充,绘制圆点时指定这个参数为 -1 即可。代码如下所示。该语句可以在 frame 图像上以 ( x , y ) 点为圆心绘制一个半径为 2 的黄色实心圆。但此前输出的目标物体中心坐标 x 和 y 都是小数(float 类型),而 circle 函数只能接受整数(int 类型)的圆心坐标值。Python 中,使用 int 函数可以将其他类型的值转为整数。对小数而言,使用该函数转化后将直接舍弃掉小数点后的数据。结合这一语句,我们可以在图像上检测到的物体中心坐标上打上一个小圆点。通常情况下,为了绘点时看起来更舒适,我们期望能让摄像头像照镜子一样显示镜面的图像,这可以用 flip 函数转换得到,例如:翻转方式值为 0 表示上下翻转,为任意正数表示左右翻转,为任意负数表示上下左右均翻转。flip 函数将对传入的图像进行翻转并返回翻转后的图像。Python 中的列表与元组现在我们只能让 OpenCV 在每一帧中绘制一个点。要绘制连续的点,需要将过去所有帧中物体的中心坐标记录下来,再将它们在一帧中全部绘制出来。在需要存储一组数据时,我们可以使用 Python 中的列表(list)类型。列表是一种数据类型,它可以存储一系列按顺序排列的数据。这些数据可以是一个数字、一个字符串,或是另一个列表。我们可以用下面的语句给一个列表型变量 list 赋值:list = [1, 2, 3, 4, 5]对列表赋值时须使用中括号 [ ] 括起,并以逗号分隔其中的数据。这个名为 list 的列表按顺序排列了 5 个数字,使用列表名 [ 排列序号 ] 的方式可以调出指定位置的值。排列序号从 0 开始,例如 list[0] 将取出第一个值 1。列表中的每一个值被称为列表中的一个元素。在记录物体坐标的例子中,我们可以先建立一个空的列表:每刷新一帧找到新的坐标值时,使用 append 函数可以方便地将一个值添加到列表pointlist 的最后一位:这里用中括号括起的 [x, y] 实际上也是一个列表,它包含 x 和 y 两个值。在此前的程序中,实际上我们曾多次提到过“一组数据”这个概念,它代表了与列表非常类似的元组类型。元组(tuple)是 Python 中另一种由一系列按顺序排列的数据构成的数据类型,它基本上是一个不可修改的列表。例如,元组不能用 append 函数追加一个值,也不能对其中的任何值进行更改。在赋值时,元组需要使用小括号括起,而不是用中括号括起:在不涉及修改操作时,列表和元组能使用的函数基本是一致的。例如 len 函数可以得到一个列表或元组中元素的数量,也就是它的长度。for 循环遍历结构按照这样的方式,我们可以得到一个存储着过往每一帧物体中心坐标的列表。接下来只需要在这个列表中的每一个坐标上都画上一个小圆点即可。要实现这一操作,可以使用 for 循环遍历结构。与 while 循环类似,for 循环结构也是一个循环型结构,但它主要用于对列表、元组等的元素进行遍历。for 循环结构的写法如下:我们需要设定一个变量,在每一次循环中依次用列表或元组中的元素对它赋值,for 循环遍历的对象也可以不是列表或元组,而是一个字符串或是数值范围。 该程序段可以从左至右按顺序打印出字符串中的每一个单个的字符此段程序则可以从 0 开始以 1 递增,打印到 99 为止。注意这里的 range( 范围起点 ,范围终点 ) 设定了遍历的数值范围,实际遍历的区间包括起点,但不包括终点。回到绘制列表中小圆点的例子中,该程序也可以用 for 循环结构编写在每次循环中,point 变量存储着列表中的一个元素,即形式为 [x, y] 的坐标列表。用point[0] 和 point[1] 可以分别得到对应的横坐标和纵坐标。按键值与键盘控制利用 OpenCV 的 waitKey 函数可以方便地实现手动开始、暂停或清除图案的功能。此前我们知道,waitKey 函数必须跟在 imshow 之后,表示在显示一帧后等待的时间(单位为毫秒)。事实上,设置 waitKey 函数的主要作用是在等待的时间内获取键盘的按键指令:这样得到的 k 变量可以获得在这段时间内键盘按下的按键值。在计算机中,每一个键盘按键都可与一个数字值对应,对应的方式称为键盘编码。OpenCV 使用的编码方式是 ASCII(美国信息交换标准代码)。我们并不需要记忆每一个按键对应的值,可以用Python 中的 ord 函数将按键的字母、符号或数字转化为其 ASCII 值。其代码如下所示。这样我们在按下 q 键后,程序将跳出循环。为了用键盘指令告诉程序开始绘制,可以建立一个变量来表示绘制状态,例如令 start = 0,0 表示不绘制,1 表示进行绘制。首先在循环中加入判断,只有当 start = 1 时才记录目标物体的坐标。 再用一个键盘指令改变 start 的值。这样我们在运行程序后按下键盘 s 键即可开始绘制。依照相似的方式,我们也可以通过改变 start 变量的取值实现绘制图案的停止、清除等键盘指令。“魔法棒”的完整示例代码import cv2 cap = cv2.VideoCapture(0) # 开始读取摄像头信号 pointlist = [] # 声明一个列表用于存储点的位置 start = 0 # 声明一个变量表示是否开始记录点的位置 while cap.isOpened(): # 当读取到信号时 (ret, frame) = cap.read() # 读取每一帧视频图像为 frame hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # 将颜色空间转换为 HSV yellow_lower = (26, 43, 46) # 指定目标颜色的下限 yellow_upper = (34, 255, 255) # 指定目标颜色的上限 mask = cv2.inRange(hsv, yellow_lower, yellow_upper) # 使用目标范围分割 图像并二值化 (mask, cnts, hierarchy) = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 寻找其中的所有外轮廓 if len(cnts) > 0: # 如果至少找到一个轮廓 c = max(cnts, key=cv2.contourArea) # 找出其中面积最大的轮廓 ((x, y), radius) = cv2.minEnclosingCircle(c) # 分析轮廓的中心位置和 大小 if radius > 20: # 仅当半径大于 20 时 if start == 1: #start = 1 说明开始记录 pointlist.append([x, y]) # 将点的位置追加到 pointlist 列表 elif start == 0: #start = 0 说明需要清除图像上的点 pointlist = [] # 将 pointlist 重新置空 for point in pointlist: # 遍历 pointlist 中所有点的位置 x = point[0] y = point[1] # 在这些点的位置上绘制一个彩色小圆点 cv2.circle(frame, (int(x), int(y)), 2, (0, 255, 255), -1) cv2.imshow('MagicWand', frame) # 将图像显示到屏幕上 k = cv2.waitKey(5) # 每一帧后等待 5 毫秒,并将键盘的按键值存为 k # 如果按 q 键,程序退出;按 s 键,开始绘制;按 p 键,停止绘制;按 e 键,清除绘制 if k == ord("q"): break elif k == ord("s"): start = 1 elif k == ord("p"): start = -1 elif k == ord("e"): start = 0
三轴机械臂结构分析机械臂在不同使用场景中的设计有所不同。这种机械臂有 3 个驱动用的舵机,其中底端的舵机 1 用来控制机械臂整体在平面内的转动;舵机 2 控制后臂绕底部关节的上下运动,舵机转角等同于图 6.3 中的ɑ 角,最大旋转角度为 180°,90°对应后臂垂直于桌面;舵机 3 控制前臂绕中间关节的上下运动,舵机转角等同于图 6.3 中的 β 角,最大旋转角度为 180°,90°对应前臂平行于桌面。程序编写连接完成后,硬件上的控制准备就完成了。我们接下来要编写一段程序,让树莓派控制机械臂转动。打开 Geany 编辑器,输入下面的程序,并保存文件。然后打开机械臂电源,在树莓派上运行此程序即可控制机械臂运转 下面对程序中每行代码进行说明。 导入调用串口的 Python 第三方包 serial。 导入 python 中与时间相关的扩展包。我们需要知道应该发送什么信息给舵机控制板才能按照预想的方式控制机械臂,但是舵机控制板可以识别的信息往往有特殊的形式。调用第三方包 roboticarm 中预置的 get_message 函数可以帮助我们转换需要发送的信息使用 serial.Serial(串口位置 , 串口通信波特率)可以初始化树莓派的串口通信。在预先配置好的树莓派中,其硬件串口通信的串口位置是“/dev/ttyAMA0”。舵机控制板默认的通信波特率是 9600 波特,填入 9600 即可。初始化完成后,我们将它存为 ser。通过 ser.write(通信信息)的方式可以向外发送串口信息。在 get_message 函数中,我们需要填入 3 个信息:get_message(控制板编号,PWM 值,速度)。控制板编号是指舵机控制板上 M0~M23 的编号值,例如 M1 的编号是 1。PWM 值表示舵机的转角,500~2500 对应 0°~180°,例如 500 对应 0°,1500对应 90°,2500 对应 180°。速度是指舵机从当前角度运动到新角度的快慢。0~40 对应 0~360°/s,速度为 1 表示每秒转动 9°;最大值为 40,即每秒转动 360°。这行代码的意思是,让 M1 接口连接的舵机以 36° /s 每秒的速度运动到 90°的位置。由于舵机运动需要一定时间,因此在每次发送串口信息后需用 time.sleep() 等待舵机运动结束再继续运行后面的程序。全部程序运行完后,用 ser.close() 关闭该串口连接。总结一下,因为我们是用程序控制树莓派给舵机控制板发送串口通信信号,然后由舵机控制板来驱动机械臂运动的,所以在树莓派上编写的程序整体流程如下:(1)导入必要的模块;(2)初始化树莓派的串口通信;(3)发送串口信息,让某个舵机以一定速度转动一定角度;(4)关闭该串口连接。用摄像头找到木块位置我们可以把这个任务分解为这样几个步骤:(1)摄像头获取图像;(2)进行图像分析,得到待抓取木块的位置;(3)控制机械臂吸盘到达目标位置的正上方;(4)控制机械臂抓取木块;(5)控制机械臂移动到特定位置,放下木块。使用此前连接测试摄像头的程序调用 OpenCV 检查摄像头是否能正确首先,为方便分析,我们将机械臂的初始位置定在轴 1置中,轴 2、轴 3均为 90°的状态 机械臂初始位置 3 个舵机均需要置中,PWM 信号值均为 1500,程序如下所示:仿照此前学过的识别颜色的程序,假定目标颜色为黄色,下面的程序可以找到摄像头区当物体位于吸盘正下方时,它的坐标是多少我们知道,机械臂可以根据指定物体的实时坐标不断调整位置,直到物体位于吸盘的正下方。因此,我们需要先了解当物体位于吸盘正下方时的坐标情况。我们将一个物体(这里还用木块示范,你可以选用其他物体)摆放在吸盘的正下方,观察它在图像中的大致位置,如图可以看出,当物体位于吸盘正下方时,在摄像头图像中,它位于左右方向的中点上,并 且根据树莓派摄像头的图像坐标系可知,此时物体中心的x 坐标应为 320。物体在上下方向的位置并不在中点,而是在中点上方一定距离,这是因为摄像头与吸盘的位置并不重叠。那么此时物体中心的 y 坐标是多少呢?它和图像中心的纵向距离是否就是摄像头与吸盘的距离呢?我们可以保持物体处于吸盘的正下方,调整吸盘的高度,观察物体在图像中的位置变化,可以观察到:◆ 无论高度如何变化,物体都处于图像左右的中轴线上;◆ 摄像头高度越低,物体在图像中占据的面积就越大;◆ 摄像头高度越低,其中心在图像中的位置就离中轴线越远。因此,如果可以让机械臂在调整位置时前端维持在同一高度,那么当物体位于吸盘正下方时,它在图像中的 y 坐标就是可以确定的。我们以机械臂初始位置(即轴 1 置中,轴 2、轴 3 均为 90°的状态)的高度值为准。此后的调整均保持此高度不变。在此状态下,物体的坐标为(320, 150)。请注意,不同系统会有差别,以实际情况为准。现在我们已经知道了吸盘位于物体正上方时物体的坐标。那么当摄像头识别到指定物体的坐标后,应该调整机械臂的位置,使物体坐标接近(320, 150)。机械臂的调整可以分为两步:(1)控制 1 号舵机左右运动,使物体处于图像左右方向的正中间( x 坐标等于320);(2)控制 2 号、3 号舵机,使机械臂末端在同一高度前后运动,使物体 y 坐标等于150。控制 1 号舵机左右运动,使物体x 坐标为 320我们需要先调整 1 号舵机的角度,使得物体的 x 坐标为 320。我们已经知道,当 1 号舵机角度减小时,机械臂向右运动;舵机角度增加,机械臂向左运动。因为摄像机镜头是倒置的,所以当探测到的物体 x 坐标大于 320 时,说明其位于吸盘左侧,机械臂需要向左转动;当 x 坐标小于 320 时,机械臂需要向右转动。首先,我们声明一个变量,表示轴 1 舵机的 PWM 值,其初始值为 1500。然后在识别到物体坐标后,调整机械臂的运动,程序如下这样,结合前面的图像识别和机械臂控制的程序,我们就得到了一个能够控制 1 号舵机左右运动,使物体 x 坐标接近 320 的一个完整程序:import serial # 导入调用串口的 Python 第三方包 serial import cv2 # 导入 OpenCV 包 import time # 导入 time 包 from roboticarm import get_message # 导入控制机械臂所必须的 get_message 函数 # 声明一个变量,表示轴 1 舵机的 PWM 值 pwm1 = 1500 ser = serial.Serial("/dev/ttyAMA0", 9600) # 初始化树莓派的通信串口 # 机械臂位置初始化 ser.write(get_message(1, 1500, 4)) ser.write(get_message(2, 1500, 4)) ser.write(get_message(3, 1500, 4)) time.sleep(2) # 等待舵机执行完成 # 识别黄色物体的坐标 cap = cv2.VideoCapture(0) while cap.isOpened(): (ret, frame) = cap.read() hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) yellow_lower = (26, 43, 46) yellow_upper = (34, 255, 255) mask = cv2.inRange(hsv, yellow_lower, yellow_upper) (mask, cnts, hierarchy) = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(cnts) > 0: c = max(cnts, key=cv2.contourArea) ((x, y), radius) = cv2.minEnclosingCircle(c) if radius > 50: print("center: ", (x, y)) # 根据坐标控制机械臂运动 if x > 320: pwm1 += 1 else: pwm1 -= 1 ser.write(get_message(1, pwm1, 4))
控制机械臂等高运动,使物体y 坐标为 150
import serial
import cv2
import time
import numpy
from roboticarm import get_message
from roboticarm import get_angle
# 获取摄像头视频数据并初始化串口
cap = cv2.VideoCapture(0)
ser = serial.Serial("/dev/ttyAMA0", 9600)
# 载入位置文件并设定相关变量
matrix = numpy.load("/home/pi/position.npy")
pwm1 = 1500
distance = 1000
height = 1000
count = 0
# 使用 get_angle 函数将位置坐标转换为 2 号、3 号舵机的 PWM 值
(pwm2, pwm3)=get_angle(matrix, height, distance)
# 机械臂位置初始化
ser.write(get_message(1, pwm1, 4))
ser.write(get_message(2, pwm2, 4))
ser.write(get_message(3, pwm3, 4))
time.sleep(2)
while cap.isOpened():
(ret, frame) = cap.read()
# 跳过前 60 帧
if count <= 60:
count += 1
continue
# 识别黄色物体坐标
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
yellow_lower = (26, 43, 46)
yellow_upper = (34, 255, 255)
mask = cv2.inRange(hsv, yellow_lower, yellow_upper)
(mask, cnts, hierarchy) = cv2.findContours(mask, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
if len(cnts) > 0:
c = max(cnts, key=cv2.contourArea)
((x, y), radius) = cv2.minEnclosingCircle(c)
if radius > 50:
print("center: ", (x, y))
# 根据坐标控制机械臂运动
if x > 330:
pwm1 += 5
elif x < 310:
pwm1 -= 5
if y > 160:
distance += 5
elif y < 140:
distance -= 5
(pwm2, pwm3) = get_angle(matrix, height, distance)
ser.write(get_message(1, pwm1, 3))
ser.write(get_message(2, pwm2, 3))
ser.write(get_message(3, pwm3, 3))
ser.close() # 关闭串口连接
可以明显地看出,图 7.7 中右侧的脸颊位置整体比眼窝位置更白,即平均灰度值更大。
import cv2
face_cascade = cv2.CascadeClassifier('/home/pi/cascade/haarcascade_
frontalface_default.xml') # 载入人脸分类器
cap = cv2.VideoCapture(0) # 开始读取摄像头信号
while cap.isOpened():
(ret, frame) = cap.read() # 读取每一帧视频图像为 frame
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 将图像转换为灰度图
faces = face_cascade.detectMultiScale(gray, minSize=(100, 100),
flags=cv2.CASCADE_SCALE_IMAGE) # 检测人脸的位置
for (left, top, width, height) in faces: # 遍历找到的人脸的位置信息
frame = cv2.rectangle(frame, (left, top), (left + width, top +
height), (0, 0, 255), 2) # 绘制矩形框
cv2.imshow('cascade', frame) # 预览图像
cv2.waitKey(5) # 每帧等待 5 毫秒
cap.release()
cv2.destroyAllWindows()