智能抠图工具:生成证件照与背景替换的完整解决方案及完整的Python源代码

引言

在数字化时代,我们经常需要处理各种图像需求,无论是制作证件照、更换背景,还是简单的图像抠图。传统方法往往需要专业的图像处理软件和复杂的技术操作,这对于普通用户来说门槛较高。今天我要介绍一款基于Python开发的智能抠图软件,它集成了人脸检测、证件照生成、背景替换等多项实用功能,让复杂的图像处理变得简单易用。

程序运行初始画面:

上传图片:

抠图结果:

人脸检测结果:

选择证件照片背景颜色:

证件照类型选择:

生成的证件照:

保存后的证件照片:尺寸是 295*413px
​​​​​​​

背景操作:(可缩放和改变位置)

更多抠图对比:

软件功能概览

这款智能抠图软件具备以下核心功能:

1. 智能抠图

  • 高质量前景分割:使用rembg库进行精准的图像分割

  • 多算法支持:提供智能抠图和传统分割两种方法

  • 保持原始分辨率:所有处理操作都保持图像原始质量

2. 人脸检测与识别

  • 自动人脸定位:使用OpenCV Haar级联分类器检测人脸

  • 多脸处理:自动选择图像中最大的人脸

  • 可视化反馈:在人脸周围显示绿色边框和尺寸信息

3. 专业证件照生成

  • 多种标准尺寸:支持1寸、2寸、护照等15种常用证件照规格,并可自定义

  • 智能裁剪:基于人脸位置自动调整裁剪区域

  • 背景颜色自定义:支持白色、红色、蓝色、透明及自选背景颜色

  • 自适应缩放:根据头像大小自动调整证件照比例

4. 灵活的背景替换

  • 任意背景图像:可以将抠出的前景放置在任何背景上

  • 交互式调整:支持鼠标拖拽调整位置,滚轮调整大小,前景和背景分别缩放

  • 实时预览:所见即所得的编辑体验

安装与运行说明

系统要求

  • Python 3.7 或更高版本

  • Windows7 及以上系统

  • 至少 2GB 可用内存

安装步骤

  1. 安装Python依赖库

pip install PyQt5 opencv-python pillow numpy rembg
  1. 重要注意事项

首次运行时间较长:程序第一次运行时需要从网络下载AI模型文件(约200MB),请耐心等待。这个过程是自动的,但可能需要几分钟时间,具体取决于网络速度。

rembg库的特殊说明

  • rembg是基于深度学习的图像分割工具,提供高质量的抠图效果

  • 如果rembg安装失败,程序会自动回退到传统分割方法

  • 传统方法虽然效果稍逊,但完全离线运行,速度更快

人脸检测库

  • 使用OpenCV的Haar级联分类器进行人脸检测

  • 该技术成熟稳定,检测准确率高

  • 分类器文件会自动下载,无需手动配置

运行程序

python photo_cutout_tool.py

详细使用教程

基本操作流程


       上传图像
        ┗━开始抠图
            ┣━检测头像
            ┃   ┣━选择颜色━┓
            ┃   ┗━规格尺寸━┫
            ┃        生成证件照━┓
            ┗━背景替换         ┃
                ┗━调整大小和位置━━━┫  
                          ┃
                         下载图像 

分步操作指南

1. 上传图像
  • 点击"上传图像"按钮,支持常见图像格式(PNG、JPG、JPEG、BMP等)

  • 程序会自动显示图像信息和预览

2. 智能抠图
  • 选择抠图方法(推荐使用智能抠图)

  • 点击"开始抠图"进行处理

  • 处理时间取决于图像大小和硬件性能

3. 人脸检测(可选)
  • 点击"检测头像"自动识别人脸

  • 系统会框出检测到的人脸并显示尺寸

  • 如果未检测到人脸,会提供相应提示

4. 证件照生成
  • 选择背景颜色(白、红、蓝、透明或自定义)

  • 选择证件照规格(15种预设尺寸或自定义)

  • 点击"生成证件照"完成制作

  • 系统会根据人脸位置智能调整裁剪区域

5. 背景替换
  • 点击"更换背景"选择背景图像

  • 启用交互模式进行精细调整:

    • 鼠标拖拽:移动前景位置

    • 鼠标滚轮:调整前景大小

    • Ctrl+滚轮:调整背景大小

6. 下载结果
  • 支持PNG(带透明通道)和JPG格式

  • 自动保存处理后的图像

技术特色与创新点

1. 多线程处理架构

class ImageProcessingThread(QThread):
    """图像处理线程,避免界面卡顿"""
    finished = pyqtSignal(np.ndarray, float)
    progress_updated = pyqtSignal(int)
    error_occurred = pyqtSignal(str)

程序采用多线程设计,所有耗时的图像处理操作都在后台线程执行,确保界面流畅响应。

2. 智能证件照生成算法

def generate_id_photo(self):
    # 基于人脸检测的智能裁剪
    if self.face_rect is not None:
        x, y, w, h = self.face_rect
        # 根据人脸比例自动计算裁剪区域
        left=0.438  # 左右延伸比例
        top=0.367   # 向上延伸比例

证件照生成算法会根据检测到的人脸自动计算最佳的裁剪区域,确保头像在证件照中的位置和大小都符合专业标准。延伸比例为经验值,如果需要更智能,该段代码可以提升判断能力。

3. 交互式背景替换

class InteractiveBackgroundReplacement:
    """交互式背景替换,支持实时调整"""
    def wheel_event(self, event):
        # Ctrl+滚轮调整背景,普通滚轮调整前景
        modifiers = event.modifiers()
        if modifiers == Qt.ControlModifier:
            # 调整背景大小
            self.bg_scale *= scale_factor
        else:
            # 调整前景大小
            self.fg_scale *= scale_factor

创新的交互式背景替换功能,让用户可以直观地调整前景和背景的相对大小和位置。

4. 完善的错误处理

程序包含完整的异常处理机制,对各种可能出现的错误情况都有友好的提示和处理方案。

证件照拍摄建议

为了获得最佳的证件照效果,建议:

  1. 拍摄环境

    • 光线充足均匀,避免强烈阴影

    • 背景简洁,颜色单一

    • 头部四周预留足够空间

  2. 人物要求

    • 姿势端正,头部正直

    • 脸部清晰可见,不遮挡耳朵眉毛

    • 不戴首饰,嘴唇自然闭合

    • 穿着有领深色衣服

  3. 图像质量

    • 使用高分辨率相机或手机拍摄

    • 对焦准确,图像清晰

    • 避免过度美颜或滤镜

常见问题解答

Q: 第一次运行时为什么很慢?

A: 程序需要下载AI模型文件(约200MB),这是正常现象。下载完成后再次运行就会很快。

Q: 人脸检测失败怎么办?

A: 可以尝试:调整图像角度、确保光线充足、脸部清晰可见,或者直接使用更换背景功能手动调整。

Q: 如何获得更好的抠图效果?

A: 选择对比度明显的图像,确保前景与背景颜色差异较大。对于复杂背景,智能抠图(rembg)效果更好。

Q: 支持批量处理吗?

A: 当前版本主要针对单张图像处理,批量处理可以通过多次运行程序实现。

技术架构详解

核心库介绍

  1. PyQt5:构建图形用户界面,提供丰富的UI组件和事件处理

  2. OpenCV:图像处理和人脸检测的核心库

  3. rembg:基于深度学习的图像分割工具

  4. PIL/Pillow:图像读取和格式转换

  5. numpy:高效的数值计算和数组操作

算法优势

  • 人脸检测:使用成熟的Haar特征分类器,准确率高

  • 图像分割:结合传统算法和AI技术,适应不同场景

  • 智能裁剪:基于人脸位置的自动化证件照生成

  • 实时交互:流畅的用户体验和即时反馈

结语

这款智能抠图软件将复杂的图像处理技术封装在简单易用的界面中,让普通用户也能轻松完成专业的图像处理任务。无论是制作证件照、更换背景,还是简单的抠图需求,都能得到满意的结果。

程序的源代码完全开放,开发者可以根据自己的需求进行定制和扩展。如果你在使用过程中遇到任何问题或有改进建议,欢迎通过CSDN博客与我交流。

资源链接

希望这个工具能够帮助到你,让你的图像处理工作变得更加高效和愉快!如果引用,请注意出处。

完整的源代码

# -*- coding: utf-8 -*-
# 版权声明:本代码为原创,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。链接:https://blog.csdn.net/weixin_69832035

import sys
import os
import cv2
import numpy as np
import time
from datetime import datetime
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QPushButton, QLabel, QTextEdit, QFileDialog, QMessageBox,
                             QComboBox, QDialog, QScrollArea, QProgressBar, QTabWidget,
                             QSpinBox, QColorDialog)
from PyQt5.QtCore import Qt, QUrl, QThread, pyqtSignal,QTimer
from PyQt5.QtGui import QImageReader, QPixmap, QImage, QDesktopServices, QFont, QPainter,QColor,QPen
import traceback
from PIL import Image

try:
    import rembg
    REMBG_AVAILABLE = True
except ImportError:
    REMBG_AVAILABLE = False
    print("rembg库未安装,将使用传统图像分割方法")

class ImageProcessingThread(QThread):
    """图像处理线程"""
    finished = pyqtSignal(np.ndarray, float)
    progress_updated = pyqtSignal(int)
    error_occurred = pyqtSignal(str)

    def __init__(self, image_path, method="traditional"):
        super().__init__()
        self.image_path = image_path
        self.method = method

    def run(self):
        try:
            start_time = time.time()

            # 读取图像 - 使用PIL处理中文路径
            try:
                pil_image = Image.open(self.image_path)
                # 确保图像是RGB格式
                if pil_image.mode != 'RGB':
                    pil_image = pil_image.convert('RGB')
                # 直接转换为RGB数组,避免BGR转换
                image = np.array(pil_image)
            except Exception as e:
                # 如果PIL无法读取,尝试OpenCV
                image = cv2.imread(self.image_path)
                if image is None:
                    raise Exception(f"无法读取图像文件: {str(e)}")
                # OpenCV读取的是BGR,转换为RGB
                image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

            self.progress_updated.emit(30)

            # 根据选择的方法进行图像分割
            if self.method == "rembg" and REMBG_AVAILABLE:
                result_rgba = self.rembg_segmentation(image)
            elif self.method == "traditional":
                result_rgba = self.traditional_segmentation(image)
            else:
                # 如果rembg不可用,回退到传统方法
                result_rgba = self.traditional_segmentation(image)
                self.error_occurred.emit("rembg不可用,已使用传统分割方法")

            self.progress_updated.emit(80)

            processing_time = time.time() - start_time
            self.progress_updated.emit(100)
            self.finished.emit(result_rgba, processing_time)

        except Exception as e:
            self.error_occurred.emit(f"处理错误: {str(e)}")

    def rembg_segmentation(self, image):
        """使用rembg进行智能抠图"""
        try:
            # rembg需要BGR格式输入
            if len(image.shape) == 3 and image.shape[2] == 3:
                image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
            else:
                image_bgr = image

            result = rembg.remove(image_bgr)
            # rembg返回的是BGRA格式,转换为RGBA
            result_rgba = cv2.cvtColor(result, cv2.COLOR_BGRA2RGBA)
            return result_rgba
        except Exception as e:
            # 如果rembg失败,回退到传统方法
            print(f"rembg处理失败: {e}")
            return self.traditional_segmentation(image)

    def traditional_segmentation(self, image):
        """使用传统图像分割方法"""
        # 方法1: GrabCut算法
        try:
            return self.grabcut_segmentation(image)
        except:
            # 如果GrabCut失败,使用简单的颜色阈值方法
            return self.threshold_segmentation(image)

    def grabcut_segmentation(self, image):
        """使用GrabCut算法进行图像分割"""
        # GrabCut需要BGR格式
        if len(image.shape) == 3 and image.shape[2] == 3:
            image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
        else:
            image_bgr = image

        mask = np.zeros(image_bgr.shape[:2], np.uint8)

        # 初始化背景和前景模型
        bgd_model = np.zeros((1, 65), np.float64)
        fgd_model = np.zeros((1, 65), np.float64)

        # 定义矩形区域(假设前景在图像中央)
        height, width = image_bgr.shape[:2]
        rect = (int(width*0.1), int(height*0.1), int(width*0.8), int(height*0.8))

        # 应用GrabCut算法
        cv2.grabCut(image_bgr, mask, rect, bgd_model, fgd_model, 5, cv2.GC_INIT_WITH_RECT)

        # 创建掩码:0和2为背景,1和3为前景
        mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')

        # 创建带透明通道的图像 - 使用RGBA格式
        result = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGBA)
        result[:, :, 3] = mask2 * 255

        # 对边缘进行模糊处理以获得更自然的效果
        kernel = np.ones((3,3), np.uint8)
        mask2 = cv2.erode(mask2, kernel, iterations=1)
        mask2 = cv2.GaussianBlur(mask2.astype(np.float32), (5,5), 0)
        result[:, :, 3] = mask2 * 255

        return result

    def threshold_segmentation(self, image):
        """使用颜色阈值进行简单分割"""
        # 转换为HSV颜色空间 - 需要BGR格式
        if len(image.shape) == 3 and image.shape[2] == 3:
            image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
        else:
            image_bgr = image

        hsv = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV)

        # 根据颜色范围创建掩码(这里假设背景较亮)
        lower_bound = np.array([0, 0, 100])
        upper_bound = np.array([180, 255, 255])
        mask = cv2.inRange(hsv, lower_bound, upper_bound)

        # 反转掩码(我们想要前景)
        mask = cv2.bitwise_not(mask)

        # 形态学操作改善掩码质量
        kernel = np.ones((5,5), np.uint8)
        mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
        mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)

        # 创建带透明通道的图像 - 使用RGBA格式
        result = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGBA)
        result[:, :, 3] = mask

        return result


class FaceDetectionThread(QThread):
    """人脸检测线程"""
    finished = pyqtSignal(dict)
    progress_updated = pyqtSignal(int)
    error_occurred = pyqtSignal(str)

    def __init__(self, image):
        super().__init__()
        self.image = image

    def run(self):
        try:
            self.progress_updated.emit(30)

            # 加载人脸检测器
            face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')

            # 转换为灰度图像进行人脸检测
            if len(self.image.shape) == 3 and self.image.shape[2] == 4:
                gray = cv2.cvtColor(self.image, cv2.COLOR_RGBA2GRAY)
            elif len(self.image.shape) == 3 and self.image.shape[2] == 3:
                gray = cv2.cvtColor(self.image, cv2.COLOR_RGB2GRAY)
            else:
                gray = self.image

            self.progress_updated.emit(60)

            # 检测人脸
            faces = face_cascade.detectMultiScale(
                gray,
                scaleFactor=1.1,
                minNeighbors=5,
                minSize=(30, 30),
                flags=cv2.CASCADE_SCALE_IMAGE
            )

            self.progress_updated.emit(90)

            result = {
                'faces': faces,
                'face_count': len(faces),
                'image_size': self.image.shape
            }

            self.progress_updated.emit(100)
            self.finished.emit(result)

        except Exception as e:
            self.error_occurred.emit(f"人脸检测错误: {str(e)}")

class PhotoSizes:
    # 定义证件照尺寸(像素)
    def __init__(self):
        self.sizes = {
            "自定义":(295,413),
            "1寸(25*35)mm":(295,413),
            "2寸(35*49)mm":(413,579),
            "大1寸(33*48)mm":(390,567),
            "小1寸(22*32)mm":(260,378),
            "大2寸(35*53)mm":(413,626),
            "小2寸(35*45)mm":(413,531),
            "3寸(55*84)mm":(650,992),
            "4寸(76*100)mm":(898,1181),
            "5寸(89*127)mm":(1051,1500),
            "护照(3*48)mm":(390,567),
            "离婚登记照(35*53)mm":(413,626),
            "注册会计师证(15*19)mm":(178,220),
            "驾驶证(22*32)mm":(260,378),
            "导游资格证(24*33)mm":(285,385),
            "简历照片(25*35)mm":(295,413)
        }
    def get_size(self, size_name):
        return self.sizes.get(size_name, (295, 413))

    def get_names(self):
        return list(self.sizes.keys())

class PhotoIDThread(QThread):
    """证件照生成线程"""
    finished = pyqtSignal(np.ndarray)
    progress_updated = pyqtSignal(int)
    error_occurred = pyqtSignal(str)

    def __init__(self, foreground, size="1寸", background_color=(255, 255, 255), face_rect=None):
        super().__init__()
        self.foreground = foreground.copy()  # 创建副本,避免修改原始图像
        self.size = size
        # 使用RGB颜色顺序
        self.background_color = background_color
        self.face_rect = face_rect
        # self.photo_sizes = PhotoSizes()

    def run(self):
        self.progress_updated.emit(30)

        target_size = window.photo_sizes.get_size(self.size) # 获取证件照尺寸(像素)
        if target_size[0]/target_size[1] < 0.5 or target_size[0]/target_size[1]>1:
            self.error_occurred.emit(f"警告:证件照尺寸的 宽高比 应在 0.5:1 至 1:1 之间,当前是 {target_size[0]}:{target_size[1]} 即:{target_size[0]/target_size[1]:.3f}:1 ,生成的照片可能会有问题!")

        try:
            # 如果检测到人脸,根据人脸区域裁剪图像
            if self.face_rect is not None:
                x, y, w, h = self.face_rect
                left=0.438 # 按当前脸部比例向左、右延伸宽度比例
                top=0.367  # 按当前脸部比例向上延伸高度比例
                x1 = x - int(w * left)
                x2 = x + w + int(w * left)
                height = int((x2-x1)*target_size[1]/target_size[0]) # 照片应有高度
                y1 = y - int(h * top)
                y2 = y1 + height # 向下延伸是剩余高度
                fg_height, fg_width = self.foreground.shape[:2] # 图像的高和宽
                err=False
                if x1 < 0:
                    x1=0
                    err=True
                if y1 < 0:
                    y1=0
                    err=True
                if x2 > fg_width:
                    x2=fg_width
                    err=True
                if y2 > fg_height:
                    y2=fg_height
                    err=True
                if err:
                    self.error_occurred.emit("在生成证件照时原始照片尺寸不足!\n生成的照片可能会不完整,如果需要完整的证件照片,请重新选择一张合适的照片再次生成。")

                # 裁剪图像
                self.foreground = self.foreground[y1:y2, x1:x2]

            # 调整前景大小以适应证件照尺寸
            fg_height, fg_width = self.foreground.shape[:2]

            # 计算缩放比例
            if self.face_rect is not None:
                # 基于裁剪后的图像计算缩放
                scale = min(target_size[0] / fg_width, target_size[1] / fg_height)
            else:
                # 如果没有检测到人脸,使用整个图像
                scale = min(target_size[0] / fg_width, target_size[1] / fg_height) * 0.8

            new_width = int(fg_width * scale)
            new_height = int(fg_height * scale)

            # 调整前景大小
            foreground_resized = cv2.resize(self.foreground, (new_width, new_height))

            self.progress_updated.emit(60)

            # 创建背景 - 确保是3通道,使用RGB颜色顺序
            background = np.ones((target_size[1], target_size[0], 3), dtype=np.uint8)
            background[:, :, 0] = self.background_color[0]  # R
            background[:, :, 1] = self.background_color[1]  # G
            background[:, :, 2] = self.background_color[2]  # B

            # 将前景放在背景中央
            x_offset = (target_size[0] - new_width) // 2
            y_offset = (target_size[1] - new_height) // 2

            # 检查前景的通道数,正确处理3通道和4通道图像
            if foreground_resized.shape[2] == 4:  # RGBA图像
                alpha = foreground_resized[:, :, 3] / 255.0
                foreground_rgb = foreground_resized[:, :, :3]
            else:  # RGB图像
                alpha = np.ones(foreground_resized.shape[:2], dtype=np.float32)
                foreground_rgb = foreground_resized

            # 将前景与背景合成
            for c in range(3):
                background[y_offset:y_offset+new_height, x_offset:x_offset+new_width, c] = \
                    (alpha * foreground_rgb[:, :, c] +
                     (1 - alpha) * background[y_offset:y_offset+new_height, x_offset:x_offset+new_width, c])

            self.progress_updated.emit(100)
            self.finished.emit(background)
        except Exception as e:
            self.error_occurred.emit(f"证件照生成错误: {str(e)}\n{traceback.format_exc()}")


class ImageViewer(QLabel):
    """图像显示组件"""
    def __init__(self):
        super().__init__()
        self.setAlignment(Qt.AlignCenter)
        self.setText("图像预览区域")
        self.setStyleSheet("border: 1px solid gray; background-color: #f0f0f0;")
        self.setMinimumSize(400, 400)
        self.setMouseTracking(True)  # 启用鼠标跟踪
        self.current_image_array = None  # 存储当前显示的图像数组

    def set_image(self, image_array):
        """设置显示的图像"""
        if image_array is None:
            return

        self.current_image_array = image_array  # 保存当前图像
        height, width = image_array.shape[:2]

        # 确保颜色顺序正确
        if len(image_array.shape) == 3 and image_array.shape[2] == 4:
            # RGBA图像 - 直接使用RGBA格式显示
            bytes_per_line = 4 * width
            q_image = QImage(image_array.data, width, height, bytes_per_line, QImage.Format_RGBA8888)
        elif len(image_array.shape) == 3 and image_array.shape[2] == 3:
            # RGB图像 - 直接使用
            bytes_per_line = 3 * width
            q_image = QImage(image_array.data, width, height, bytes_per_line, QImage.Format_RGB888)
        else:
            # 单通道图像
            bytes_per_line = width
            q_image = QImage(image_array.data, width, height, bytes_per_line, QImage.Format_Grayscale8)

        pixmap = QPixmap.fromImage(q_image)
        self.setPixmap(pixmap.scaled(self.width(), self.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation))

class InteractiveBackgroundReplacement:
    """交互式背景替换功能"""
    def __init__(self):
        self.bg_image = None
        self.fg_image = None
        self.fg_scale = 1.0
        self.fg_position = [0, 0]
        self.bg_scale = 1.0  # 背景缩放比例
        self.bg_position = [0, 0]  # 背景位置
        self.is_dragging = False
        self.last_mouse_pos = None
        self.is_active = False
        self.viewer_size = None  # 添加预览区尺寸跟踪
        self.original_bg_size = None  # 原始背景图像尺寸

    def set_background(self, bg_image):
        """设置背景图像"""
        self.bg_image = bg_image
        self.original_bg_size = bg_image.shape[:2]  # 保存原始背景尺寸 (高度, 宽度)
        self.bg_scale = 1.0  # 重置背景缩放
        self.bg_position = [0, 0]  # 重置背景位置
        self.is_active = True

    def set_foreground(self, fg_image):
        """设置前景图像"""
        self.fg_image = fg_image
        self.fg_scale = 1.0
        self.fg_position = [0, 0]

    def mouse_press_event(self, event, viewer_size):
        """鼠标按下事件 - 添加viewer_size参数"""
        if not self.is_active or self.fg_image is None:
            return False

        self.viewer_size = viewer_size  # 保存预览区尺寸

        if event.button() == Qt.LeftButton:
            self.is_dragging = True
            self.last_mouse_pos = event.pos()
            return True
        return False

    def mouse_move_event(self, event):
        """鼠标移动事件 - 改进拖动逻辑"""
        if not self.is_active or not self.is_dragging or self.fg_image is None:
            return False

        if self.last_mouse_pos:
            delta = event.pos() - self.last_mouse_pos

            # 允许任意拖动,不限制边界
            self.fg_position[0] += delta.x()
            self.fg_position[1] += delta.y()

            self.last_mouse_pos = event.pos()
            return True
        return False

    def mouse_release_event(self, event):
        """鼠标释放事件"""
        if event.button() == Qt.LeftButton and self.is_dragging:
            self.is_dragging = False
            self.last_mouse_pos = None
            return True
        return False

    def wheel_event(self, event):
        """鼠标滚轮事件 - 添加Ctrl+滚轮调整背景大小"""
        if not self.is_active or self.fg_image is None:
            return False

        # 获取滚轮角度增量
        delta = event.angleDelta().y()

        # 检查是否按下了Ctrl键
        modifiers = event.modifiers()
        if modifiers == Qt.ControlModifier:
            # Ctrl+滚轮:调整背景大小
            scale_factor = 1.1 if delta > 0 else 0.9
            old_bg_scale = self.bg_scale
            self.bg_scale *= scale_factor

            # 计算最小缩放比例 - 确保背景图像至少能完全显示在预览区内
            if self.viewer_size and self.original_bg_size:
                viewer_width, viewer_height = self.viewer_size.width(), self.viewer_size.height()
                bg_height, bg_width = self.original_bg_size

                # 计算背景图像完全显示在预览区内所需的最小缩放比例
                min_scale_x = viewer_width / bg_width
                min_scale_y = viewer_height / bg_height
                min_scale = min(min_scale_x, min_scale_y)

                # 限制背景缩放范围
                self.bg_scale = max(min_scale, min(self.bg_scale, 10.0))  # 最小为完全显示,最大为10倍

            return True
        else:
            # 普通滚轮:调整前景大小
            scale_factor = 1.1 if delta > 0 else 0.9
            old_scale = self.fg_scale
            self.fg_scale *= scale_factor

            # 限制前景缩放范围
            self.fg_scale = max(0.1, min(10.0, self.fg_scale))  # 扩大缩放范围

            # 基于鼠标位置进行缩放中心调整
            if self.last_mouse_pos:
                mouse_pos = self.last_mouse_pos
                # 计算缩放中心相对于前景图像的位置
                scale_ratio = self.fg_scale / old_scale
                self.fg_position[0] = mouse_pos.x() - (mouse_pos.x() - self.fg_position[0]) * scale_ratio
                self.fg_position[1] = mouse_pos.y() - (mouse_pos.y() - self.fg_position[1]) * scale_ratio

            return True

    def get_composite_image(self, viewer_size):
        """获取合成图像 - 改进边界处理"""
        if not self.is_active or self.bg_image is None or self.fg_image is None:
            return None

        viewer_width, viewer_height = viewer_size.width(), viewer_size.height()

        # 调整背景图像大小
        bg_height, bg_width = self.bg_image.shape[:2]

        # 使用背景缩放比例
        new_bg_width = int(bg_width * self.bg_scale)
        new_bg_height = int(bg_height * self.bg_scale)

        # 缩放背景图像
        bg_resized = cv2.resize(self.bg_image, (new_bg_width, new_bg_height))

        # 创建与预览区相同大小的背景
        background = np.zeros((viewer_height, viewer_width, 3), dtype=np.uint8)

        # 计算背景在预览区中的位置(居中显示)
        bg_x = (viewer_width - new_bg_width) // 2 + self.bg_position[0]
        bg_y = (viewer_height - new_bg_height) // 2 + self.bg_position[1]

        # 将背景图像放置到背景上
        y1_bg, y2_bg = max(0, bg_y), min(viewer_height, bg_y + new_bg_height)
        x1_bg, x2_bg = max(0, bg_x), min(viewer_width, bg_x + new_bg_width)

        # 计算背景图像中对应的区域
        y1_img = max(0, -bg_y)
        y2_img = y1_img + (y2_bg - y1_bg)
        x1_img = max(0, -bg_x)
        x2_img = x1_img + (x2_bg - x1_bg)

        # 确保索引有效
        if (y2_bg > y1_bg and x2_bg > x1_bg and
            y2_img > y1_img and x2_img > x1_img and
            y2_img <= new_bg_height and x2_img <= new_bg_width):

            # 将背景图像放置到背景上
            background[y1_bg:y2_bg, x1_bg:x2_bg] = bg_resized[y1_img:y2_img, x1_img:x2_img]

        # 调整前景图像大小
        fg_height, fg_width = self.fg_image.shape[:2]
        new_fg_width = int(fg_width * self.fg_scale)
        new_fg_height = int(fg_height * self.fg_scale)

        if new_fg_width > 0 and new_fg_height > 0:
            fg_resized = cv2.resize(self.fg_image, (new_fg_width, new_fg_height))
        else:
            fg_resized = self.fg_image.copy()
            new_fg_width = fg_width
            new_fg_height = fg_height

        # 使用保存的位置,不限制边界
        fg_x = self.fg_position[0]
        fg_y = self.fg_position[1]

        # 将前景与背景合成
        if fg_resized.shape[2] == 4:  # RGBA图像
            alpha = fg_resized[:, :, 3] / 255.0
            fg_rgb = fg_resized[:, :, :3]

            # 计算前景在背景中的可见区域
            y1_fg, y2_fg = max(0, fg_y), min(viewer_height, fg_y + new_fg_height)
            x1_fg, x2_fg = max(0, fg_x), min(viewer_width, fg_x + new_fg_width)

            # 计算前景图像中对应的区域
            y1_fg_img = max(0, -fg_y)
            y2_fg_img = y1_fg_img + (y2_fg - y1_fg)
            x1_fg_img = max(0, -fg_x)
            x2_fg_img = x1_fg_img + (x2_fg - x1_fg)

            # 确保索引有效
            if (y2_fg > y1_fg and x2_fg > x1_fg and
                y2_fg_img > y1_fg_img and x2_fg_img > x1_fg_img and
                y2_fg_img <= new_fg_height and x2_fg_img <= new_fg_width):

                # 裁剪前景图像
                fg_cropped = fg_rgb[y1_fg_img:y2_fg_img, x1_fg_img:x2_fg_img]
                alpha_cropped = alpha[y1_fg_img:y2_fg_img, x1_fg_img:x2_fg_img]

                # 合成
                for c in range(3):
                    background[y1_fg:y2_fg, x1_fg:x2_fg, c] = (
                        alpha_cropped * fg_cropped[:, :, c] +
                        (1 - alpha_cropped) * background[y1_fg:y2_fg, x1_fg:x2_fg, c]
                    )
        else:  # RGB图像
            # 直接复制(不考虑透明度)
            y1, y2 = max(0, fg_y), min(viewer_height, fg_y + new_fg_height)
            x1, x2 = max(0, fg_x), min(viewer_width, fg_x + new_fg_width)

            if y2 > y1 and x2 > x1:
                # 计算前景图像中对应的区域
                fg_y1 = max(0, -fg_y)
                fg_y2 = fg_y1 + (y2 - y1)
                fg_x1 = max(0, -fg_x)
                fg_x2 = fg_x1 + (x2 - x1)

                # 裁剪前景图像
                fg_cropped = fg_resized[fg_y1:fg_y2, fg_x1:fg_x2]
                background[y1:y2, x1:x2] = fg_cropped

        return background

    def reset(self):
        """重置交互状态"""
        self.fg_scale = 1.0
        self.fg_position = [0, 0]
        self.bg_scale = 1.0
        self.bg_position = [0, 0]
        self.is_dragging = False
        self.last_mouse_pos = None

class ColorButton(QPushButton):
    """自定义颜色按钮类,用于显示透明背景的棋盘格图案"""
    def __init__(self, color_name, color_value, parent=None):
        super().__init__(parent)
        self.color_name = color_name
        self.color_value = color_value
        self.setFixedSize(30, 30)
        self.setToolTip(color_name)
        self.hovered = False
        self.isclicked = color_name == '白色'

    def enterEvent(self, event):
        """鼠标进入"""
        self.hovered = True
        self.update()
        super().enterEvent(event)

    def leaveEvent(self, event):
        """鼠标离开"""
        self.hovered = False
        self.update()
        super().leaveEvent(event)

    def paintEvent(self, event):
        """绘制按钮"""
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        # 确定边框样式
        if self.hovered:
            # 悬停状态
            pen = QPen(QColor(100, 100, 100), 5)
        elif self.isclicked:
            # 选中状态
            pen = QPen(QColor(192, 192, 0), 3)
        else:
            # 正常状态 - 黑色细边框
            pen = QPen(Qt.black, 1)

        painter.setPen(pen)

        # 绘制背景
        if self.color_name == "透明":
            # 绘制棋盘格
            tile_size = 5
            for y in range(0, self.height(), tile_size):
                for x in range(0, self.width(), tile_size):
                    if (x // tile_size + y // tile_size) % 2 == 0:
                        painter.fillRect(x, y, tile_size, tile_size, Qt.lightGray)
                    else:
                        painter.fillRect(x, y, tile_size, tile_size, Qt.white)
        else:
            # 绘制纯色
            painter.fillRect(self.rect(), self.color_value)

        # 绘制边框
        painter.drawRect(1, 1, self.width()-3, self.height()-3)

        # # 如果按钮被按下(但不是选中状态),添加按下效果
        # if self.isDown() and not self.isChecked():
        #     painter.fillRect(self.rect(), QColor(0, 0, 0, 230))

    def mousePressEvent(self, event):
        """鼠标按下事件"""
        if event.button() == Qt.LeftButton:
            if self.color_name == '自定义':
                """选择自定义颜色 """
                color = QColorDialog.getColor()
                if color.isValid():
                    self.selected_color = (color.red(), color.green(), color.blue())
                    self.color_value = QColor(*self.selected_color)
                    self.setStyleSheet(f"background-color: rgb{self.selected_color};selection-background-color: rgb{self.selected_color};")
                    window.select_color(self.color_name, self.selected_color)
        super().mousePressEvent(event)

class PhotoCutoutApp(QMainWindow):
    """主应用程序窗口"""
    def __init__(self):
        super().__init__()
        self.photo_sizes = PhotoSizes()
        # 添加节流计时器
        self.info_update_timer = QTimer()
        self.info_update_timer.setSingleShot(True)
        self.info_update_timer.timeout.connect(self.process_pending_info)
        self.pending_info = []

        self.init_ui()
        self.current_image = None
        self.processed_image = None
        self.original_cutout = None  # 保存原始抠图结果
        self.image_path = None
        self.interactive_bg = InteractiveBackgroundReplacement()  # 交互式背景替换
        self.is_comparing = False  # 是否正在对比原始图像
        self.face_rect = None  # 存储检测到的人脸区域
        self.all_filter_parts = self.all_formats()
        self.selected_color = (255, 255, 255)  # 默认白色

    def throttle_append_info(self, text, delay=100):
        """节流的信息追加方法"""
        self.pending_info.append(text)

        # 限制信息条数,避免内存过度使用
        if len(self.pending_info) > 50:
            self.pending_info = self.pending_info[-30:]  # 保留最近30条

        if not self.info_update_timer.isActive():
            self.info_update_timer.start(delay)

    def process_pending_info(self):
        """处理积压的信息"""
        if self.pending_info:
            # 使用阻塞信号避免频繁更新
            self.info_text.blockSignals(True)

            for text in self.pending_info:
                self.info_text.append(text)

            # 滚动到底部
            scrollbar = self.info_text.verticalScrollBar()
            scrollbar.setValue(scrollbar.maximum())

            # 恢复信号
            self.info_text.blockSignals(False)

            self.pending_info = []

    def all_formats(self):
        # 获取所有支持的图像格式
        supported_formats = QImageReader.supportedImageFormats() # 返回 QByteArray 列表
        # 将 QByteArray 转换为字符串,并转换为大写(可选,以使过滤器中的格式显示更统一)
        format_strings = [fmt.data().decode().upper() for fmt in supported_formats]

        # 构建过滤器字符串
        filter_parts = []
        if format_strings:
            # 创建一个包含所有支持格式的过滤器
            all_images_filter = "常用图形文件 ("
            for fmt in format_strings:
                if fmt in ('BMP' ,'GIF' ,'JPEG' ,'JPG' ,'PNG' ,'TGA' ,'TIF' ,'TIFF' ,'WEBP' ):
                    all_images_filter += f"*.{fmt} "
            all_images_filter += ")"
            filter_parts.append(all_images_filter)

            # 也可以为每种格式创建单独的过滤器(可选)
            for fmt in format_strings:
                filter_parts.append(f"{fmt} 格式文件 (*.{fmt})")

        # # 添加一个"所有文件"过滤器
        # filter_parts.append("All files (*)")

        # 用两个分号连接所有过滤器
        return ";;".join(filter_parts)

    def init_ui(self):
        """初始化用户界面"""
        self.setWindowTitle("智能抠图、背景替换、人脸检测及证件照生成工具")
        self.setGeometry(100, 100, 1000, 700)

        # 创建中央部件
        central_widget = QWidget()
        self.setCentralWidget(central_widget)

        # 创建主布局
        main_layout = QHBoxLayout()
        central_widget.setLayout(main_layout)

        # 左侧:预览区
        left_layout = QVBoxLayout()
        self.image_viewer = ImageViewer()
        left_layout.addWidget(self.image_viewer)

        # 右侧:功能区(固定宽度)
        right_widget = QWidget()
        right_widget.setFixedWidth(300)  # 固定右侧功能区宽度
        right_layout = QVBoxLayout()
        right_widget.setLayout(right_layout)

        # 按钮区
        button_layout = QVBoxLayout()

        # 上传图像按钮
        self.upload_btn = QPushButton("上传图像")
        self.upload_btn.clicked.connect(self.upload_image)
        button_layout.addWidget(self.upload_btn)

        # 抠图方法选择
        method_layout = QHBoxLayout()
        method_layout.addWidget(QLabel("抠图方法:"))
        self.method_combo = QComboBox()
        if REMBG_AVAILABLE:
            self.method_combo.addItems(["智能抠图 (rembg)", "传统分割方法"])
        else:
            self.method_combo.addItems(["传统分割方法"])
            self.method_combo.setEnabled(False)
        button_layout.addLayout(method_layout)
        button_layout.addWidget(self.method_combo)

        # 处理按钮
        self.process_btn = QPushButton("开始抠图")
        self.process_btn.clicked.connect(self.process_image)
        self.process_btn.setEnabled(False)
        button_layout.addWidget(self.process_btn)

        # 进度条
        self.progress_bar = QProgressBar()
        self.progress_bar.setVisible(False)
        button_layout.addWidget(self.progress_bar)

        # 创建标签页
        self.tab_widget = QTabWidget()

        # 标签页2:证件照生成
        tab0 = QWidget()
        tab0_layout = QVBoxLayout()
        tab0.setLayout(tab0_layout)

        # 证件照生成
        id_photo_layout = QVBoxLayout()

        # 人脸检测按钮
        self.face_detect_btn = QPushButton("检测头像")
        self.face_detect_btn.clicked.connect(self.detect_faces)
        self.face_detect_btn.setEnabled(False)
        id_photo_layout.addWidget(self.face_detect_btn)

        # 背景色选择 - 改为颜色方块
        color_layout = QVBoxLayout()
        color_layout.addWidget(QLabel("背景色:"))

        # 颜色方块布局
        color_buttons_layout = QHBoxLayout()

        # 白色按钮
        self.white_btn = ColorButton("白色", Qt.white)
        self.white_btn.clicked.connect(lambda: self.select_color("白色", (255, 255, 255)))
        color_buttons_layout.addWidget(self.white_btn)

        # 红色按钮
        self.red_btn = ColorButton("红色", Qt.red)
        self.red_btn.clicked.connect(lambda: self.select_color("红色", (255, 0, 0)))
        color_buttons_layout.addWidget(self.red_btn)

        # 蓝色按钮
        self.blue_btn = ColorButton("蓝色", Qt.blue)
        self.blue_btn.clicked.connect(lambda: self.select_color("蓝色", (0, 0, 255)))
        color_buttons_layout.addWidget(self.blue_btn)

        # 透明按钮
        self.transparent_btn = ColorButton("透明", None)
        self.transparent_btn.clicked.connect(lambda: self.select_color("透明", None))
        color_buttons_layout.addWidget(self.transparent_btn)

        # 自定义按钮
        self.custom_btn = ColorButton("自定义", QColor(255,255,255))
        self.custom_btn.clicked.connect(lambda: self.select_color("自定义", (255,255,255)))
        color_buttons_layout.addWidget(self.custom_btn)

        # self.color_bottons = {"white": self.white_btn, "red": self.red_btn, "blue": self.blue_btn, "transparent": self.transparent_btn}
        self.color_bottons = {"白色": self.white_btn, "红色": self.red_btn, "蓝色": self.blue_btn, "透明": self.transparent_btn , "自定义" :self.custom_btn}

        color_layout.addLayout(color_buttons_layout)
        id_photo_layout.addLayout(color_layout)

        # 尺寸选择
        size_layout = QVBoxLayout()
        size_layout.addWidget(QLabel("证件照规格及尺寸:"))

        # 尺寸组合框
        size_combo_layout = QHBoxLayout()
        self.id_size_combo = QComboBox()
        self.id_size_combo.addItems(self.photo_sizes.get_names())
        self.id_size_combo.setMaxVisibleItems(len(self.photo_sizes.get_names()))
        self.id_size_combo.setCurrentIndex(1)  # 设置默认显示第二个项目(索引为1)       
        self.id_size_combo.currentIndexChanged.connect(self.on_size_combo_changed)
        size_combo_layout.addWidget(self.id_size_combo)
        size_layout.addLayout(size_combo_layout)

        # 自定义尺寸输入
        custom_size_layout = QHBoxLayout()
        custom_size_layout.addWidget(QLabel("宽:"))
        self.width_spinbox = QSpinBox()
        self.width_spinbox.setRange(50, 2000)
        self.width_spinbox.setValue(295)
        self.width_spinbox.valueChanged.connect(self.on_custom_size_changed)
        custom_size_layout.addWidget(self.width_spinbox)

        custom_size_layout.addWidget(QLabel("高:"))
        self.height_spinbox = QSpinBox()
        self.height_spinbox.setRange(50, 2000)
        self.height_spinbox.setValue(413)
        self.height_spinbox.valueChanged.connect(self.on_custom_size_changed)
        custom_size_layout.addWidget(self.height_spinbox)

        size_layout.addLayout(custom_size_layout)
        id_photo_layout.addLayout(size_layout)

        # 生成证件照按钮
        self.id_photo_btn = QPushButton("生成证件照")
        self.id_photo_btn.clicked.connect(self.generate_id_photo)
        self.id_photo_btn.setEnabled(False)
        id_photo_layout.addWidget(self.id_photo_btn)

        tab0_layout.addLayout(id_photo_layout)
        tab0_layout.addStretch()

        # 标签页1:背景操作
        tab1 = QWidget()
        tab1_layout = QVBoxLayout()
        tab1.setLayout(tab1_layout)

        # 背景替换
        self.bg_replace_btn = QPushButton("更换背景")
        self.bg_replace_btn.clicked.connect(self.replace_background)
        self.bg_replace_btn.setEnabled(False)
        tab1_layout.addWidget(self.bg_replace_btn)

        # 交互式背景替换控制
        self.bg_interactive_btn = QPushButton("启用交互模式")
        self.bg_interactive_btn.clicked.connect(self.toggle_interactive_mode)
        self.bg_interactive_btn.setEnabled(False)
        tab1_layout.addWidget(self.bg_interactive_btn)

        # 添加交互模式操作提示
        tip_font = QFont()
        tip_font.setPointSize(9)  # 使用较小的字体

        # 使用多行标签显示提示信息
        tip_label = QLabel("交互模式操作提示:\n• 鼠标 拖拽:改变前景图位置\n• 鼠标 滚轮:调整前景图像大小\n• Ctrl+滚轮:调整背景图像大小")
        tip_label.setFont(tip_font)
        tip_label.setStyleSheet("color: #666; margin: 5px 0 10px 0; padding: 5px; background-color: #f8f8f8; border-radius: 3px;")
        tip_label.setWordWrap(True)  # 允许文本换行
        tab1_layout.addWidget(tip_label)

        tab1_layout.addStretch()

        # 添加标签页
        self.tab_widget.addTab(tab0, "证件照生成")
        self.tab_widget.addTab(tab1, "背景操作")

        button_layout.addWidget(self.tab_widget)

        # 下载图像按钮
        self.download_btn = QPushButton("下载图像")
        self.download_btn.clicked.connect(self.download_image)
        self.download_btn.setEnabled(False)
        button_layout.addWidget(self.download_btn)

        # 重置按钮 - 用于返回到原始抠图结果
        self.reset_btn = QPushButton("重置到抠图结果")
        self.reset_btn.clicked.connect(self.reset_to_cutout)
        self.reset_btn.setEnabled(False)
        button_layout.addWidget(self.reset_btn)

        # 与上传图片对比按钮
        self.compare_btn = QPushButton("与上传图片对比")
        self.compare_btn.setCheckable(True)
        self.compare_btn.pressed.connect(self.show_original_image)
        self.compare_btn.released.connect(self.show_processed_image)
        self.compare_btn.setEnabled(False)
        button_layout.addWidget(self.compare_btn)

        # 交流按钮
        self.communication_btn = QPushButton("说明与交流")
        self.communication_btn.clicked.connect(self.show_communication_dialog)
        button_layout.addWidget(self.communication_btn)

        # 退出按钮
        self.exit_btn = QPushButton("退出")
        self.exit_btn.clicked.connect(self.close)
        button_layout.addWidget(self.exit_btn)

        right_layout.addLayout(button_layout)

        # 图像缩放信息
        self.scale_info_label = QLabel("缩放信息: 无")
        self.scale_info_label.setStyleSheet("background-color: #f8f8f8; padding: 5px; border: 1px solid #ddd;")
        right_layout.addWidget(self.scale_info_label)

        # 说明区
        info_layout = QVBoxLayout()
        info_layout.addWidget(QLabel("提示信息:"))

        self.info_text = QTextEdit()
        self.info_text.setReadOnly(True)
        # self.info_text.setMaximumHeight(200)
        info_layout.addWidget(self.info_text)

        right_layout.addLayout(info_layout)

        # 将左右布局添加到主布局
        main_layout.addWidget(self.image_viewer, 3)  # 预览区可缩放
        main_layout.addWidget(right_widget, 1)  # 右侧功能区固定宽度

        # 显示初始化信息
        if not REMBG_AVAILABLE:
            self.append_info_text("rembg库未安装,仅使用传统分割方法。\n如需更好的抠图效果,请安装rembg库。")
        else:
            self.append_info_text("程序已就绪,请上传图像开始处理。")

    def append_info_text(self, text):
        """追加信息并自动滚动到底部 - 优化版本"""
        # 使用阻塞信号来避免频繁更新
        self.info_text.blockSignals(True)

        # 追加新文本
        self.info_text.append(text)

        # 获取垂直滚动条并滚动到底部
        scrollbar = self.info_text.verticalScrollBar()
        scrollbar.setValue(scrollbar.maximum())

        # 恢复信号
        self.info_text.blockSignals(False)

    def select_color(self, color_name, color_value):
        """选择背景色"""
        for color_btn in self.color_bottons.values():
            color_btn.isclicked = color_btn.color_name == color_name
            color_btn.update()

        if color_name == "透明":
            self.selected_color = None
            self.throttle_append_info(f"已选择 透明 背景")
        elif color_name == "自定义":
            self.selected_color = color_value
            self.throttle_append_info(f"已选择 {color_name} 背景,颜色: RGB{self.selected_color}")
        else:
            self.selected_color = color_value
            self.throttle_append_info(f"已选择 {color_name} 背景")

    def on_size_combo_changed(self, index):
        """尺寸组合框改变时的处理"""
        # if index > 0:  # 不是"自定义"选项
        size_name = self.id_size_combo.currentText()
        width, height = self.photo_sizes.get_size(size_name)
        # 更新SpinBox的值,但不触发自定义尺寸改变事件
        self.width_spinbox.blockSignals(True)
        self.height_spinbox.blockSignals(True)
        self.width_spinbox.setValue(width)
        self.height_spinbox.setValue(height)
        self.width_spinbox.blockSignals(False)
        self.height_spinbox.blockSignals(False)

        # 更新信息
        current_info = self.info_text.toPlainText()
        current_info += f"\n已选择 {size_name} 分辨率: ({width}*{height})px"
        self.append_info_text(current_info)

    def on_custom_size_changed(self):
        """自定义尺寸改变时的处理 - 优化版本"""
        if not hasattr(self, '_size_change_timer'):
            self._size_change_timer = QTimer()
            self._size_change_timer.setSingleShot(True)
            self._size_change_timer.timeout.connect(self._process_size_change)

        self._size_change_timer.start(150)  # 150ms 防抖

    def _process_size_change(self):
        """实际处理尺寸改变"""
        self.id_size_combo.blockSignals(True)
        self.id_size_combo.setCurrentIndex(0)
        self.id_size_combo.blockSignals(False)

        width = self.width_spinbox.value()
        height = self.height_spinbox.value()
        self.photo_sizes.sizes['自定义'] = (width, height)
        self.throttle_append_info(f"已设置自定义尺寸: {width}*{height}")

    def update_scale_info(self):
        """更新缩放比例信息"""
        if self.interactive_bg.is_active:
            bg_scale = self.interactive_bg.bg_scale
            fg_scale = self.interactive_bg.fg_scale

            # 计算背景最小缩放比例
            if self.interactive_bg.viewer_size and self.interactive_bg.original_bg_size:
                viewer_width = self.interactive_bg.viewer_size.width()
                viewer_height = self.interactive_bg.viewer_size.height()
                bg_height, bg_width = self.interactive_bg.original_bg_size

                min_scale_x = viewer_width / bg_width
                min_scale_y = viewer_height / bg_height
                min_scale = min(min_scale_x, min_scale_y)

                scale_info = f"背景缩放:{bg_scale:.2f}x(最小:{min_scale:.2f}x),前景缩放:{fg_scale:.2f}x"
            else:
                scale_info = f"背景缩放:{bg_scale:.2f}x,前景缩放:{fg_scale:.2f}x"
        else:
            if self.image_viewer.pixmap() and not self.image_viewer.pixmap().isNull() :
                width = self.current_image.shape[1]
                display_size = self.image_viewer.pixmap().size()
                scale_info = f"缩放信息: {display_size.width() / width :.2f}x"
            else:
                scale_info = "缩放信息: 无"

        self.scale_info_label.setText(scale_info)

    def resizeEvent(self, event):
        """处理窗口大小变化事件""" #TODO 需要判断生成证件照按钮是否可用
        # 更新显示的图像
        if self.interactive_bg.is_active:
            self.update_interactive_preview()
        elif self.processed_image is not None:
            self.image_viewer.set_image(self.processed_image)
            self.update_scale_info()
        else:
            self.image_viewer.set_image(self.current_image)
            # 输出新的尺寸信息
            self.update_scale_info()

        super().resizeEvent(event)

    def upload_image(self):
        """上传图像"""
        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择图像", "",
            self.all_filter_parts
        )

        if file_path:
            self.image_path = file_path
            try:
                # 使用PIL读取图像,支持中文路径
                pil_image = Image.open(file_path)
                # 确保图像是RGB格式
                if pil_image.mode != 'RGB':
                    pil_image = pil_image.convert('RGB')
                # 直接转换为RGB数组
                self.current_image = np.array(pil_image)

                if self.current_image is not None:
                    # 直接显示RGB图像
                    self.image_viewer.set_image(self.current_image)
                    self.process_btn.setEnabled(True)
                    self.face_detect_btn.setEnabled(True)

                    # 显示图像信息
                    height, width = self.current_image.shape[:2]
                    file_size = os.path.getsize(file_path) / 1024  # KB
                    info_text = f"图像路径: {file_path}\n"
                    info_text += f"分辨率: {width} x {height}\n"
                    info_text += f"文件大小: {file_size:.2f} KB\n"
                    info_text += f"上传时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
                    self.append_info_text(info_text)

                    # 重置状态
                    self.processed_image = None
                    self.original_cutout = None
                    self.face_rect = None
                    self.download_btn.setEnabled(False)
                    self.bg_replace_btn.setEnabled(False)
                    self.id_photo_btn.setEnabled(False)
                    self.reset_btn.setEnabled(False)
                    self.compare_btn.setEnabled(False)
                    self.bg_interactive_btn.setEnabled(False)
                    self.interactive_bg.is_active = False

                    # 更新缩放信息
                    self.update_scale_info()
                else:
                    QMessageBox.warning(self, "错误", "无法读取图像文件")
            except Exception as e:
                QMessageBox.warning(self, "错误", f"无法读取图像文件: {str(e)}")

    def process_image(self):
        """处理图像(抠图)"""
        if self.image_path is None:
            return

        self.progress_bar.setVisible(True)
        self.progress_bar.setValue(0)

        method = "rembg" if self.method_combo.currentIndex() == 0 and REMBG_AVAILABLE else "traditional"

        self.processing_thread = ImageProcessingThread(self.image_path, method)
        self.processing_thread.progress_updated.connect(self.progress_bar.setValue)
        self.processing_thread.finished.connect(self.on_processing_finished)
        self.processing_thread.error_occurred.connect(self.on_processing_error)
        self.processing_thread.start()

    def detect_faces(self):
        """检测图像中的人脸"""
        if self.processed_image is None and self.current_image is None:
            QMessageBox.warning(self, "警告", "请先上传图像或进行抠图处理")
            return

        # 使用抠图结果或原始图像进行人脸检测
        image_to_detect = self.processed_image if self.processed_image is not None else self.current_image

        self.progress_bar.setVisible(True)
        self.progress_bar.setValue(0)

        self.face_detection_thread = FaceDetectionThread(image_to_detect)
        self.face_detection_thread.progress_updated.connect(self.progress_bar.setValue)
        self.face_detection_thread.finished.connect(self.on_face_detection_finished)
        self.face_detection_thread.error_occurred.connect(self.on_face_detection_error)
        self.face_detection_thread.start()

    def on_face_detection_finished(self, result):
        """人脸检测完成后的回调"""
        self.progress_bar.setVisible(False)

        face_count = result['face_count']
        faces = result['faces']

        if face_count == 0:
            QMessageBox.information(self, "人脸检测结果",
                                  "未检测到人脸。\n\n建议:\n1. 确保人脸清晰可见\n2. 尝试调整图像角度\n3. 或直接使用更换背景颜色功能")
            self.face_rect = None
        else:
            # 选择最大的人脸(按面积)
            largest_face = max(faces, key=lambda rect: rect[2] * rect[3])
            self.face_rect = largest_face

            # 在图像上绘制人脸框
            image_with_faces = self.processed_image.copy() if self.processed_image is not None else self.current_image.copy()

            # 如果是RGBA图像,转换为RGB用于显示
            if len(image_with_faces.shape) == 3 and image_with_faces.shape[2] == 4:
                image_with_faces = cv2.cvtColor(image_with_faces, cv2.COLOR_RGBA2RGB)

            x, y, w, h = largest_face
            cv2.rectangle(image_with_faces, (x, y), (x+w, y+h), (0, 255, 0), 2)
            cv2.putText(image_with_faces, f"Face: {w}x{h}", (x, y-10),
                       cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)

            self.image_viewer.set_image(image_with_faces)

            QMessageBox.information(self, "人脸检测结果",
                                  f"检测到 {face_count} 个人脸,已选择最大的一个。\n\n人脸大小: {w} x {h} 像素\n\n现在可以生成证件照,系统将自动根据头像大小进行调整。")

        # 更新信息
        current_info = self.info_text.toPlainText()
        current_info += f"\n人脸检测完成: 检测到 {face_count} 个人脸"
        self.append_info_text(current_info)

    def on_face_detection_error(self, error_msg):
        """人脸检测错误回调"""
        self.progress_bar.setVisible(False)
        QMessageBox.warning(self, "人脸检测错误", error_msg)
        current_info = self.info_text.toPlainText()
        current_info += f"\n人脸检测错误: {error_msg}"
        self.append_info_text(current_info)

    def on_processing_finished(self, result_image, processing_time):
        """处理完成后的回调"""
        self.processed_image = result_image
        self.original_cutout = result_image.copy()  # 保存原始抠图结果
        self.image_viewer.set_image(result_image)
        self.download_btn.setEnabled(True)
        self.bg_replace_btn.setEnabled(True)
        self.id_photo_btn.setEnabled(True)
        self.reset_btn.setEnabled(True)
        self.compare_btn.setEnabled(True)
        self.face_detect_btn.setEnabled(True)
        self.progress_bar.setVisible(False)

        # 更新信息
        current_info = self.info_text.toPlainText()
        current_info += f"\n处理时间: {processing_time:.2f} 秒"
        current_info += f"\n输出格式: RGBA (带透明通道)"
        self.append_info_text(current_info)

    def on_processing_error(self, error_msg):
        """处理错误回调"""
        self.progress_bar.setVisible(False)
        QMessageBox.warning(self, "处理错误", error_msg)
        current_info = self.info_text.toPlainText()
        current_info += f"\n错误: {error_msg}"
        self.append_info_text(current_info)

    def download_image(self):
        """下载当前显示的图像"""
        current_image = self.image_viewer.current_image_array
        if current_image is None:
            QMessageBox.warning(self, "警告", "没有可下载的图像")
            return

        # 使用UTF-8编码处理中文文件名
        file_path, _ = QFileDialog.getSaveFileName(
            self, "保存图像", "图像结果.png",
            "PNG图像 (*.png);;JPEG图像 (*.jpg);;所有文件 (*)"
        )

        if file_path:
            try:
                # 根据文件扩展名选择保存格式
                if file_path.lower().endswith('.jpg') or file_path.lower().endswith('.jpeg'):
                    # 对于JPEG,需要移除透明通道
                    if len(current_image.shape) == 3 and current_image.shape[2] == 4:
                        # RGBA -> RGB
                        rgb_image = cv2.cvtColor(current_image, cv2.COLOR_RGBA2RGB)
                        # 使用PIL保存,支持中文路径
                        pil_image = Image.fromarray(rgb_image)
                        pil_image.save(file_path, 'JPEG', quality=95)
                    else:
                        # 直接使用RGB图像
                        pil_image = Image.fromarray(current_image)
                        pil_image.save(file_path, 'JPEG', quality=95)
                else:
                    # 对于PNG,保存为RGBA或RGB
                    if len(current_image.shape) == 3 and current_image.shape[2] == 4:
                        # 使用PIL保存RGBA图像
                        pil_image = Image.fromarray(current_image, 'RGBA')
                        pil_image.save(file_path, 'PNG')
                    else:
                        # 使用PIL保存RGB图像
                        pil_image = Image.fromarray(current_image)
                        pil_image.save(file_path, 'PNG')

                QMessageBox.information(self, "成功", f"图像已保存到: {file_path}")
            except Exception as e:
                QMessageBox.warning(self, "保存错误", f"保存图像时出错: {str(e)}\n{traceback.format_exc()}")

    def replace_background(self):
        """更换背景"""
        if self.processed_image is None:
            return

        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择背景图像", "",
            self.all_filter_parts
        )

        if file_path:
            try:
                # 使用PIL读取背景图像,支持中文路径
                pil_bg = Image.open(file_path)
                # 确保背景图像是RGB格式
                if pil_bg.mode != 'RGB':
                    pil_bg = pil_bg.convert('RGB')
                # 直接转换为RGB数组
                bg_image = np.array(pil_bg)

                # 设置交互式背景替换
                self.interactive_bg.set_background(bg_image)
                self.interactive_bg.set_foreground(self.original_cutout if self.original_cutout is not None else self.processed_image)

                # 启用交互模式按钮
                self.bg_interactive_btn.setEnabled(True)

                # 更新预览
                self.update_interactive_preview()

                # 更新信息
                current_info = self.info_text.toPlainText()
                current_info += f"\n已加载背景图像,可使用交互模式调整前景后景大小及位置"
                self.append_info_text(current_info)

            except Exception as e:
                QMessageBox.warning(self, "错误", f"无法读取背景图像: {str(e)}")

    def toggle_interactive_mode(self):
        """切换交互模式"""
        if self.interactive_bg.is_active:
            # 应用当前交互结果
            composite = self.interactive_bg.get_composite_image(self.image_viewer.size())
            if composite is not None:
                self.processed_image = composite
                self.image_viewer.set_image(composite)

            self.interactive_bg.is_active = False
            self.bg_interactive_btn.setText("启用交互模式")

            # 更新信息
            current_info = self.info_text.toPlainText()
            current_info += f"\n已应用交互结果"
            self.append_info_text(current_info)
        else:
            # 启用交互模式
            self.interactive_bg.is_active = True
            self.bg_interactive_btn.setText("应用交互结果")

            # 更新信息
            current_info = self.info_text.toPlainText()
            current_info += f"\n已启用交互模式,可使用鼠标调整前景位置和大小"
            self.append_info_text(current_info)

            # 更新预览
            self.update_interactive_preview()

    def update_interactive_preview(self):
        """更新交互式预览"""
        if self.interactive_bg.is_active:
            composite = self.interactive_bg.get_composite_image(self.image_viewer.size())
            if composite is not None:
                self.image_viewer.set_image(composite)
                # 更新缩放信息
                self.update_scale_info()

    def mousePressEvent(self, event):
        """鼠标按下事件"""
        if self.interactive_bg.mouse_press_event(event, self.image_viewer.size()):
            self.update_interactive_preview()
        else:
            super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        """鼠标移动事件"""
        if self.interactive_bg.mouse_move_event(event):
            self.update_interactive_preview()
        else:
            super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        """鼠标释放事件"""
        if self.interactive_bg.mouse_release_event(event):
            self.update_interactive_preview()
        else:
            super().mouseReleaseEvent(event)

    def wheelEvent(self, event):
        """鼠标滚轮事件"""
        if self.interactive_bg.wheel_event(event):
            self.update_interactive_preview()
        else:
            super().wheelEvent(event)

    def generate_id_photo(self):
        """生成证件照"""
        if self.processed_image is None:
            return

        self.progress_bar.setVisible(True)
        self.progress_bar.setValue(0)

        # 获取尺寸
        if self.id_size_combo.currentIndex() == 0:  # 自定义尺寸
            width = self.width_spinbox.value()
            height = self.height_spinbox.value()
            size = "自定义"
            # 更新PhotoSizes中的自定义尺寸
            self.photo_sizes.sizes["自定义"] = (width, height)
        else:
            size = self.id_size_combo.currentText()

        # 获取背景色
        if self.selected_color is None:
            # 透明背景 - 这里需要特殊处理,暂时使用白色
            background_color = (255, 255, 255)
            # 注意:当前PhotoIDThread不支持透明背景,需要修改
        else:
            background_color = self.selected_color

        # 使用原始抠图结果生成证件照,而不是当前处理结果
        foreground = self.original_cutout if self.original_cutout is not None else self.processed_image

        # 如果没有检测到人脸,提示用户
        if self.face_rect is None:
            reply = QMessageBox.question(self, "未检测到人脸",
                                       "未检测到清晰的人脸。\n\n是否继续生成证件照?\n\n建议:\n- 点击\"否\"先使用\"检测头像\"功能\n- 或点击\"是\"直接生成证件照(可能效果不佳)",
                                       QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
            if reply == QMessageBox.No:
                self.progress_bar.setVisible(False)
                return

        self.id_photo_thread = PhotoIDThread(foreground, size, background_color, self.face_rect)
        self.id_photo_thread.progress_updated.connect(self.progress_bar.setValue)
        self.id_photo_thread.finished.connect(self.on_id_photo_finished)
        self.id_photo_thread.error_occurred.connect(self.on_processing_error)
        self.id_photo_thread.start()

    def on_id_photo_finished(self, result_image):
        """证件照生成完成后的回调"""
        self.processed_image = result_image
        self.image_viewer.set_image(result_image)
        self.interactive_bg.is_active = False
        self.progress_bar.setVisible(False)

        # 更新信息
        current_info = self.info_text.toPlainText()
        if self.face_rect is not None:
            current_info += f"\n已生成基于头像检测的证件照"
        else:
            current_info += f"\n已生成证件照(未使用头像检测)"
        self.append_info_text(current_info)
        # 更新缩放信息
        self.update_scale_info()

    def reset_to_cutout(self):
        """重置到原始抠图结果"""
        if self.original_cutout is not None:
            self.processed_image = self.original_cutout.copy()
            self.image_viewer.set_image(self.processed_image)

            # 更新信息
            current_info = self.info_text.toPlainText()
            current_info += f"\n已重置到原始抠图结果"
            self.append_info_text(current_info)

    def show_original_image(self):
        """显示原始上传图片"""
        if self.current_image is not None:
            self.image_viewer.set_image(self.current_image)

    def show_processed_image(self):
        """显示处理后的图片"""
        if self.processed_image is not None:
            self.image_viewer.set_image(self.processed_image)

    def show_communication_dialog(self):
        """显示交流对话框"""
        long_message="""
        软件概述
        本软件是一款功能强大的智能抠图、人脸检测、证件照生成及背景替换工具软件,可以智能抠出前景图,无论是物或人均能识别,可以更换背景。特别集成了人脸检测技术,能够自动识别图像中的人脸并生成专业证件照。软件提供直观的图形界面,操作简单便捷。

        核心功能
        1. 智能抠图
            采用先进的rembg库进行高质量的智能前景分割
            保持原始分辨率处理,确保图像质量
            支持多种抠图方法,推荐使用智能抠图

        2. 人脸检测与识别
            智能人脸检测:自动识别图像中的人脸位置和大小
            多脸处理:当图像中有多个人脸时,自动选择面积最大的人脸
            可视化反馈:在检测到的人脸周围绘制绿色边框,并显示人脸尺寸信息

        3. 证件照生成
            多样化选择:支持多种标准证件照尺寸和背景颜色
            智能裁剪:根据检测到的人脸位置,自动裁剪包含头部和肩部的区域
            自适应缩放:根据头像大小自动调整证件照中的头像比例
            优化布局:确保头像在证件照中位置合适、大小恰当

        4. 背景替换
            灵活背景:可以将抠出的前景放置在任何背景图像上
            大小可变:鼠标滚轮:调整前景图像大小;Ctrl+滚轮:调整背景图像大小

        5. 实时预览与信息显示
            图像预览:实时查看处理结果
            参数显示:显示图像基本信息和处理时间

        使用指南
        基本操作流程
       上传图像
        ┗━开始抠图
            ┣━检测头像
            ┃   ┣━选择颜色━┓
            ┃   ┗━规格尺寸━┫
            ┃        生成证件照━┓
            ┗━背景替换         ┃
                ┗━调整大小和位置━━┫  
                           ┃
                          下载图像              
                    
            上传图像:点击"上传图像"按钮选择要处理的图片,支持常见的多数图形文件格式
            开始抠图:点击"开始抠图"按钮进行智能前景分割
            检测头像:点击"检测头像"按钮识别图像中的人脸
            生成证件照:系统会自动根据检测到的头像生成合适的证件照
            背景替换:将抠出的前景图像放置在任何背景图像上,大小和位置可调
            下载图像:系统会根据设定的大小缩放图像,保存时可以选择格式和文件名

        特殊情况处理
            未检测到头像:系统会提供清晰的提示信息和操作建议
            选择性操作:即使未检测到头像,您仍然可以选择继续生成证件照或更换背景颜色

        证件照生成注意事项
            在拍摄照片用于证件照时,需要注意以下几点:
            1. 应在光线充足场合,面向镜头且头部四周空间要预留充分。
            2. 背景应为均匀的颜色,尽量避免杂乱颜色。
            3. 姿势端正、头部正直,确保脸部清晰可见,不应被头发或其他物品遮挡。
            4. 不戴首饰,嘴唇合拢,露出耳朵眉毛。对于女性,不应涂抹口红或其他可能影响真实面貌的化妆色彩。
            5. 穿有领深色衣服,避免穿着制式服装、背带式服装或过于花哨的衣服,衣服颜色不要和背景色过于相近。
            6. 对于经常戴眼镜的人,拍摄时确保镜框不得遮住眼睛和眉毛;不能佩戴墨镜。
            7. 照片应为正面免冠头像,不得戴帽子或头饰。

        技术特点
        高精度处理
            使用成熟的Haar级联分类器进行人脸检测,准确度高
            智能裁剪算法确保包含完整的头部和部分肩部

        用户体验优化
            直观的GUI界面设计,操作简单易懂
            清晰的视觉反馈和操作指引
            完善的错误处理机制,提供友好的提示信息

        注意事项
            每一次运行时,会从网上下载相应支持库,大约200M,请耐心等待
            为确保最佳效果,请上传清晰、光线充足的人像照片
            处理过程中请勿关闭程序窗口
            抠图会保持原始图像分辨率,后续生成的证件照也根据情况调整分辨率

        技术支持与反馈
            如在使用过程中遇到问题或有改进建议,请通过以下方式联系我们:
            微信号: yikehongxin6666
            CSDN博客: https://blog.csdn.net/weixin_69832035

        """
        # 创建并显示对话框
        dialog = CustomInfoDialog(message=long_message,title='软件使用说明与交流',width=800,height=800)
        result = dialog.exec_()

class CustomInfoDialog(QDialog):
    def __init__(self, parent=None, message="", title='信息提示', website_url="https://blog.csdn.net/weixin_69832035", width=400, height=200):
        super().__init__(parent)
        self.title = title
        self.width = width
        self.height = height
        self.website_url = website_url
        self.setup_ui(message)

    def setup_ui(self, message):
        self.setWindowTitle(self.title)
        self.setMinimumSize(self.width, self.height)
        self.setModal(True)

        # 获取屏幕高度并设置最大高度
        screen_geometry = QApplication.primaryScreen().availableGeometry()
        max_height = screen_geometry.height() - 80
        self.setMaximumHeight(max_height)

        main_layout = QVBoxLayout()
        main_layout.setSpacing(20)
        main_layout.setContentsMargins(20, 20, 20, 20)

        # 添加标题(如果标题不为空)
        if self.title:
            title_label = QLabel(self.title)
            title_label.setAlignment(Qt.AlignCenter)
            title_font = QFont()
            title_font.setPointSize(14)
            title_font.setBold(True)
            title_label.setFont(title_font)
            main_layout.addWidget(title_label)

        content_layout = QHBoxLayout()

        # 图标
        icon_label = QLabel()
        try:
            icon = self.style().standardIcon(self.style().SP_MessageBoxInformation)
            icon_label.setPixmap(icon.pixmap(48, 48))
        except:
            pixmap = QPixmap(48, 48)
            pixmap.fill(Qt.blue)
            icon_label.setPixmap(pixmap)

        # 创建滚动区域
        scroll_area = QScrollArea()
        scroll_area.setWidgetResizable(True)
        scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
        scroll_area.setFrameShape(QScrollArea.NoFrame)

        # 消息标签放在滚动区域内
        message_label = QLabel(message)
        message_label.setWordWrap(True)
        message_label.setFont(QFont("Arial", 10))
        message_label.setTextInteractionFlags(Qt.TextSelectableByMouse)

        # 创建滚动区域的widget
        scroll_widget = QWidget()
        scroll_layout = QVBoxLayout(scroll_widget)
        scroll_layout.addWidget(message_label)
        scroll_layout.setContentsMargins(0, 0, 0, 0)

        scroll_area.setWidget(scroll_widget)

        content_layout.addWidget(icon_label)
        content_layout.addSpacing(15)
        content_layout.addWidget(scroll_area, 1)  # 将滚动区域添加到布局

        # 按钮
        button_layout = QHBoxLayout()
        button_layout.addStretch(1)

        self.website_btn = QPushButton("访问网页")
        self.website_btn.setFixedSize(100, 30)
        self.website_btn.clicked.connect(self.open_website)

        self.cancel_btn = QPushButton("取消")
        self.cancel_btn.setFixedSize(100, 30)
        self.cancel_btn.clicked.connect(self.reject)

        button_layout.addWidget(self.website_btn)
        button_layout.addSpacing(10)
        button_layout.addWidget(self.cancel_btn)

        main_layout.addLayout(content_layout, 1)  # 给内容区域设置伸展因子
        main_layout.addLayout(button_layout)
        self.setLayout(main_layout)

        self.cancel_btn.setDefault(True)
        self.adjustSize()

    def open_website(self):
        QDesktopServices.openUrl(QUrl(self.website_url))
        self.accept()

def main():
    global window

    """主函数"""
    app = QApplication(sys.argv)

    # 设置应用程序信息
    app.setApplicationName("智能抠图、人脸检测、生成证件照及背景替换工具")
    app.setApplicationVersion("1.1")

    # 创建并显示主窗口
    window = PhotoCutoutApp()
    window.show()

    # 启动应用程序事件循环
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐