''' 找到贴图图片中的人脸,并对人脸的鼻孔部分进行肉色填充,去除黑色鼻孔。 ''' import os,platform import sys import time import yaml import cv2 import dlib import numpy as np import mediapipe as mp import logging import shutil def rotate_img_fill_bound(image, angle): ''' 仿射变换,旋转图片angle角度,缺失背景白色(0, 0, 0)填充 :param image: 需要旋转的图片; :param angle: 旋转角度; :return: 输出旋转后的图片。 ''' if image is None: return None # 获取图像的尺寸,确定中心 (h, w) = image.shape[:2] (cX, cY) = (w // 2, h // 2) # 获取旋转矩阵(应用角度的负数 顺时针旋转),然后抓取正弦和余弦 (即矩阵的旋转分量) # -angle位置参数为角度参数负值表示顺时针旋转; 1.0位置参数scale是调整尺寸比例(图像缩放参数),建议0.75 M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0) cos = np.abs(M[0, 0]) sin = np.abs(M[0, 1]) # 计算图像的新边界尺寸 nW = int((h * sin) + (w * cos)) nH = int((h * cos) + (w * sin)) # 调整旋转矩阵以考虑平移 M[0, 2] += (nW / 2) - cX M[1, 2] += (nH / 2) - cY # 执行实际旋转并返回图像 # borderValue 缺失背景填充色彩,此处为白色,默认是黑色,可自定义 img_result = cv2.warpAffine(image, M, (nW, nH), borderValue=(0, 0, 0)) return img_result def get_face_hog(image): ''' 从原始图像中获取人脸位置,并记录信息,没有检测到人脸,坐标使用 [0, 0, 0, 0] 替代。 :param image: 需要识别到原始图片; :return: 从原图抠取的人脸图片face_img,及其坐标信息[x1, y1, x2, y2]。 ''' hog_face_detector = dlib.get_frontal_face_detector() # 加载预训练的 HoG 人脸检测器 # results:存在人脸:rectangles[[(x1, y1) (x2, y2)]],不存在人脸:rectangles[] results = hog_face_detector(image, 0) # 对图片进行人脸检测 # print('face_num:', len(results)) if results is None: return 0, 0, 0, 0, 0 for bbox in results: x1 = bbox.left() # 人脸左上角x坐标 y1 = bbox.top() # 人脸左上角y坐标 x2 = bbox.right() # 人脸右下角x坐标 y2 = bbox.bottom() # 人脸右下角y坐标 face_img = image[y1:y2, x1:x2] return face_img, x1, y1, x2, y2 def is_face_detected(image): ''' 判断图片是否检测到人脸 :param image: 被判断的图片 :return: True/False ''' face_detection = mp.solutions.face_detection.FaceDetection(min_detection_confidence=0.5) # 创建FaceDetection对象 image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results = face_detection.process(image_rgb) # 调用process函数来检测人脸 # 判断是否检测到了人脸 if results.detections is None: return False else: return True def cv_show(name, img): ''' 展示图片 :param name: 窗口名 :param img: 展示的图片 ''' cv2.imshow(name, img) cv2.waitKey(0) cv2.destroyAllWindows() def get_distance(p1, p2): ''' 计算两个像素点之间的距离 :param p1: 坐标点1 :param p2: 坐标点2 :return: p1,p2 像素之间的距离 ''' distance = np.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) distance = round(distance, 2) return distance def compute_distance(rect1, rect2): ''' rect1, rect2 是鼻孔矩形列表(nose_list)的元素,元素格式:(x, y, w, h, ...) :param rect1: (x1, y1, w1, h1, ...1) :param rect2: (x2, y2, w2, h2, ...2) :return: 矩形中心点之间的距离 ''' x1, y1, w1, h1 = rect1[:-1] x2, y2, w2, h2 = rect2[:-1] center_x1 = x1 + w1 / 2 center_y1 = y1 + h1 / 2 center_x2 = x2 + w2 / 2 center_y2 = y2 + h2 / 2 distance = get_distance((center_x1, center_y1), (center_x2, center_y2)) return distance def filter_rectangles(rectangles): ''' 鼻孔筛选条件,限制鼻孔位置,去除非鼻孔轮廓的干扰 :param rectangles: 列表,元素格式:(x, y, w, h, ...) :return: 新列表,元素格式:(x, y, w, h, ...) ''' filtered_rectangles = [] for i in range(len(rectangles)): for j in range(i+1, len(rectangles)): rect1 = rectangles[i] rect2 = rectangles[j] distance = compute_distance(rect1, rect2) max_width = max(rect1[2], rect2[2]) x1, y1, w1, h1 = rect1[:-1] x2, y2, w2, h2 = rect2[:-1] if max_width < distance < 3 * max_width and 4 * max(w1, w2) > abs(x1 - x2) > w1 and 3 * max(h1, h2) > abs(y1 - y2) >= 0: filtered_rectangles.append((rect1, rect2)) else: return None return filtered_rectangles def lock_nose(face_img): ''' 锁定人脸图片中的鼻孔位置:通过查找图片轮廓,筛选轮廓,找到鼻孔位置。 :param face_img: 导入人脸图片 :return:处理好的 face_img, 鼻孔的位置信息 nose_list ''' face_h2, face_w2, _ = face_img.shape face_gray = cv2.cvtColor(face_img, cv2.COLOR_BGR2GRAY) retval, thresh = cv2.threshold(face_gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) nose_list = [] for cnt in contours: area = cv2.contourArea(cnt) # 1.面积筛选轮廓 if 200 <= area <= 1600: rect = cv2.boundingRect(cnt) x, y, w, h = rect # 2.宽高比筛选轮廓 if 0.5 < w / h < 4: # 鼻孔大致为椭圆形,所以要判断长宽比 # 3.加一个距离筛选,去除眼睛的轮廓; face_center = (int(face_w2 / 2), int(face_h2 / 2)) cnt_point = (int(x + w / 2), int(y + h / 2)) cnt_distance = get_distance(face_center, cnt_point) if cnt_distance < int(face_h2 / 6): # 距离筛选 # 4.颜色筛选 ———— 鼻孔中心点像素值较暗 pixel_center = face_img[int(y + h / 2), int(x + w / 2)] nose_list.append((x, y, w, h, sum(pixel_center))) # 存储满足上述条件的鼻孔位置信息 # print('初步筛选得到的鼻孔列表nose_list:', nose_list) if len(nose_list) == 2: x1, y1, w1, h1 = nose_list[0][:-1] x2, y2, w2, h2 = nose_list[1][:-1] max_width = max(w1, w2) distance = compute_distance(nose_list[0], nose_list[1]) if max_width < distance < 3 * max_width and 4 * max(w1, w2) > abs(x1 - x2) > w1 and 3 * max(h1, h2) > abs( y1 - y2) >= 0: for x, y, w, h, _ in nose_list: if draw_nose_rectangle: cv2.rectangle(face_img, (x, y), (x + w, y + h), (0, 255, 0), 2) return face_img, nose_list else: # print('两鼻孔未满足筛选条件!') return None elif len(nose_list) > 2: new_nose_list = filter_rectangles(nose_list) # print('鼻孔数大于2,筛选后 new_nose_list:', new_nose_list) if new_nose_list is None: # print('鼻孔不满足筛选条件!') logging.info('鼻孔不满足函数filter_rectangles筛选条件!') return None else: for x, y, w, h, _ in new_nose_list: if draw_nose_rectangle: cv2.rectangle(face_img, (x, y), (x + w, y + h), (0, 255, 0), 2) return face_img, new_nose_list else: # print('未检测到两个鼻孔,跳过...') logging.info('未检测到两个鼻孔,跳过...') for x, y, w, h, _ in nose_list: if draw_nose_rectangle: cv2.rectangle(face_img, (x, y), (x + w, y + h), (0, 255, 0), 2) return None def select_nose_contour(image, fill_pixel, iterations=0): ''' 挑选出鼻孔轮廓位置,如果鼻孔轮廓和鼻孔图片外轮廓连接到一起,需要分隔开。 :param image: 待处理的鼻孔原图 :param fill_pixel: 需要填充鼻孔的像素 :param iterations: 膨胀次数 :return: 处理完的鼻孔图片 ''' max_cnt = "" gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) retval, img_thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 对黑色区域来说是腐蚀,对白色区域来说是膨胀,闭运算 image_erode = cv2.morphologyEx(img_thresh, cv2.MORPH_CLOSE, kernel, iterations=2) # 选取黑色区域中面积最大的(需要除去图片整体的外轮廓) ———— 鼻孔 contours, hierarchy = cv2.findContours(image_erode, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) sum_area = [] for ccc in contours: area1 = cv2.contourArea(ccc) sum_area.append((ccc, area1)) # 存储轮廓数据和该轮廓面积 areas_list = sorted(sum_area, key=lambda x: x[1], reverse=True) # 降序 # 轮廓数目判断,并且轮廓面积必须达到一定面积,暂定大于图片总面积的 1/6 ! if len(areas_list) > 1: if areas_list[1][1] > (areas_list[0][1] / 6): max_cnt = areas_list[1][0] else: return None else: # 注意:鼻孔颜色较浅,鼻孔外围颜色较深,那么两者二值化图像黑色块融合在一起了! # 此时,总轮廓数目为 1 ,鼻孔轮廓和外轮廓连接在一起,解决办法:将鼻孔二值图片外围一圈以3个像素为单位全部填充为白色 height, width = gray.shape # 构造一个新的大一圈的白色矩阵,并在中心部分复制二值图片。 new_img = np.ones((height, width), dtype=np.uint8) * 255 new_img[3:-3, 3:-3] = gray[3:-3, 3:-3] # 使用 rectangle 函数填充矩阵外围为白色 cv2.rectangle(new_img, (0, 0), (width + 5, height + 5), 255, thickness=3) # 再重新获取鼻孔轮廓 new_contours, new_hierarchy = cv2.findContours(new_img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) new_sum_area = [] for ccc2 in new_contours: are1 = cv2.contourArea(ccc2) new_sum_area.append((ccc2, are1)) new_areas_list = sorted(new_sum_area, key=lambda x: x[1], reverse=True) if len(new_areas_list) > 1: # print('图片外轮廓和鼻孔轮廓链接到一起,处理中...') logging.info('图片外轮廓和鼻孔轮廓链接到一起,处理中...') if new_areas_list[1][1] > (new_areas_list[0][1] / 6): max_cnt = new_areas_list[1][0] else: return None hull_cnt = cv2.convexHull(max_cnt) # cv2.drawContours(image, [max_cnt], -1, (0, 255, 0), 1) # 展示 # cv2.polylines(image, [hull], True, (0, 0, 255), 1) # cv2.imshow('image', image) mask = np.zeros_like(img_thresh) cv2.drawContours(mask, [hull_cnt], 0, 255, -1) # 使用白色(255)填充整个轮廓(-1)区域 mask_dilate = cv2.dilate(mask, kernel, iterations=iterations) indices = np.where(mask_dilate == 255) # 替换指定像素值 image[indices[0], indices[1]] = fill_pixel return image def nose_color_fill(face_img, nose_list, pid): ''' 对获取到的左右鼻孔填充像素 :param face_img: 未处理的人脸图片 :param nose_list: 人脸图片左右鼻孔位置信息 :param pid: 图片id,这里主要用于打印处理过程图片 :return: face_img:鼻孔填充颜色后的人脸图片 ''' # 1.获取填充鼻孔的像素 ———— 取左右鼻孔外界矩形中心坐标,鼻尖坐标是两者连线的中间点,以该点像素作为填充像素 pixel_nose_list = [] for x, y, w, h, _ in nose_list: pixel_nose_list.append((int(x + w / 2), int(y + h / 2))) fill_pixel_coor = (int((pixel_nose_list[0][0] + pixel_nose_list[1][0]) / 2), int((pixel_nose_list[0][1] + pixel_nose_list[1][1]) / 2)) # 鼻尖点坐标(x, y) fill_pixel = face_img[fill_pixel_coor[1], fill_pixel_coor[0]] # 鼻尖像素值 # 2.找到需要改变像素的鼻孔区域,扩大截取鼻孔矩形范围,包裹整个鼻孔 # if nose_list[0][0] < nose_list[1][0]: # x1 < x2 # 左鼻孔:nose_list[0] ====》 x1, y1, w1, h1, sum(pixel_center)1;face_img[(y1-2):(y1+h1+2), (x1-2):(x1+w1+2)] # 右鼻孔:nose_list[1] ====》 x2, y2, w2, h2, sum(pixel_center)2;face_img[(y2-2):(y2+h2+2), (x2-2):(x2+w2+2)] nose_list = sorted(nose_list, key=lambda z: z[0]) # 以 x 大小进行升序,x1 < x2 ,x1是左鼻孔,x2是右鼻孔 # 鼻孔尺寸调整外扩矩形框 l_add_w = int(np.ceil(nose_list[0][2] / 10)) # 向上取整 l_add_h = int(np.ceil(nose_list[0][3] / 10)) r_add_w = int(np.ceil(nose_list[1][2] / 10)) r_add_h = int(np.ceil(nose_list[1][3] / 10)) # 图片尺寸调整外扩矩形框 img_h, img_w = face_img.shape[:2] add_img_h = int(np.ceil(img_h / 100)) add_img_w = int(np.ceil(img_w / 100)) left_nose_img = face_img[(nose_list[0][1] - l_add_h - add_img_h):(nose_list[0][1] + nose_list[0][3] + l_add_h + add_img_h), (nose_list[0][0] - l_add_w - add_img_w):(nose_list[0][0] + nose_list[0][2] + l_add_w + add_img_w)] right_nose_img = face_img[(nose_list[1][1] - r_add_h - add_img_h):(nose_list[1][1] + nose_list[1][3] + r_add_h + add_img_h), (nose_list[1][0] - r_add_w - add_img_w):(nose_list[1][0] + nose_list[1][2] + r_add_w + add_img_w)] # select_nose_contour函数里面的外边界轮廓和内边界轮廓分开 left_nose_img = select_nose_contour(left_nose_img, fill_pixel, iterations=0) right_nose_img = select_nose_contour(right_nose_img, fill_pixel, iterations=0) if left_nose_img is None or right_nose_img is None: # 检测到两鼻孔,但是不满足筛选条件的情况 return None # 高斯模糊 left_nose_gauss = cv2.GaussianBlur(left_nose_img, (7, 7), 0) right_nose_gauss = cv2.GaussianBlur(right_nose_img, (7, 7), 0) face_img[(nose_list[0][1] - l_add_h - add_img_h):(nose_list[0][1] + nose_list[0][3] + l_add_h + add_img_h), (nose_list[0][0] - l_add_w - add_img_w):(nose_list[0][0] + nose_list[0][2] + l_add_w + add_img_w)] = left_nose_gauss face_img[(nose_list[1][1] - r_add_h - add_img_h):(nose_list[1][1] + nose_list[1][3] + r_add_h + add_img_h), (nose_list[1][0] - r_add_w - add_img_w):(nose_list[1][0] + nose_list[1][2] + r_add_w + add_img_w)] = right_nose_gauss return face_img def main(pids): ''' 批量处理人脸图片。 :param pids: 待处理图片的id :return: ''' # 设置日志等级为最低(DEBUG),并保存到文件中 logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s', filename=os.path.join(workdir, run_logging)) for item in pids: pid, order_id = item.split('_') start_time = time.time() print('正在处理图片 {} ...'.format(pid)) logging.info('正在处理图片 {} ...'.format(pid)) # 判断文件目录是否存在,不存在则创建 # if not os.path.exists(workdir + f'/{pid}/'): # os.makedirs(workdir + f'/{pid}/') image_path = os.path.join(workdirImgPath, f'{pid}_{order_id}', f'{pid}Tex1.jpg') copy_save_path = os.path.join(workdirImgPath,"copy_texture", f'{pid}_{order_id}', f'{pid}Tex1_old.jpg') #判断是否存在目录,不存在就创建 if not os.path.exists( os.path.join(workdirImgPath,'copy_texture', f'{pid}_{order_id}')): os.makedirs(os.path.join(workdirImgPath,'copy_texture', f'{pid}_{order_id}')) save_path = os.path.join(workdirImgPath, f'{pid}_{order_id}', f'{pid}Tex1.jpg') # 保存修改后图像 shutil.copy(image_path, copy_save_path) # 备份原图,复制操作 image = cv2.imread(image_path) for img_angle in image_angle_list: # save_face_path = os.path.join(workdir, save_face_prefix, f'{pid}_{img_angle}.jpg') img_rotated = rotate_img_fill_bound(image, img_angle) face_hog = get_face_hog(img_rotated) if face_hog is None: print('图片 {0} 旋转角度为{1}时,没有检测到人脸,跳过...'.format(pid, img_angle)) logging.info('图片 {0} 旋转角度为{1}时,没有检测到人脸,跳过...'.format(pid, img_angle)) else: print('图片 {0} 旋转角度为{1}时,检测到人脸,处理中...'.format(pid, img_angle)) logging.info('图片 {0} 旋转角度为{1}时,检测到人脸,处理中...'.format(pid, img_angle)) face_img, x1, y1, x2, y2 = get_face_hog(img_rotated) # 注意:这里会存在错误检测的人脸! face_h, face_w, _ = face_img.shape if face_h >= 300 and face_w >= 300: # 去除face_img 尺寸较小的图片 judge = is_face_detected(face_img) # mediapipe人脸识别筛除非人脸部分 if judge: # print('mediapipe人脸识别成功!') if lock_nose(face_img) is None: print('鼻孔不满足筛选条件!') continue # print('lock_nose检测到的鼻孔满足筛选条件!') draw_nose_face, nose_list = lock_nose(face_img) face_img_result = nose_color_fill(face_img, nose_list, pid) if face_img_result is None: print('旋转角度为{1}时,两个鼻孔可以检测到,但是鼻孔颜色深浅存在问题,处理失败...') continue # 将处理完成的人脸图片还原到原图中去,并将角度还原为初始状态 img_rotated[y1:y2, x1:x2] = face_img_result result_img = rotate_img_fill_bound(img_rotated, -img_angle) # 保存图片的质量是原图的 95% # if not os.path.exists(os.path.join(workdir, save_face_prefix)): # os.makedirs(os.path.join(workdir, save_face_prefix)) # cv2.imwrite(save_face_path, face_img_result, [cv2.IMWRITE_JPEG_QUALITY, 95]) # print('图片 {0} 旋转角度为{1}时,人脸已保存!'.format(pid, img_angle)) # logging.info('图片 {0} 旋转角度为{1}时,人脸已保存!'.format(pid, img_angle)) print('图片 {0} 旋转角度为{1}时,鼻孔处理成功!'.format(pid, img_angle)) logging.info('图片 {0} 旋转角度为{1}时,鼻孔处理成功!'.format(pid, img_angle)) image = result_img # 将旋转后的处理结果保存为新的图片,再次进行旋转处理 continue else: # mediapipe算法识别为非人脸 print('图片 {0} 旋转角度为{1}时,mediapipe算法判断为非人脸,跳过...'.format(pid, img_angle)) logging.info('图片 {0} 旋转角度为{1}时,mediapipe算法判断为非人脸,跳过...'.format(pid, img_angle)) nose_error = open(os.path.join(workdir, mediapipe_judge_fail), 'a') nose_error.write( f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} {pid} 人脸二级筛选失败,跳过...\n') continue else: print('图片 {0} 旋转角度为{1}时,检测到人脸尺寸小于300*300,跳过...'.format(pid, img_angle)) logging.info('图片 {0} 旋转角度为{1}时,检测到人脸尺寸小于300*300,跳过...'.format(pid, img_angle)) nose_error = open(os.path.join(workdir, small_face_error), 'a') nose_error.write( f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} {pid} 检测到人脸尺寸小于300*300,跳过...\n') continue # 保存图片的质量是原图的 95% if not os.path.exists(os.path.join(workdirImgPath, f'{pid}_{order_id}')): os.makedirs(os.path.join(workdirImgPath, f'{pid}_{order_id}')) cv2.imwrite(save_path, image, [cv2.IMWRITE_JPEG_QUALITY, 95]) end_time = time.time() solve_time = start_time - end_time print('图片:{0} 已处理完成并保存,处理时间:{1}!'.format(pid, solve_time)) logging.info('图片:{0} 已处理完成并保存,处理时间:{1}!'.format(pid, solve_time)) if __name__ == '__main__': with open("config/nose.yaml", 'r') as f: Config = yaml.load(f, Loader=yaml.FullLoader) os_type = platform.system() workdir = Config['Nose_Config']['Select_system'][f'{os_type}_path']['workdir'] workdirImgPath = Config['Nose_Config']['Select_system'][f'{os_type}_path']['workdirImgPath'] pids_txt = Config['Nose_Config']['Select_system'][f'{os_type}_path']['pids_txt'] no_detect_nose_error = Config['Nose_Config']['Select_system'][f'{os_type}_path']['no_detect_nose_error'] small_face_error = Config['Nose_Config']['Select_system'][f'{os_type}_path']['small_face_error'] mediapipe_judge_fail = Config['Nose_Config']['Select_system'][f'{os_type}_path']['mediapipe_judge_fail'] run_logging = Config['Nose_Config']['Select_system'][f'{os_type}_path']['run_logging'] image_angle_list = Config['Nose_Config']['Personal_parameter']['image_angle_list'] # 图片旋转角度0、90、180、270 draw_nose_rectangle = Config['Nose_Config']['Personal_parameter']['draw_nose_rectangle'] # 是否需要画出鼻孔的矩形框 if len(sys.argv) == 2: if sys.argv[1] == 'all': # with open('datasets/pids_all.txt', 'r') as f: with open(os.path.join(workdir, pids_txt), 'r') as f: pids = f.read().split(',') else: pids = sys.argv[1].split(',') main(pids) else: print('用法:python nose_processing.py ') sys.exit(0)