1.做一个魔法棒吧
获得了物体的坐标后,可以用它来完成一些有趣的事情,例如把物体当作“笔”在图像上绘制出图样。我们可以选择一种颜色的黏土,将其固定在任意棒状物(例如铅笔)的一端并揉成球形,做一个 DIY 的“魔法棒”
用 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 循环遍历的执行逻辑如图
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
2.认识机械臂
三轴机械臂结构分析机械臂在不同使用场景中的设计有所不同。
这种机械臂有 3 个驱动用的舵机,其中底端的舵机 1 用来控制机械臂整体在平面内的转动;舵机 2 控制后臂绕底部关节的上下运动,舵机转角等同于图 6.3 中的ɑ 角,最大旋转角度为 180°,90°对应后臂垂直于桌面;舵机 3 控制前臂绕中间关节的上下运动,舵机转角等同于图 6.3 中的 β 角,最大旋转角度为 180°,90°对应前臂平行于桌面。我们使用很常见的一种 16 路舵机控制板来控制舵机
程序编写连接完成后,硬件上的控制准备就完成了。我们接下来要编写一段程序,让树莓派控制机械臂转动。打开 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,程序如下所示:
仿照此前学过的识别颜色的程序,假定目标颜色为黄色,下面的程序可以找到摄像头区域中半径大于 50 像素的最大黄色块的中心坐标。![]()
使吸盘位于物体正上方
当物体位于吸盘正下方时,它的坐标是多少我们知道,机械臂可以根据指定物体的实时坐标不断调整位置,直到物体位于吸盘的正下方。因此,我们需要先了解当物体位于吸盘正下方时的坐标情况。我们将一个物体(这里还用木块示范,你可以选用其他物体)摆放在吸盘的正下方,观察它在图像中的大致位置,如图
可以看出,当物体位于吸盘正下方时,在摄像头图像中,它位于左右方向的中点上,并 且根据树莓派摄像头的图像坐标系可知,此时物体中心的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))
运行这段程序,观察运行效果。我们发现这段程序可能导致 3 个明显的问题:
(1)程序开始运行后一段时间内运动异常(如物体在左边,却向右运动);
(2)调整速度太慢;
(3)在目标位置附近来回摆动,不会停止。
造成第一个问题的原因是摄像头开始使用后,前面一些帧的图像不够稳定,我们可以屏
蔽掉前 60 帧图像,程序如下所示。
![](https://i-blog.csdnimg.cn/blog_migrate/adac2d0e0a2c32c4a1db37b132180f8d.png)
这里使用了一个变量 count 进行计数。+= 被称为自增运算符,count += 1 实际上等
同于 count = count + 1,即每次循环 count 的值增加 1。同理还有自减(-=)、自乘(*=)、
自除(/=)等运算符。
关键词 continue 与 break 有相似之处。在 while 或 for 语句的循环中,break 表示直
接跳出循环,而 continue 表示跳过本次循环后面的语句,直接继续下一次循环。
第二个问题是调整速度太慢,这可以通过增加每次 pwm1 的自增值、自减值来改善,
程序如下所示:
造成第三个问题的原因在于停止的条件过于苛刻,我们应该在 320 附近设定一个目标
区域,使得物体位于这个区域内即停止,程序如下所示。
![](https://i-blog.csdnimg.cn/blog_migrate/ba8f6f586a6a4e879abdf5c6491f3bdb.png)
控制机械臂等高运动,使物体y 坐标为 150
对于 3 号舵机控制的
β
角而言,其角度越小,机械臂就越高;其角
度越大,机械臂就越低;对 2 号舵机控制的
ɑ
角而言,其角度为 90°时机械臂最高;其
角度与 90°差别越大,机械臂越低。因此,为了维持机械臂等高运动,必须同时调整两个
舵机角度的变化。
要编程控制机械臂在头部高度不变的情况下运动是一件比较复杂的事情。但在此前导入
过的 roboticarm 的 Python 包中,有一个预置函数 get_angle 可以在设定机械臂前端的高
度与到底端的水平距离的情况下,返回两个舵机对应的 PWM 值。用这个函数可以较为方
便地达到预期效果。我们尝试编写并运行以下程序,它的效果是控制机械臂调整到初始位置
(即轴 1 置中,轴 2、轴 3 均为 90°的状态)。
![](https://i-blog.csdnimg.cn/blog_migrate/94cb92a827ad2ac0c113913b76857f98.png)
![](https://i-blog.csdnimg.cn/blog_migrate/f456e81c41816592332b0cafe0bd3673.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b02a800c29a3c827c8effd9b0a74dbd6.png)
get_angle 函数的格式是:get_angle(matrix, height, distance)。函数将返回一个长
度为 2 的元组,元组的第一位为轴 2 舵机的 PWM 值,第二位为轴 3 舵机的 PWM 值,我
们将返回值分别赋值给 pwm2、pwm3。
在使用 get_angle 函数时需要预先载入记录机械臂状态与位置关系的列表。这个列表
通常可以用 Python 的计算扩展包 numpy 载入文件来获取。在树莓派系统中,我们通过这
样的语句得到列表 matrix:
![](https://i-blog.csdnimg.cn/blog_migrate/a281459b9805cc03ce205e956f933bf9.png)
而 height 为前端点与底端轴的相对高度,distance 为相对水平距离(见图 6.25)。
在 2 号和 3 号舵机均为 90°的初始状态下,get_angle 函数中 height 与 distance 的值均
为 1000。注意,这两个值均为整数,最大范围为 0~2000 之间。当超出此范围时,函数将
返回 (-1, -1)
![](https://i-blog.csdnimg.cn/blog_migrate/e73cddf4e21416c53dd0637d25c3bdbc.png)
现在我们需要使机械臂在 1000 的高度上前后运动来使物体中心
y
坐标位于 150 左右。
如果测得的
y
坐标太小,应向前(增大 distance);反之应向后(减小 distance)。仿照
此前左右调整的方法,完成以下程序:
![](https://i-blog.csdnimg.cn/blog_migrate/f9f378b1095d4fa0265d29a0e7f9623e.png)
至此,我们已经可以写出控制机械臂运动,使吸盘位于物体正上方的完整程序了。其中
一些参数要根据实际情况调整,具体调参请自行尝试。
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() # 关闭串口连接
抓取和放置物体
我们在之前的活动里已经让机械臂前端的吸盘到达了物体的正上方,但是在吸取物体之
前,我们还应该让吸盘竖直向下运动到物体表面。我们可以用一个循环程序来完成此过程,
此时应该保持 distance 不变,而将 height 减小,程序如下所示。
回顾一下,for 循环可以将一个参数在一个数值范围内进行遍历,其格式为:
![](https://i-blog.csdnimg.cn/blog_migrate/7f3c4f4c53a3d7cfc00594052165303b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b6a1ab72a9cde4f215fdbc3fc95dd45d.png)
参数将从起点值开始,每次循环递增步长的值(步长为负值时实际为递减),直到终点
值为止(但终点值本身不参与循环)。
注意,这里的 79 仅为参考值,请以吸盘可以贴紧物体为目标调整这个取值。
![](https://i-blog.csdnimg.cn/blog_migrate/a1418fc66175dcc18923d55203b9c673.png)
将气泵所连的 4 号引脚的 PWM 输出值设为 2500 为打开气泵,设为 500 为关闭气泵,
同样,将电子阀门所连的 5 号引脚的 PWM 输出值设为 2500 为打开气泵,设为 500 为关
闭气泵。调整到位后,打开电子阀门来松开物体。注意电子阀门不要长时间打开,打开一段
时间后务必关闭。
![](https://i-blog.csdnimg.cn/blog_migrate/c058538f7b066f6cb9e609795770b92c.png)
人脸识别
随着算法的进一步发展,到 21 世纪初,人工智能系统可以相对轻量、快速地识别人脸,
进而推动了这项技术在社会上的实际应用。其早期的典型应用是在智能相机中,利用人脸检
测实现人像模式辅助自动对焦,而近年来随着特定人脸识别精度与识别速度的提升,面部解
锁成为了智能手机的一种新的解锁方式。
我们人类要识别一张人脸,首先要从眼前的视觉图像中找到人脸,然后分析脸上的特征
(如眼睛、鼻子、嘴巴的形状等),最后与大脑中已知的人脸特征进行比较,最终锁定面前
的这个人是谁
![](https://i-blog.csdnimg.cn/blog_migrate/e8b212427f97e47202feb335474d4acc.png)
这个过程归根结底是一种生物本能,我们几乎很难意识到识别过程中的每一个步骤的存
在。但要让人工智能模仿人类对人脸进行识别,必须借助机器视觉技术对每一步进行模拟。
与人类相似,人工智能也需要在图像信息中找到人脸的位置,再分析出其中的特征,最终与
数据库中存储的已知人脸特征进行比较,确定这个人是谁
![](https://i-blog.csdnimg.cn/blog_migrate/67463a7099e5d04f6829862e3c0f8df1.png)
使用肤色检测找到人脸
在检测人脸的多种方法中,最直接、简单的方法是通过肤色识别确定人脸的位置,这种
方法被称为肤色检测。在此前的课程中,我们学习了使用 OpenCV 工具从摄像头图像中识
别出特定颜色的物体。虽然人脸并不是颜色均匀的物体,但我们也可以通过识别皮肤颜色来
大致识别人脸的位置
![](https://i-blog.csdnimg.cn/blog_migrate/a7480c1783f8cd2e9cb650fa78128a20.png)
不同人种的肤色值通常有一定区别,东亚人脸的颜色大致处于红色至橙色之间,其 H(色
调)值的参考范围为 0~17,S(饱和度)值的参考范围为 43~255,而 V(明亮度)值的
参考范围为 46~255。
与颜色识别程序类似,我们可以设定需要识别的 HSV 颜色区间,并生成二值化黑白图
像进行预览。
![](https://i-blog.csdnimg.cn/blog_migrate/2f5ec1d1f78e84aa3008f4068a2d2324.png)
![](https://i-blog.csdnimg.cn/blog_migrate/118e92abf1e176df2f4da932502b9849.png)
理想状态下,这一程序应可以成功区分摄像头图像中的人脸与非人脸区域。如果实际效果偏差较大,可以尝试调整皮肤颜色的 H 值区间。
![](https://i-blog.csdnimg.cn/blog_migrate/d28d507c9fa977683e737e22c8662fb6.png)
在检测到肤色区域的基础上,我们需要对其所处区域进行定位。使用下面的程序方法可
以绘制出肤色区域的最小接近圆轮廓。
程序将自动找到图像中的人脸,并使用红色圆标注出来
![](https://i-blog.csdnimg.cn/blog_migrate/364eb6c2bcd7de231c13200427329894.png)
![](https://i-blog.csdnimg.cn/blog_migrate/986ac0b31fca512ba549e613d9a6ef3d.png)
尽管通过肤色检测可以在一定程度上找到图像中人脸的位置,但有一些明显的问题。
(1)不同人的皮肤颜色有较为显著的区别,过于严格的颜色范围将漏掉一些人,而过
于宽松的颜色范围很容易将环境中的其他东西误认为人脸。
(2)肤色检测并不区分面部与其他位置的皮肤,使用肤色检测不单可以找到人脸,同
样也可以找到手部等其他裸露的人体皮肤。
因此,肤色检测通常只用于特定场景下的人脸检测或作为其他检测手段的辅助。为了更
准确地检测出图像中的人脸,我们需要使用更复杂的数学方法。
使用哈尔特征检测找到人脸
事实上,除了皮肤的颜色外,人脸还具备其他的一些图像特征。通常来说,由于有黑色
的眼珠与眉毛的存在,眼睛区域的总体颜色比脸颊颜色更暗,鼻梁两侧比中央颜色更暗,而
嘴巴也比周围颜色更暗。我们可以根据亮暗程度来表明这些区域的特征,为方便起见,我们
可以先将彩色图片转换为黑白(灰度)图片
![](https://i-blog.csdnimg.cn/blog_migrate/9740d5a6c2ad0ebb69e98d49d1cc318c.png)
我们以眼睛区域与脸颊区域的对比为例,可以将这两种区域分别用矩形框框出,再计算
出这些区域内的平均灰度值。灰度值表示图像的亮暗程度,灰度值越大越接近白色,即原始
颜色越亮。
可以明显地看出,图 7.7 中右侧的脸颊位置整体比眼窝位置更白,即平均灰度值更大。
要将灰度图像目标区域划定成大量的矩形框并得到它们的关系信息,可以用名为哈尔小
波变换的数学方法来计算,因此这种图像特征常被称为哈尔特征(Haar-like features),
![](https://i-blog.csdnimg.cn/blog_migrate/b6db89afef8ba6dc71beade2e115a6e7.png)
但一个 20 像素 ×20 像素的图片的哈尔特征就能达到 10 万种以上,其中哪些特征符
合人脸的特点很难通过人为设定来找出。通过一种行之有效的训练方法,我们可以将大量面
部图片与非面部图片交给计算机,让其自动找到足以区分人脸与非人脸的图像特征信息。
由于这种检测方法只需要对图像的灰度空间进行分析,运算量大大低于传统用于图像检
测的 RGB 颜色空间或 HSV 颜色空间,因此运算速度得到了大幅提升,首次实现了有效的
实时人脸检测。21 世纪初大量相机厂商通过这一技术首次实现了人脸自动对焦。
2002—2005年,Rainer Lienhart将这种检测方法写入了OpenCV开源库的基础包中,
并提交了训练得到的包括人脸在内的多种物体的特征信息。
因此,使用 OpenCV 内置的检测算法,我们便可以轻松地检测出图像中的人脸。在使
用算法检测人脸之前,需要先用 cvtColor 函数将图像变换为灰度图(使用 cv2.COLOR_BGR2GRAY 参数)。
![](https://i-blog.csdnimg.cn/blog_migrate/2ce0067afc43f26050596e74d00ca956.png)
在检测之前,我们还需要载入人脸的哈尔特征信息。由于这些信息是用于对图像中的物
体进行分类的,它们通常被称为分类器。在OpenCV开源库中有许多针对不同对象的分类器,
这些分类器被存储在格式为 XML 的文件中,使用者可以根据需要下载这些文件。
在我们的树莓派系统已经存储了用于正面人脸识别的分类器,可以通过下面的语句载入
该分类器。
![](https://i-blog.csdnimg.cn/blog_migrate/461fffa5e1dba13f988b75b3f0893aeb.png)
括号中为文件在系统中的存储位置。载入分类器后,我们可以用 detectMultiScale 函
数进行目标物体检测:
detectMultiScale( 灰度图像 , minSize= 最小目标尺寸 , flags= 检测方式 )
这里我们可以设置两个参数的取值。
minSize 参数表示目标的最小尺寸,也就是说此函数会忽略小于该尺寸的人脸。该参
数为一个长度为 2 的元组,分别表示
x
方向和
y
方向的最小尺寸。
flags 设定检测方式,主要可选的取值有以下 3 种。
cv2.CASCADE_DO_CANNY_PRUNING:忽略图像中一些明显不是人脸的区域。
cv2.CASCADE_SCALE_IMAGE:对图像进行缩放的检测方法,可以提高检测速度,
但准确率有所下降。
cv2.CASCADE_FIND_BIGGEST_OBJECTS:只检测最大的目标物体。
detectMultiScale 函数的返回值是由每一个目标物体位置信息构成的元组。位置信息
由左上角x 坐标、左上角 y 坐标、宽度、高度构成
![](https://i-blog.csdnimg.cn/blog_migrate/0ed8b745e22899dd162c2288ded826db.png)
faces = face_cascade.detectMultiScale(gray, minSize=(100, 100),
flags=cv2.CASCADE_SCALE_IMAGE)
使用这一语句,将找到图像中所有尺寸不小于 (100, 100) 的人脸的位置并将它们存
储到变量 faces 中。faces 事实上是一个元组,使用 for 循环可以遍历元组中每一个人脸
的位置信息,从而将人脸的位置标注出来。由于得到的位置信息构成一个矩形,我们使用
rectangle 函数来绘制:
cv2.rectangle( 图像信息 , 左上角坐标 , 右下角坐标 , 框的颜色 , 框的粗细 )
其右下角坐标可以通过左上角坐标与宽、高相加得到,框的颜色按照 BGR(蓝绿红)
的方式排列。因此,我们可以写出完整的检测人脸的程序
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()