-
实训目的
基于opencv实现小车在gazebo环境中的人脸识别任务。首先,学习使用官方训练的分类器进行小车摄像头画面中人脸的检测。其次,自建人脸图像训练集完成人脸识别模型的训练。最后,学习加载训练得到的分类器文件,结合人脸检测任务的输出,实现自定义人脸的识别。
-
实训操作步骤
-
实现基于Haar特征的人脸检测,其中包括:
-
获取opencv官方训练的基于Haar特征的人脸检测器文件,将其保存在自己的工作空间下面。
-
在脚本中调用该检测器完成人脸的检测。
-
-
训练人脸识别模型,其中包括:
-
准备人脸识别训练集。
-
编写脚本,遍历训练集目录,生成训练好的分类器文件及人名列表,用于下一步人脸识别脚本调用。
-
-
加载模型训练结果文件,实现基于EigenFaces的人脸识别。
-
基于Haar特征的人脸检测
首先,我们利用opencv中的CascadeClassifier级联分类器实现人脸检测,并标注出人脸在一帧图像中的位置和大小。
级联分类器是通过不同的特征进行一步步筛选,得出这些特征组合所属的最终分类,它将一个复杂的分类问题(如人脸检测)拆解为一个个简单的分类问题(如眼睛、鼻子、嘴巴等的检测),再通过级联条件的判断得出复杂问题的分类结果,有助于提升分类的速度。
在人脸检测问题中,人脸图像中包含的模式特征十分丰富,如直方图特征、颜色特征、模板特征、结构特征及Haar特征等。其中,Haar特征是将像素划分为很多矩形模块,求相邻模块之间的差值以反映该处图像的灰度变化,以此特征作为图像的分类依据。例如:眼睛比脸颊颜色要深,鼻梁两侧比鼻梁颜色要深,嘴巴比周围颜色要深等。满足以上所有灰度变化特征,则判断该图像极有可能是一张人脸。
这里我们使用基于Haar特征的CascadeClassifier级联分类器实现人脸检测。
在工作空间src目录下。打开终端输入以下代码创建人脸检测功能包:
$ catkin_create_pkg proj04_face_detector std_msgs rospy sensor_msgs cv_bridge
在功能包下,建立/cascades目录,用于存放opencv官方基于Haar特征的人脸检测级联分类器文件。文件的获取可访问opencv在github上的官方仓库,所在的目录地址如下:https://github.com/opencv/opencv/tree/master/data/haarcascades,其中包含了十余个不同部位的分类器文件,可以根据需要选择合适的文件,如下图:
图1 opencv官方级联分类器
这里我们将haarcascade_frontalface_default.xml与haarcascade_profileface.xml拷贝至/cascades,其中前者为正面人脸检测文件,后者为侧面人脸检测文件。
建立/scripts目录,在其中添加基类脚本ros_opencv.py。我们的人脸检测脚本与上一节蓝色圆形识别脚本相同,都需要基于这个基类,通过重写图像处理方法process_image()的方式实现功能。代码如下:
ros_opencv.py:
-
#!/usr/bin/env python # coding:utf-8 import rospy import cv2 from cv_bridge import CvBridge,CvBridgeError from sensor_msgs.msg import Image from geometry_msgs.msg import Twist import numpy as np import time import os, sys class ROS2OPENCV(object): # 基类的初始化方法,实例化时自动调用 def __init__(self,node_name): self.node_name = node_name os.chdir(sys.path[0]) rospy.init_node(node_name) rospy.on_shutdown(self.shutdown) ### 定义ros图像消息的订阅者和处理后图像的发布者 # 发布/display_image话题,类型为Image,队列大小设置为1 self.rgb_image_pub = rospy.Publisher("/display_image",Image,queue_size=1) # 订阅/camera/rgb/image_raw图像话题,回调函数rgb_image_callback(self, data) # 该回调函数实现了图像处理、帧率输出 self.rgb_topic = rospy.get_param("~rgb_image_topic", "/camera/rgb/image_raw") self.rgb_image_sub = rospy.Subscriber(self.rgb_topic,Image,self.rgb_image_callback) # 订阅深度图像 self.depth_topic = rospy.get_param("~depth_image_topic", "/camera/depth/image_raw") self.depth_image_sub = rospy.Subscriber(self.depth_topic,Image,self.depth_image_callback) self.move = rospy.get_param("~if_move", False) # 定义机器人速度消息的发布者,用于机器人运动驱动 self.cmd_pub = rospy.Publisher('/cmd_vel', Twist, queue_size=1) self.bridge = CvBridge() self.frame = None self.frame_width = None self.frame_height = None self.frame_size = None self.track_box = None self.detect_box = None self.display_box = None self.marker_image = None self.processed_image = None self.display_image = None self.target_center_x = None self.cascade_frontal = None self.cascade_profile = None self.cps = 0 self.cps_values = list() self.cps_n_values = 20 self.linear_speed = 0.0 self.angular_speed = 0.0 def rgb_image_callback(self, data): start = time.time() frame = self.convert_image(data) if self.frame is None: self.frame = frame.copy() self.marker_image = np.zeros_like(frame) self.frame_size = (frame.shape[1], frame.shape[0]) self.frame_width, self.frame_height = self.frame_size else: self.frame = frame.copy() self.marker_image = np.zeros_like(frame) # 调用图像处理方法,获取处理后图像 processed_image = self.process_image(frame) self.processed_image = processed_image.copy() self.display_image = cv2.bitwise_or(self.processed_image, self.marker_image) # 绘制跟踪框 if self.track_box is not None and self.is_rect_nonzero(self.track_box): tx, ty, tw, th = self.track_box cv2.rectangle(self.display_image,(tx,ty),(tx+tw,ty+th),(255,255,0),2) ### 计算机器人移动速度 # 计算跟踪目标中心x坐标 self.target_center_x = int(tx+tw/2) # 计算目标中心与视野中心x坐标的偏移量 offset_x = (self.target_center_x - self.frame_width/2) # 计算目标区域的面积 target_area = tw*th # 若允许机器人跟踪目标 if self.move: # 目标距离较远时速度大 if target_area < 25000: linear_vel = 0.3 # 到达目标时停止前进 elif target_area > 45000: linear_vel = 0.0 # 接近目标时速度减慢 else: linear_vel = 0.1 # 根据偏移量计算角速度 angular_vel = float(-0.01 * (offset_x)) # 设置角速度极大极小值 if angular_vel > 0.1: angular_vel = 0.1 if angular_vel < -0.1: angular_vel = -0.1 # 调用update_cmd方法发布速度消息 self.update_cmd(linear_vel,angular_vel) # 绘制检测框 elif self.detect_box is not None and self.is_rect_nonzero(self.detect_box): dx, dy, dw, dh = self.detect_box cv2.rectangle(self.display_image, (dx, dy), (dx+dw, dy+dh), (255, 50, 50), 2) ### 计算时间与帧率,并打印至图像 # self.cps_values包含若干条最新fps数据,最多为20条,对其求和取平均得到cps值 end = time.time() duration = end - start fps = int(1.0/duration) self.cps_values.append(fps) if len(self.cps_values)>self.cps_n_values: self.cps_values.pop(0) self.cps = int(sum(self.cps_values)/len(self.cps_values)) # 根据不同的窗体大小设置字体位置 font_face = cv2.FONT_HERSHEY_SIMPLEX font_scale = 0.5 if self.frame_size[0] >= 640: vstart = 25 voffset = int(50+self.frame_size[1]/120.) elif self.frame_size[0] == 320: vstart = 15 voffset = int(35+self.frame_size[1]/120.) else: vstart = 10 voffset = int(20 + self.frame_size[1] / 120.) # cv2.putText函数实现在画面上输出文字信息 cv2.putText(self.display_image, "CPS: " + str(self.cps), (10, vstart),font_face, font_scale,(255, 255, 0)) cv2.putText(self.display_image, "RES: " + str(self.frame_size[0]) + "X"+ str(self.frame_size[1]), (10, voffset), font_face, font_scale, (255, 255, 0)) # 发布处理后的图像 try: self.rgb_image_pub.publish(self.bridge.cv2_to_imgmsg(self.display_image,"bgr8")) except CvBridgeError as e: print(e) # 深度图像回调函数 def depth_image_callback(self,data): depth_frame = self.convert_depth_image(data) # 转换ros图像为cv2图像 def convert_image(self,ros_image): try: cv_image = self.bridge.imgmsg_to_cv2(ros_image,"bgr8") cv_image = np.array(cv_image,dtype= np.uint8) return cv_image except CvBridgeError as e: print (e) # 转换ros深度图为cv2图像 def convert_depth_image(self,ros_image): try: depth_image = self.bridge.imgmsg_to_cv2(ros_image,"passthrough") depth_image =np.array(depth_image,dtype=np.float32) return depth_image except CvBridgeError as e: print (e) # 图像处理方法 def process_image(self,frame): return frame # 深度图处理方法 def process_depth_image(self,frame): return frame # 判断矩形框是否存在 def is_rect_nonzero(self,rect): try: (_,_,w,h) = rect return ((w>0)and(h>0)) except : try: ((_,_),(w,h),a) = rect return (w > 0)and (h > 0) except: return False # shut down def shutdown(self): rospy.loginfo("Shutting down node") # 构造速度消息并发布 def update_cmd(self, linear_speed, angular_speed): self.linear_speed = linear_speed self.angular_speed = angular_speed move_cmd = Twist() move_cmd.linear.x = linear_speed move_cmd.angular.z = angular_speed self.cmd_pub.publish(move_cmd) if __name__ == "__main__": try: ROS2OPENCV("ros_opencv") rospy.loginfo("Please subscribe the ROS image.") rospy.spin() except KeyboardInterrupt: print ("Shutting down cv_bridge_test node")
在/scripts目录下新建一个python脚本face_detector.py,代码如下。
face_detector.py:
-
#!/usr/bin/env python # -*- coding: utf-8 -*- import rospy import cv2 from ros_opencv import ROS2OPENCV import os import sys class FaceDetector(ROS2OPENCV): def __init__(self, node_name): super(FaceDetector, self).__init__(node_name) self.detect_box = None self.result = None self.count = 0 # 设置级联表的参数,优化人脸识别 self.haar_scaleFactor = 1.2 self.haar_minNeighbors = 3 self.haar_minSize = 40 self.haar_maxSize = 60 # 获取haar特征的级联表的XML文件, 其中存放的是训练好的分类器 cascade_frontal = "../cascades/haarcascade_frontalface_default.xml" cascade_profile = "../cascades/haarcascade_profileface.xml" # 使用级联表初始化haar特征检测器 self.cascade_frontal = cv2.CascadeClassifier(cascade_frontal) self.cascade_profile = cv2.CascadeClassifier(cascade_profile) def process_image(self, frame): src = frame.copy() # 创建灰度图像 gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) # 对图像进行直方图均衡化操作,减少光线影响,增加对比度 gray = cv2.equalizeHist(gray) # 调用人脸检测方法,获取人脸检测结果 faces = self.detect_face(gray) result = src.copy() self.result = result # 绘制人脸位置 # (x,y,w,h)分别代表人脸框的左上角点的坐标,人脸框的宽度和长度 for (x, y, w, h) in faces: result = cv2.rectangle(result, (x, y), (x + w, y + h), (255, 0, 0), 2) return result def detect_face(self, input_image): faces = [] # 首先匹配正面人脸的模型 if self.cascade_frontal: # detectMultiScale为人脸检测的核心函数 faces = self.cascade_frontal.detectMultiScale(input_image, self.haar_scaleFactor , self.haar_minNeighbors,cv2.CASCADE_SCALE_IMAGE,(self.haar_minSize,self.haar_maxSize)) # 如果正面人脸匹配失败,那么就尝试匹配侧面人脸的模型 if len(faces) == 0 and self.cascade_profile: faces = self.cascade_profile.detectMultiScale(input_image, self.haar_scaleFactor , self.haar_minNeighbors,cv2.CASCADE_SCALE_IMAGE,(self.haar_minSize,self.haar_maxSize)) return faces if __name__ == '__main__': try: node_name = "face_detector" FaceDetector(node_name) rospy.loginfo("Please subscribe the ROS image.") rospy.spin() except KeyboardInterrupt: print("shutting down face detector node.")
以上代码生成了一个继承类FaceDetector,继承了ROS 与 Opencv 接口类ROS2OPENCV。该脚本通过如下代码引入了同目录下的基类。
from ros_opencv import ROS2OPENCV
FaceDetector中包含了如下方法:
-
类的构造方法,用于类的初始化。
-
def __init__(self, node_name): super(FaceDetector, self).__init__(node_name) self.detect_box = None self.result = None self.count = 0 # 设置级联表的参数,优化人脸识别 self.haar_scaleFactor = 1.2 self.haar_minNeighbors = 3 self.haar_minSize = 40 self.haar_maxSize = 60 # 获取haar特征的级联表的XML文件, 其中存放的是训练好的分类器 cascade_frontal = "../cascades/haarcascade_frontalface_default.xml" cascade_profile = "../cascades/haarcascade_profileface.xml" # 使用级联表初始化haar特征检测器 self.cascade_frontal = cv2.CascadeClassifier(cascade_frontal) self.cascade_profile = cv2.CascadeClassifier(cascade_profile)
-
重写了父类的process_image 方法,用于图像处理。
process_image方法中,程序在进行了基本的图像处理操作后,调用了人脸检测方法detect_face,获取人脸检测结果,并将结果绘制到图像上。
-
def process_image(self, frame): src = frame.copy() # 创建灰度图像 gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) # 对图像进行直方图均衡化操作,减少光线影响,增加对比度 gray = cv2.equalizeHist(gray) # 调用人脸检测方法,获取人脸检测结果 faces = self.detect_face(gray) result = src.copy() self.result = result # 绘制人脸位置 # (x,y,w,h)分别代表人脸框的左上角点的坐标,人脸框的宽度和长度 for (x, y, w, h) in faces: result = cv2.rectangle(result, (x, y), (x + w, y + h), (255, 0, 0), 2) return result
-
detect_face方法,用于人脸检测的具体实现。
detect_face方法中,实现人脸识别的核心函数为detectMultiScale,这个函数将一帧图像中的全部人脸提取出来,将这些人脸在当前帧图像中的位置组成的列表放入返回值faces中。其元素为(x,y,w,h),分别代表人脸框的左上角点的x、y坐标,人脸框的宽度w和长度h。
-
def detect_face(self, input_image): faces = [] # 首先匹配正面人脸的模型 if self.cascade_frontal: # detectMultiScale为人脸检测的核心函数 faces = self.cascade_frontal.detectMultiScale(input_image, self.haar_scaleFactor , self.haar_minNeighbors,cv2.CASCADE_SCALE_IMAGE,(self.haar_minSize,self.haar_maxSize)) # 如果正面人脸匹配失败,那么就尝试匹配侧面人脸的模型 if len(faces) == 0 and self.cascade_profile: faces = self.cascade_profile.detectMultiScale(input_image, self.haar_scaleFactor , self.haar_minNeighbors,cv2.CASCADE_SCALE_IMAGE,(self.haar_minSize,self.haar_maxSize)) return faces
程序运行流程:
-
程序入口:主函数,实例化扩展类 FaceDetector。
-
实例化时执行类的构造方法,其中包含了超类的构造方法。执行完毕后返回主函数。
-
主函数中通过rospy.spin()不断触发所有回调函数,当回调函数对应的消息队列中存在消息时,进入回调函数。
-
由于继承了父类的rgb_image_callback回调函数,因此进入该回调函数,并在其中调用 FaceDetector类重写的图像处理方法process_image。
-
在图像处理方法中调用detect_face方法,在其中调用opencv提供的detectMultiScale函数,得到返回值后返回图像处理方法继续执行。
-
图像处理方法执行完毕,返回回调函数继续执行。
接下来我们运行这个节点,在终端中输入如下命令启动人脸识别环境:
$ export TURTLEBOT3_MODEL=waffle_pi
$ source devel/setup.bash
$ roslaunch billboard tb3_house_with_face_boards.launch
环境如图所示:
图2 人脸识别环境
给脚本添加可执行权限,并将脚本添加到CmakeList中后,另起终端二,输入如下命令,启动人脸检测节点:
$ source devel/setup.bash
$ rosrun proj04_face_detector face_detector.py
另起终端三,输入如下命令打开RViz,并点击add订阅/display_image。
$ rosrun rviz rviz
另起终端四,输入如下命令打开键盘控制节点,并控制小车朝向人脸标示牌。
$ rosrun teleop_twist_keyboard teleop_twist_keyboard.py
得到以下结果:
图3 人脸检测结果
-
训练人脸识别模型
在得到人脸检测结果后,要进一步得到人脸所对应的姓名标签,就要选用合适的人脸识别模型,使用数据集对其进行训练,得到训练完成的分类器文件,在后续工作中加载此文件以识别人脸,进行分类。在这里,我们选用EigenFaces方法来进行人脸识别。
EigenFaces通常也被称为特征脸,它使用主成分分析(Principal Component Analysis,PCA)方法将高维的人脸数据处理为低维数据后,在进行数据分析和处理,获取识别结果。
我们使用opencv提供的函数cv2.face.createEigenFaceRecognizer()生成特征脸识别器,然后应用该识别器的train()函数完成训练。在此之前,我们需要进行相应的数据准备。
2.1 准备人脸识别模型训练集
在功能包的/cascades目录下建立/data文件夹,在/data中按人名建立子目录,其中保存多张不同角度的脸部图片。需要注意的是,图片的大小应尽量维持在200*200像素以内,以确保程序运算的速度不会受太大影响。
2.2 编写模型训练脚本
接下来,在/scripts文件夹下建立face_trainer.py文件,用于读取/data目录下的训练集,并生成训练好的分类器文件recognize_face.xml与姓名列表文件name_list.txt。脚本内容如下。
face_trainer.py:
-
#!/usr/bin/env python # -*- coding: utf-8 -*- import rospy import os import sys import cv2 import numpy as np from ros_opencv import ROS2OPENCV class FaceTrainer(ROS2OPENCV): def __init__(self, node_name): super(FaceTrainer, self).__init__(node_name) self.images = None self.labels = None # 指定获取训练集的目录 self.read_dir = "../cascades" # 调用read_images方法获取训练集图像及标签列表 [images, labels] = self.read_images(self.read_dir) self.images = images self.labels = labels # 初始化Eigen人脸识别模型 self.model = cv2.face.createEigenFaceRecognizer() # 将图像与标签列表转换为数组,输入模型中进行训练 self.model.train(np.asarray(images), np.asarray(labels)) # 将训练得到的分类器文件保存至指定目录 self.model.save(self.read_dir+"/recognize_face.xml") print ("Train Succeed !") # 读取训练集图像文件,构建图像及标签列表 def read_images(self, path, sz=(100,100)): images, labels = [], [] names = [] ids = 0 # 在入参目录后附加data目录 path = os.path.join(path,"data") # 遍历data目录 for dirname, dirnames, filenames, in os.walk(path): # 遍历data的子目录 for subdirname in dirnames: subject_path = os.path.join(dirname, subdirname) # 遍历子目录中的文件 for filename in os.listdir(subject_path): try: # 跳过其中包含的文件夹 if filename == ".directory": continue # 构造文件路径,将图像读取为灰度图 filepath = os.path.join(subject_path, filename) im = cv2.imread(filepath, cv2.IMREAD_GRAYSCALE) # 处理空图像 if im is None: print("image" + filepath + "is None") # 若指定了图像尺寸,重设读入图像的尺寸 if sz is not None: im = cv2.resize(im, sz) # 将读取到的图像转换为数组,追加到图像列表尾 images.append(np.asarray(im, dtype=np.uint8)) # 将标签追加到标签列表尾 labels.append(ids) except: print("unexpected error") raise # 遍历完一个子目录的文件后标签自增,用于标识人脸属于同一个人 ids = ids + 1 # 将子目录名追加到names列表 names.append(subdirname) # 遍历data目录结束后,将names列表转换为字符串,输出到txt文件中,保存在data目录下 txtpath = os.path.join(path, 'name_list.txt') f=open(txtpath,'w+') res=f.write(' '.join(names)) return [images, labels] if __name__ == "__main__": try: node_name = "face_trainer" FaceTrainer(node_name) rospy.spin() except KeyboardInterrupt: print ("Shutting down face trainer node.")
以上代码同样实现了一个继承类FaceDetector,其中包含了类的构造方法和read_images方法。
-
类的构造方法
在构造方法中,调用了read_images方法,获取了其构建的训练集图片及标签列表,并送入Eigen人脸识别模型中进行训练,后将训练结果文件保存至/ cascades目录下。
-
def __init__(self, node_name): super(FaceTrainer, self).__init__(node_name) self.images = None self.labels = None # 指定获取训练集的目录 self.read_dir = "../cascades" # 调用read_images方法获取训练集图像及标签列表 [images, labels] = self.read_images(self.read_dir) self.images = images self.labels = labels # 初始化Eigen人脸识别模型 self.model = cv2.face.createEigenFaceRecognizer() # 将图像与标签列表转换为数组,输入模型中进行训练 self.model.train(np.asarray(images), np.asarray(labels)) # 将训练得到的分类器文件保存至指定目录 self.model.save(self.read_dir+"/recognize_face.xml") print ("Train Succeed !")
-
read_images方法
read_images方法则通过遍历/data目录,构建图像列表,同时构建标签列表来标识属于同一个人的人脸图像,并将图像列表和标签列表一同作为返回值。此外,该方法将对应于序号的人名(即/data的子目录名)保存到一个txt文件中,存放至data目录下。
-
def read_images(self, path, sz=(100,100)): images, labels = [], [] names = [] ids = 0 # 在入参目录后附加data目录 path = os.path.join(path,"data") # 遍历data目录 for dirname, dirnames, filenames, in os.walk(path): # 遍历data的子目录 for subdirname in dirnames: subject_path = os.path.join(dirname, subdirname) # 遍历子目录中的文件 for filename in os.listdir(subject_path): try: # 跳过其中包含的文件夹 if filename == ".directory": continue # 构造文件路径,将图像读取为灰度图 filepath = os.path.join(subject_path, filename) im = cv2.imread(filepath, cv2.IMREAD_GRAYSCALE) # 处理空图像 if im is None: print("image" + filepath + "is None") # 若指定了图像尺寸,重设读入图像的尺寸 if sz is not None: im = cv2.resize(im, sz) # 将读取到的图像转换为数组,追加到图像列表尾 images.append(np.asarray(im, dtype=np.uint8)) # 将标签追加到标签列表尾 labels.append(ids) except: print("unexpected error") raise # 遍历完一个子目录的文件后标签自增,用于标识人脸属于同一个人 ids = ids + 1 # 将子目录名追加到names列表 names.append(subdirname) # 遍历data目录结束后,将names列表转换为字符串,输出到txt文件中,保存在data目录下 txtpath = os.path.join(path, 'name_list.txt') f=open(txtpath,'w+') res=f.write(' '.join(names)) return [images, labels]
该程序的运行流程如下:
-
程序入口:主函数,在主函数中实例化FaceTrainer类。
-
进入类的构造方法,调用read_images方法。
-
进入read_images方法,执行完毕返回训练集数据。
-
继续执行构造方法,初始化模型并进行模型训练,保存训练结果。
2.3 执行模型训练脚本
给脚本添加可执行权限,并将脚本添加到CmakeList中后,在开启了gazebo的情况下,在终端中输入如下命令启动训练节点。
$ source devel/setup.bash
$ rosrun proj04_face_detector face_trainer.py
如未开启gazebo,首先执行roscore命令启动ros节点服务器,并在训练完成后按下ctrl+c关闭该节点服务器。
等待终端返回如下信息:
检查是否生成了xml文件:
检查是否生成了人名文件:
至此,模型训练完成。
-
基于EigenFaces的人脸识别
得到基于EigenFaces的模型训练结果后,我们需要编写程序,加载上一节保存的分类器文件recognize_face.xml与姓名列表文件name_list.txt,用于提供给EigenFaces分类器判别摄像头中的人脸究竟属于谁。
我们同样使用cv2.face.createEigenFaceRecognizer()生成特征脸识别器,在加载第二节训练得到的分类器文件后,即可应用该识别器的predict()函数完成人脸分类。
EigenFaces人脸识别要求训练的图像与测试的图像尺寸必须一致,因此我们需要在代码中通过resize函数进行相应的处理。
人脸识别的返回值包含了当前人脸图像的标签以及分类置信度。EigenFaces人脸识别算法的置信度数值在0到20000之间,置信度数值越低,则代表分类结果越可靠。当置信度数值低于5000时,可以认为是可靠的结果。
在/scripts文件夹下,创建face_ recognizer.py文件,文件内容如下。
face_recognizer.py:
-
#!/usr/bin/env python # -*- coding: utf-8 -*- import rospy import os import sys import cv2 import numpy as np from ros_opencv import ROS2OPENCV class FaceRecognizer(ROS2OPENCV): def __init__(self, node_name): super(FaceRecognizer, self).__init__(node_name) self.detect_box = None self.result = None self.read_dir = "../cascades" # 调用get_labels方法获取人脸图像所对应的标签列表 self.labels = self.get_labels("../cascades/data/name_list.txt") # 设置级联表的参数,优化人脸检测 self.haar_scaleFactor = 1.2 self.haar_minNeighbors = 3 self.haar_minSize = 40 self.haar_maxSize = 60 # 获取haar特征的级联表的XML文件,文件路径在launch文件中传入 cascade_frontal = "../cascades/haarcascade_frontalface_default.xml" cascade_profile = "../cascades/haarcascade_profileface.xml" # 使用级联表初始化haar特征检测器 self.cascade_frontal = cv2.CascadeClassifier(cascade_frontal) self.cascade_profile = cv2.CascadeClassifier(cascade_profile) # 初始化Eigen人脸识别器 self.model = cv2.face.createEigenFaceRecognizer() self.model.load(self.read_dir+"/recognize_face.xml") # 图像处理函数 def process_image(self, frame): src = frame.copy() gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) gray = cv2.equalizeHist(gray) faces = self.detect_face(gray) result = src.copy() self.result = result # 遍历人脸列表 for (x, y, w, h) in faces: # 绘制检测框 result = cv2.rectangle(result, (x, y), (x + w, y + h), (255, 0, 0), 2) # 获取目标人脸图像 roi = gray[x:x + w, y:y + h] try: # 重置人脸图像尺寸为100*100,与训练集相匹配 roi = cv2.resize(roi, (100, 100), interpolation=cv2.INTER_LINEAR) # 获取模型对当前人脸的分类结果及置信度 [p_label, p_confidence] = self.model.predict(roi) print("confidence: %s" % p_confidence + " name: %s" % self.labels[p_label]) # 置信度小于5000认为结果可靠,并将标签打印到人脸检测框下方 if p_confidence < 5000: cv2.putText(result, self.labels[p_label], (x, y + h + 15 ), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255)) print("This is %s" % self.labels[p_label] + "!") except: continue return result def detect_face(self, input_image): faces = [] # 首先匹配正面人脸的模型 if self.cascade_frontal: faces = self.cascade_frontal.detectMultiScale(input_image, self.haar_scaleFactor, self.haar_minNeighbors, cv2.CASCADE_SCALE_IMAGE, (self.haar_minSize, self.haar_maxSize)) # 如果正面人脸匹配失败,那么就尝试匹配侧面人脸的模型 if len(faces) == 0 and self.cascade_profile: faces = self.cascade_profile.detectMultiScale(input_image, self.haar_scaleFactor, self.haar_minNeighbors, cv2.CASCADE_SCALE_IMAGE, (self.haar_minSize, self.haar_maxSize)) return faces # 从文件中获取人脸对应的name列表 def get_labels(self,path): f=open(path,'r') return(f.read().split(' ')) if __name__ == "__main__": try: node_name = "face_recognizer" FaceRecognizer(node_name) rospy.loginfo("Please subscribe the ROS image.") rospy.spin() except KeyboardInterrupt: print ("Shutting down face recognizer node.")
代码实现了一个继承类FaceRecognizer,包含了构造方法、process_image方法、detect_face方法与get_labels方法。代码在第一节人脸检测的基础上,增加了对检测到的人脸图像进行识别的功能。
-
类的构造方法
在构造方法中,增加了基于EigenFace的人脸识别模型的初始化,并加载了第二节生成的recognize_face.xml文件。此外,调用get_labels方法获取了训练时得到的与人脸编号所对应的姓名列表。
-
def __init__(self, node_name): super(FaceRecognizer, self).__init__(node_name) self.detect_box = None self.result = None self.read_dir = "../cascades" # 调用get_labels方法获取人脸图像所对应的标签列表 self.labels = self.get_labels("../cascades/data/name_list.txt") # 设置级联表的参数,优化人脸检测 self.haar_scaleFactor = 1.2 self.haar_minNeighbors = 3 self.haar_minSize = 40 self.haar_maxSize = 60 # 获取haar特征的级联表的XML文件,文件路径在launch文件中传入 cascade_frontal = "../cascades/haarcascade_frontalface_default.xml" cascade_profile = "../cascades/haarcascade_profileface.xml" # 使用级联表初始化haar特征检测器 self.cascade_frontal = cv2.CascadeClassifier(cascade_frontal) self.cascade_profile = cv2.CascadeClassifier(cascade_profile) # 初始化Eigen人脸识别器 self.model = cv2.face.createEigenFaceRecognizer() self.model.load(self.read_dir+"/recognize_face.xml")
-
process_image方法
在process_image方法中,将调用detect_face方法得到的图像区域的尺寸修改为100*100,与训练时的图像尺寸保持一致。并使用predict()函数对获取到的人脸进行判别,得到判别结果的标签及置信度。最后判断置信度小于5000时,认为判别结果可靠,将人脸对应的姓名打印至人脸检测框下方。
-
def process_image(self, frame): src = frame.copy() gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) gray = cv2.equalizeHist(gray) faces = self.detect_face(gray) result = src.copy() self.result = result # 遍历人脸列表 for (x, y, w, h) in faces: # 绘制检测框 result = cv2.rectangle(result, (x, y), (x + w, y + h), (255, 0, 0), 2) # 获取目标人脸图像 roi = gray[x:x + w, y:y + h] try: # 重置人脸图像尺寸为100*100,与训练集相匹配 roi = cv2.resize(roi, (100, 100), interpolation=cv2.INTER_LINEAR) # 获取模型对当前人脸的分类结果及置信度 [p_label, p_confidence] = self.model.predict(roi) print("confidence: %s" % p_confidence + " name: %s" % self.labels[p_label]) # 置信度小于5000认为结果可靠,并将标签打印到人脸检测框下方 if p_confidence < 5000: cv2.putText(result, self.labels[p_label], (x, y + h + 15 ), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255)) print("This is %s" % self.labels[p_label] + "!") except: continue return result
-
get_labels方法
get_labels方法是从指定路径下读取txt文件内容,生成names列表并返回。
-
def get_labels(self,path):
-
f=open(path,'r')
-
return(f.read().split(' '))
-
detect_face方法
detect_face方法则与第一节人脸检测程序中的detect_face方法完全一致。
程序运行流程:
-
程序入口:主函数,实例化扩展类 FaceRecognizer。
-
实例化时执行类的构造方法,其中包含了超类的构造方法。执行完毕后返回主函数。
-
主函数中通过rospy.spin()不断触发所有回调函数,当回调函数对应的消息队列中存在消息时,进入回调函数。
-
由于继承了父类的rgb_image_callback回调函数,因此进入该回调函数,并在其中调用 FaceRecognizer类重写的图像处理方法process_image。
-
在图像处理方法中调用detect_face方法,在其中调用opencv提供的detectMultiScale函数,得到返回值后返回图像处理方法继续执行。
-
在得到候选人脸列表后,调用初始化时加载好的人脸识别器,对人脸列表进行遍历,并识别,输出识别结果。图像处理方法执行完毕,返回回调函数继续执行。
给脚本添加可执行权限,并将脚本添加到CmakeList中后,我们在开启人脸识别仿真环境的基础上,在终端中输入如下命令打开人脸识别节点:
$ source devel/setup.bash
$ rosrun proj04_face_detector face_ recognizer.py
同样的,打开RViz与键盘控制节点,并在RViz中订阅/display_image后,控制小车朝向环境中的人脸标识牌,可以看到如下结果:
(a) lisa识别结果 | (b) julian识别结果 |
图4 人脸识别结果
-
实训结果
-
了解了人脸识别任务的各个环节,包括人脸检测、模型训练及人脸识别。
-
学习使用opencv提供的人脸识别方法完成人脸识别任务。