#!/usr/bin/env python # -*- coding: utf-8 -*- r""" @DATE: 2024/9/5 20:02 @File: photo_adjuster.py @IDE: pycharm @Description: 证件照调整 """ from .context import Context from .layout_calculator import generate_layout_photo import hivision.creator.utils as U import numpy as np import math import cv2 def adjust_photo(ctx: Context): # Step1. 准备人脸参数 face_rect = ctx.face standard_size = ctx.params.size params = ctx.params x, y = face_rect[0], face_rect[1] w, h = face_rect[2] - x + 1, face_rect[3] - y + 1 height, width = ctx.processing_image.shape[:2] width_height_ratio = standard_size[0] / standard_size[1] # Step2. 计算高级参数 face_center = (x + w / 2, y + h / 2) # 面部中心坐标 face_measure = w * h # 面部面积 crop_measure = ( face_measure / params.head_measure_ratio ) # 裁剪框面积:为面部面积的 5 倍 resize_ratio = crop_measure / (standard_size[0] * standard_size[1]) # 裁剪框缩放率 resize_ratio_single = math.sqrt( resize_ratio ) # 长和宽的缩放率(resize_ratio 的开方) crop_size = ( int(standard_size[0] * resize_ratio_single), int(standard_size[1] * resize_ratio_single), ) # 裁剪框大小 # 裁剪框的定位信息 x1 = int(face_center[0] - crop_size[1] / 2) y1 = int(face_center[1] - crop_size[0] * params.head_height_ratio) y2 = y1 + crop_size[0] x2 = x1 + crop_size[1] # Step3, 裁剪框的调整 cut_image = IDphotos_cut(x1, y1, x2, y2, ctx.processing_image) cut_image = cv2.resize(cut_image, (crop_size[1], crop_size[0])) y_top, y_bottom, x_left, x_right = U.get_box( cut_image.astype(np.uint8), model=2, correction_factor=0 ) # 得到 cut_image 中人像的上下左右距离信息 # Step5. 判定 cut_image 中的人像是否处于合理的位置,若不合理,则处理数据以便之后调整位置 # 检测人像与裁剪框左边或右边是否存在空隙 if x_left > 0 or x_right > 0: status_left_right = 1 cut_value_top = int( ((x_left + x_right) * width_height_ratio) / 2 ) # 减去左右,为了保持比例,上下也要相应减少 cut_value_top else: status_left_right = 0 cut_value_top = 0 """ 检测人头顶与照片的顶部是否在合适的距离内: - status==0: 距离合适,无需移动 - status=1: 距离过大,人像应向上移动 - status=2: 距离过小,人像应向下移动 """ status_top, move_value = U.detect_distance( y_top - cut_value_top, crop_size[0], max=params.head_top_range[0], min=params.head_top_range[1], ) # Step6. 对照片的第二轮裁剪 if status_left_right == 0 and status_top == 0: result_image = cut_image else: result_image = IDphotos_cut( x1 + x_left, y1 + cut_value_top + status_top * move_value, x2 - x_right, y2 - cut_value_top + status_top * move_value, ctx.processing_image, ) # 换装参数准备 relative_x = x - (x1 + x_left) relative_y = y - (y1 + cut_value_top + status_top * move_value) # Step7. 当照片底部存在空隙时,下拉至底部 result_image, y_high = move(result_image.astype(np.uint8)) relative_y = relative_y + y_high # 更新换装参数 # Step8. 标准照与高清照转换 result_image_standard = standard_photo_resize(result_image, standard_size) result_image_hd, resize_ratio_max = resize_image_by_min( result_image, esp=max(600, standard_size[1]) ) # Step9. 参数准备 - 为换装服务 clothing_params = { "relative_x": relative_x * resize_ratio_max, "relative_y": relative_y * resize_ratio_max, "w": w * resize_ratio_max, "h": h * resize_ratio_max, } # Step7. 排版照参数获取 typography_arr, typography_rotate = generate_layout_photo( input_height=standard_size[0], input_width=standard_size[1] ) return ( result_image_hd, result_image_standard, clothing_params, { "arr": typography_arr, "rotate": typography_rotate, }, ) def IDphotos_cut(x1, y1, x2, y2, img): """ 在图片上进行滑动裁剪,输入输出为 输入:一张图片 img,和裁剪框信息 (x1,x2,y1,y2) 输出:裁剪好的图片,然后裁剪框超出了图像范围,那么将用 0 矩阵补位 ------------------------------------ x:裁剪框左上的横坐标 y:裁剪框左上的纵坐标 x2:裁剪框右下的横坐标 y2:裁剪框右下的纵坐标 crop_size:裁剪框大小 img:裁剪图像(numpy.array) output_path:裁剪图片的输出路径 ------------------------------------ """ crop_size = (y2 - y1, x2 - x1) """ ------------------------------------ temp_x_1:裁剪框左边超出图像部分 temp_y_1:裁剪框上边超出图像部分 temp_x_2:裁剪框右边超出图像部分 temp_y_2:裁剪框下边超出图像部分 ------------------------------------ """ temp_x_1 = 0 temp_y_1 = 0 temp_x_2 = 0 temp_y_2 = 0 if y1 < 0: temp_y_1 = abs(y1) y1 = 0 if y2 > img.shape[0]: temp_y_2 = y2 y2 = img.shape[0] temp_y_2 = temp_y_2 - y2 if x1 < 0: temp_x_1 = abs(x1) x1 = 0 if x2 > img.shape[1]: temp_x_2 = x2 x2 = img.shape[1] temp_x_2 = temp_x_2 - x2 # 生成一张全透明背景 print("crop_size:", crop_size) background_bgr = np.full((crop_size[0], crop_size[1]), 255, dtype=np.uint8) background_a = np.full((crop_size[0], crop_size[1]), 0, dtype=np.uint8) background = cv2.merge( (background_bgr, background_bgr, background_bgr, background_a) ) background[ temp_y_1 : crop_size[0] - temp_y_2, temp_x_1 : crop_size[1] - temp_x_2 ] = img[y1:y2, x1:x2] return background def move(input_image): """ 裁剪主函数,输入一张 png 图像,该图像周围是透明的 """ png_img = input_image # 获取图像 height, width, channels = png_img.shape # 高 y、宽 x y_low, y_high, _, _ = U.get_box(png_img, model=2) # for 循环 base = np.zeros((y_high, width, channels), dtype=np.uint8) # for 循环 png_img = png_img[0 : height - y_high, :, :] # for 循环 png_img = np.concatenate((base, png_img), axis=0) return png_img, y_high def standard_photo_resize(input_image: np.array, size): """ input_image: 输入图像,即高清照 size: 标准照的尺寸 """ resize_ratio = input_image.shape[0] / size[0] resize_item = int(round(input_image.shape[0] / size[0])) if resize_ratio >= 2: for i in range(resize_item - 1): if i == 0: result_image = cv2.resize( input_image, (size[1] * (resize_item - i - 1), size[0] * (resize_item - i - 1)), interpolation=cv2.INTER_AREA, ) else: result_image = cv2.resize( result_image, (size[1] * (resize_item - i - 1), size[0] * (resize_item - i - 1)), interpolation=cv2.INTER_AREA, ) else: result_image = cv2.resize( input_image, (size[1], size[0]), interpolation=cv2.INTER_AREA ) return result_image def resize_image_by_min(input_image, esp=600): """ 将图像缩放为最短边至少为 esp 的图像。 :param input_image: 输入图像(OpenCV 矩阵) :param esp: 缩放后的最短边长 :return: 缩放后的图像,缩放倍率 """ height, width = input_image.shape[0], input_image.shape[1] min_border = min(height, width) if min_border < esp: if height >= width: new_width = esp new_height = height * esp // width else: new_height = esp new_width = width * esp // height return ( cv2.resize( input_image, (new_width, new_height), interpolation=cv2.INTER_AREA ), new_height / height, ) else: return input_image, 1