建模程序 多个定时程序
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

437 lines
22 KiB

'''
找到贴图图片中的人脸,并对人脸的鼻孔部分进行肉色填充,去除黑色鼻孔。
'''
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(workdir, f'{pid}_{order_id}', f'{pid}Tex1.jpg')
copy_save_path = os.path.join(workdir, f'{pid}_{order_id}', f'{pid}Tex1_old.jpg')
save_path = os.path.join(workdir, 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(workdir, f'{pid}_{order_id}')):
os.makedirs(os.path.join(workdir, 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']
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 <pids>')
sys.exit(0)