照片裁剪的算法问题?

咨询一个照片裁剪的算法问题,客户的要求为两眼间距100到120,头顶间距40到80,分辨率540*700,下面是我初步写的代码,我的思路是先通过dlib对人像照片进行检测,然后获取到两眼中间的位置,然后通过
horizontal_padding_width = round((original_image_resolution.width - two_eyes_middle.x) * 0.85)
vertical_padding_top = round(head_top.y * 0.3)
vertical_padding_bottom = round((original_image_resolution.height - chin.y) * 0.6)
这些值从两眼中间往上下左右偏移出间距,这样做可以最大化保留细节,然后再通过resize函数缩放成540700保留的细节完整,图片不会模糊,人物形变的也不多,但是不满足两眼间距和头顶间距,因为这里是固定的值,我不知道怎么通过计算让裁剪区域再缩放成540700刚好就是我要求的两眼间距

def cop(
    target_resolution: Resolution,
    two_eyes_target_distance: DistanceRange,
    head_to_top_margin_distance: DistanceRange,
    file_size: DistanceRange | None,
    img_path: str,
    save_path: str,
    bg_color: Color = (255, 255, 255, 255),
) -> None:
    mat = from_path(img_path)
    if mat is None:
        logger.error("无法读取图片文件")
        raise Exception("无法读取图片文件")

    original_image = remove_bg(mat, bg_color)
    if original_image is None:
        logger.error("未获取移除背景的图片")
        raise Exception("未获取移除背景的图片")

    gray_image = to_gray_image(original_image)
    if gray_image is None:
        logger.error("未获取到灰度图")
        raise Exception("未获取到灰度图")

    faces = get_faces(gray_image)

    if not faces:
        logger.error("未检测到人脸")
        raise Exception("未检测到人脸")

    if len(faces) > 1:
        logger.error("一张照片同时有多个人员, 请重新拍照")
        raise Exception("一张照片同时有多个人员, 请重新拍照")

    face = faces[0]
    landmarks = get_face_landmarks(gray_image, face)
    left_ear = get_left_ear(landmarks)  # 左耳
    chin = get_chin(landmarks)  # 下巴
    right_ear = get_right_ear(landmarks)  # 右耳
    head_top = get_highest_hair_point(gray_image)  # 头顶
    left_pupil = get_left_pupil(landmarks)  # 左瞳孔
    right_pupil = get_right_pupil(landmarks)  # 右瞳孔
    two_eyes_middle = get_two_eyes_middle_by_xy(left_pupil, right_pupil)  # 两眼中间坐标
    original_image_resolution = get_image_resolution(original_image)  # 原图分辨率
    two_eyes_distance = get_two_eyes_distance_by_xy(left_pupil, right_pupil)  # 两眼间距 461
    highest_hair_point = get_highest_hair_point(gray_image)  # 获取图片的最高点

    for target_eye in range(two_eyes_target_distance.min, two_eyes_target_distance.max + 1):
        horizontal_padding_width = round((original_image_resolution.width - two_eyes_middle.x) * 0.85)  # 水平填充宽度
        vertical_padding_top = round(head_top.y * 0.3)  # 垂直填充高度
        vertical_padding_bottom = round((original_image_resolution.height - chin.y) * 0.6)  # 垂直填充高度

        print(horizontal_padding_width)
        print(vertical_padding_top)
        print(vertical_padding_bottom)

        crop_box_left = two_eyes_middle.x - horizontal_padding_width  # 裁剪区域的左边
        crop_box_top = head_top.y - vertical_padding_top  # 裁剪区域的上边
        crop_box_right = two_eyes_middle.x + horizontal_padding_width  # 裁剪区域的右边
        crop_box_bottom = chin.y + vertical_padding_bottom  # 裁剪区域的下边

        crop_box_width = crop_box_right - crop_box_left  # 裁剪后的宽
        crop_box_height = crop_box_bottom - crop_box_top  # 裁剪后的高

        # 裁剪后图像中的瞳孔坐标
        left_eye_pupil_x_cropped = left_pupil.x - crop_box_left
        left_eye_pupil_y_cropped = left_pupil.y - crop_box_top
        right_eye_pupil_x_cropped = right_pupil.x - crop_box_left
        right_eye_pupil_y_cropped = right_pupil.y - crop_box_top

        scale_left_pupil = Point2D(x=left_eye_pupil_x_cropped, y=left_eye_pupil_y_cropped)
        scale_right_pupil = Point2D(x=right_eye_pupil_x_cropped, y=right_eye_pupil_y_cropped)
        scale_two_eye_distance = round(get_two_eyes_distance_by_xy(scale_left_pupil, scale_right_pupil))  # 两眼间距
        scale_head_top_distance = round(highest_hair_point.y - crop_box_top)

        print(f"--------------------{target_eye}----------------------")
        print(f"左耳:{left_ear}")
        print(f"右耳:{right_ear}")
        print(f"下巴:{chin}")
        print(f"头顶:{head_top}")
        print(f"左瞳孔:{left_pupil}")
        print(f"右瞳孔:{right_pupil}")
        print(f"原图:{(original_image_resolution.width, original_image_resolution.height)}")
        print(f"原图两眼间距:{two_eyes_distance}")
        print(f"原图头顶间距:{highest_hair_point.y}")
        print(f"左:{crop_box_left}")
        print(f"右:{crop_box_right}")
        print(f"上:{crop_box_top}")
        print(f"下:{crop_box_bottom}")
        print(f"裁剪后的宽:{crop_box_width}")
        print(f"裁剪后的高:{crop_box_height}")
        print(f"缩放后的两眼间距:{scale_two_eye_distance}")
        print(f"缩放后的头顶间距:{scale_head_top_distance}")

        scale_x = target_resolution.width / crop_box_width
        scale_y = target_resolution.height / crop_box_height
        scale_two_eye_distance = scale_two_eye_distance * scale_x
        scale_head_top_distance = scale_head_top_distance * scale_y

        print(f"scale_x:{scale_x}")
        print(f"scale_y:{scale_y}")
        print(f"裁剪后宽度:{crop_box_width}")
        print(f"裁剪后高度:{crop_box_height}")
        print(f"两眼间距:{scale_two_eye_distance}")
        print(f"头顶间距:{scale_head_top_distance}")
        print(f"--------------------{target_eye}----------------------")

        # eyes_result = two_eyes_target_distance.min < scale_two_eye_distance < two_eyes_target_distance.max
        # head_top_result = head_to_top_margin_distance.min < scale_head_top_distance < head_to_top_margin_distance.max
        # if not eyes_result or not head_top_result:
        #     continue

        new_img = original_image[crop_box_top:crop_box_bottom, crop_box_left:crop_box_right]
        new_img = cv2.resize(src=new_img, dsize=(target_resolution.width, target_resolution.height), interpolation=cv2.INTER_LANCZOS4)
        new_img = Image.fromarray(cv2.cvtColor(new_img, cv2.COLOR_BGR2RGB))
        save_img_path = save_path + "_" + str(target_eye) + ".jpg"
        if file_size is None:
            new_img.save(save_img_path, format="jpeg", quality=100, dpi=(350, 350))
        else:
            save_file(save_img_name=save_img_path, img=new_img, target_size_kb=file_size)

我尝试了不少方式,但是都失败了,所以我一点一点的改比例值调试,发现如果要两眼间距为110,
horizontal_padding_width = round((original_image_resolution.width - two_eyes_middle.x) * 0.85)这里就改成0.85左右就接近了,但是这个值怎么得来我不知道怎么去计算。

阅读 252
avatarAI BotBETA

为了解决你提出的问题,可以通过以下步骤来动态计算裁剪区域的宽度和高度,从而确保在缩放后满足特定的两眼间距和头顶间距要求。

  1. 确定目标两眼间距和头顶间距:客户要求两眼间距在100到120之间,头顶间距在40到80之间。
  2. 计算缩放比例:根据目标分辨率(540x700)和裁剪区域中两眼间距以及头顶间距的实际值来计算缩放比例。
  3. 调整裁剪区域:通过计算得到的缩放比例调整裁剪区域的宽度和高度,使得缩放后的图片满足客户的间距要求。

以下是对你代码的一些修改和解释,以实现上述逻辑:

import cv2
from PIL import Image
from typing import NamedTuple, Tuple

# 定义一些辅助类型
class Resolution(NamedTuple):
    width: int
    height: int

class DistanceRange(NamedTuple):
    min: int
    max: int

class Point2D(NamedTuple):
    x: int
    y: int

def cop(
    target_resolution: Resolution,
    two_eyes_target_distance: DistanceRange,
    head_to_top_margin_distance: DistanceRange,
    img_path: str,
    save_path: str,
    bg_color: Tuple[int, int, int, int] = (255, 255, 255, 255),
) -> None:
    # 读取并处理图像
    mat = cv2.imread(img_path)
    if mat is None:
        raise Exception("无法读取图片文件")

    gray_image = cv2.cvtColor(mat, cv2.COLOR_BGR2GRAY)

    # 假设你有一个函数可以检测人脸和面部特征点
    # faces, landmarks = detect_faces_and_landmarks(gray_image)
    # 这里使用伪代码模拟这些函数
    faces = [[100, 100, 200, 200]]  # 假设检测到的人脸区域
    landmarks = [[...]]  # 假设检测到的面部特征点

    if not faces or len(faces) > 1:
        raise Exception("未检测到人脸或检测到多个人脸")

    face = faces[0]
    left_pupil = Point2D(x=landmarks[36][0], y=landmarks[36][1])  # 假设左瞳孔的索引为36
    right_pupil = Point2D(x=landmarks[45][0], y=landmarks[45][1])  # 假设右瞳孔的索引为45

    two_eyes_middle = Point2D(x=(left_pupil.x + right_pupil.x) // 2, y=(left_pupil.y + right_pupil.y) // 2)
    head_top = Point2D(x=landmarks[0][0], y=landmarks[0][1])  # 假设头顶的索引为0
    chin = Point2D(x=landmarks[8][0], y=landmarks[8][1])  # 假设下巴的索引为8

    # 计算目标裁剪区域的宽高比
    target_aspect_ratio = target_resolution.width / target_resolution.height

    # 初始裁剪区域(根据面部特征计算)
    initial_crop_width = abs(left_pupil.x - right_pupil.x) * 2  # 两眼间距的两倍作为初始宽度
    initial_crop_height = abs(head_top.y - chin.y)  # 头顶到下巴的距离作为初始高度

    # 调整裁剪区域以符合目标宽高比
    if initial_crop_width / initial_crop_height > target_aspect_ratio:
        # 宽度过大,需要调整高度
        initial_crop_height = int(initial_crop_width / target_aspect_ratio)
    else:
        # 高度过大,需要调整宽度
        initial_crop_width = int(initial_crop_height * target_aspect_ratio)

    # 计算缩放比例
    target_two_eyes_distance = (two_eyes_target_distance.min + two_eyes_target_distance.max) // 2
    target_head_top_distance = (head_to_top_margin_distance.min + head_to_top_margin_distance.max) // 2

    scale_x = target_resolution.width / initial_crop_width
    scale_y = target_resolution.height / initial_crop_height

    actual_two_eyes_distance_after_scale = (left_pupil.x - right_pupil.x) * scale_x
    actual_head_top_distance_after_scale = head_top.y * scale_y  # 假设裁剪区域的顶部与头顶对齐

    # 调整缩放比例以满足间距要求
    scale_factor = min(
        target_two_eyes_distance / actual_two_eyes_distance_after_scale,
        target_head_top_distance / actual_head_top_distance_after_scale
    )

    final_scale_x = scale_x * scale_factor
    final_scale_y = scale_y * scale_factor

    # 计算最终的裁剪区域
    crop_box_left = two_eyes_middle.x - (target_resolution.width / (2 * final_scale_x))
    crop_box_top = head_top.y - (target_resolution.height / final_scale_y - (chin.y - head_top.y) * final_scale_y)
    crop_box_right = two_eyes_middle.x + (target_resolution.width / (2 * final_scale_x))
    crop_box_bottom = head_top.y + (target_resolution.height / final_scale_y)

    # 裁剪并调整大小
    new_img = mat[int(crop_box_top):int(crop_box_bottom), int(crop_box_left):int(crop_box_right)]
    new_img = cv2.resize(new_img, (target_resolution.width, target_resolution.height), interpolation=cv2.INTER_LANCZOS4)

    # 保存结果
    new_img_pil = Image.fromarray(cv2.cvtColor(new_img, cv2.COLOR_BGR2RGB))
    new_img_pil.save(save_path, format="jpeg", quality=100)

# 注意:你需要实现 detect_faces_and_landmarks 函数来获取 faces 和 landmarks
# 这里只是用伪代码模拟了这些值

解释

  1. 计算初始裁剪区域:根据检测到的面部特征点计算初始裁剪区域的宽度和高度。
  2. 调整裁剪区域:使裁剪区域的宽高比与目标分辨率的宽高比一致。
  3. 计算缩放比例:根据目标两眼间距和头顶间距计算缩放比例。
  4. 调整缩放比例:通过比较实际缩放后的间距和目标间距,调整缩放比例以确保满足要求。
  5. 计算最终裁剪区域:使用调整后的缩放比例计算最终的裁剪区域。
  6. 裁剪并调整大小:裁剪图像并调整其大小以符合目标分辨率。
  7. 保存结果:将处理后的图像保存为JPEG文件。

这种方法可以确保在缩放后的图像中,两眼间距和头顶间距满足客户的要求。

1 个回答
def cop(
    target_resolution: Resolution,
    two_eyes_target_distance: DistanceRange,
    head_to_top_margin_distance: DistanceRange,
    file_size: DistanceRange | None,
    img_path: str,
    save_path: str,
    bg_color: Color = (255, 255, 255, 255),
) -> None:
    mat = from_path(img_path)
    if mat is None:
        logger.error("无法读取图片文件")
        raise Exception("无法读取图片文件")

    original_image = remove_bg(mat, bg_color)
    if original_image is None:
        logger.error("未获取移除背景的图片")
        raise Exception("未获取移除背景的图片")

    gray_image = to_gray_image(original_image)
    if gray_image is None:
        logger.error("未获取到灰度图")
        raise Exception("未获取到灰度图")

    faces = get_faces(gray_image)

    if not faces:
        logger.error("未检测到人脸")
        raise Exception("未检测到人脸")

    if len(faces) > 1:
        logger.error("一张照片同时有多个人员, 请重新拍照")
        raise Exception("一张照片同时有多个人员, 请重新拍照")

    face = faces[0]
    landmarks = get_face_landmarks(gray_image, face)
    left_ear = get_left_ear(landmarks)  # 左耳
    chin = get_chin(landmarks)  # 下巴
    right_ear = get_right_ear(landmarks)  # 右耳
    head_top = get_highest_hair_point(gray_image)  # 头顶
    left_pupil = get_left_pupil(landmarks)  # 左瞳孔
    right_pupil = get_right_pupil(landmarks)  # 右瞳孔
    two_eyes_middle = get_two_eyes_middle_by_xy(left_pupil, right_pupil)  # 两眼中间坐标
    original_image_resolution = get_image_resolution(original_image)  # 原图分辨率
    two_eyes_distance = get_two_eyes_distance_by_xy(left_pupil, right_pupil)  # 两眼间距

    max_attempts = 10  # 最大尝试次数

    for attempt in range(max_attempts):
        horizontal_padding_width = round((original_image_resolution.width - two_eyes_middle.x) * 0.85)
        vertical_padding_top = round(head_top.y * 0.3)
        vertical_padding_bottom = round((original_image_resolution.height - chin.y) * 0.6)

        crop_box_left = two_eyes_middle.x - horizontal_padding_width
        crop_box_top = head_top.y - vertical_padding_top
        crop_box_right = two_eyes_middle.x + horizontal_padding_width
        crop_box_bottom = chin.y + vertical_padding_bottom

        crop_box_width = crop_box_right - crop_box_left
        crop_box_height = crop_box_bottom - crop_box_top

        left_eye_pupil_x_cropped = left_pupil.x - crop_box_left
        left_eye_pupil_y_cropped = left_pupil.y - crop_box_top
        right_eye_pupil_x_cropped = right_pupil.x - crop_box_left
        right_eye_pupil_y_cropped = right_pupil.y - crop_box_top

        scale_left_pupil = Point2D(x=left_eye_pupil_x_cropped, y=left_eye_pupil_y_cropped)
        scale_right_pupil = Point2D(x=right_eye_pupil_x_cropped, y=right_eye_pupil_y_cropped)
        
        scale_two_eye_distance = round(get_two_eyes_distance_by_xy(scale_left_pupil, scale_right_pupil))
        scale_head_top_distance = round(head_top.y - crop_box_top)

        print(f"Attempt {attempt + 1}:")
        print(f"裁剪后的宽:{crop_box_width}, 裁剪后的高:{crop_box_height}")
        print(f"缩放后的两眼间距:{scale_two_eye_distance}, 缩放后的头顶间距:{scale_head_top_distance}")

        if (two_eyes_target_distance.min <= scale_two_eye_distance <= two_eyes_target_distance.max and 
            head_to_top_margin_distance.min <= scale_head_top_distance <= head_to_top_margin_distance.max):
            new_img = original_image[crop_box_top:crop_box_bottom, crop_box_left:crop_box_right]
            new_img = cv2.resize(src=new_img, dsize=(target_resolution.width, target_resolution.height), interpolation=cv2.INTER_LANCZOS4)
            new_img = Image.fromarray(cv2.cvtColor(new_img, cv2.COLOR_BGR2RGB))
            save_img_path = save_path + f"_{attempt + 1}.jpg"
            if file_size is None:
                new_img.save(save_img_path, format="jpeg", quality=100, dpi=(350, 350))
            else:
                save_file(save_img_name=save_img_path, img=new_img, target_size_kb=file_size)
            break
        
        # 动态调整填充宽度和高度以接近目标值
        if scale_two_eye_distance < two_eyes_target_distance.min:
            horizontal_padding_width += round((two_eyes_target_distance.min - scale_two_eye_distance) / 2)
            vertical_padding_top += round((head_to_top_margin_distance.min - scale_head_top_distance) / 2)
        elif scale_two_eye_distance > two_eyes_target_distance.max:
            horizontal_padding_width -= round((scale_two_eye_distance - two_eyes_target_distance.max) / 2)
            vertical_padding_top -= round((scale_head_top_distance - head_to_top_margin_distance.max) / 2)

在循环中我添加了动态调整horizontal_padding_widthvertical_padding_top,根据眼距和头顶间距与目标值间的差进行调整。
我添加了退出循环的逻辑,当眼距和头顶间距满足要求时退出。
我还添加了max_attempts变量限制尝试次数。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏