吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 11002|回复: 182
收起左侧

[Python 原创] 学籍照片批量处理工具

    [复制链接]
ORZtester 发表于 2025-11-22 11:41
本帖最后由 ORZtester 于 2025-11-22 19:14 编辑

新人发帖,如格式有问题请大家多多包涵小程序是根据前辈   中小学学籍照片自动批量剪裁脚本 - 吾爱破解 - 52pojie.cn  帖子中的原始代码使用AI制作的,添加了一些功能,使用AI一并写好了打包代码。
①现在可以智能化识别人脸后定位剪裁了
②可以设置想要的分辨率了
③可以对生成的图片文件大小进行自定义限制了
④对于原图片分辨率某一参数小于设定值的处理效果还不理想,本人小白一个,大佬们如有兴趣可以直接动手解决



我使用的是auto-py-to-exe打包,需要注意设置重要文件(xml那个),我放在了压缩包里了,设置如图:



https://www.123865.com/s/OV5A-OWkAd?pwd=52pj#     ←这是打包好的文件,如果无法运行还得大佬们亲自动手打包[/url]
https://www.123865.com/s/OV5A-VWkAd?pwd=52pj#      ←这是9楼大佬优化过的文件,建议使用这个~~
重新排列了一下图片,麻烦审核老师了
希望大佬多多指点!感谢您的回复

以下是源码,我直接发出来:
[Python] 纯文本查看 复制代码
import sys
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import cv2
import numpy as np
import shutil
import threading
import urllib.request
import traceback
import gc
import time

# 版本信息
APP_VERSION = "学籍-V1.3.2"  # 更新版本号
APP_NAME = "照片批处理工具"


# 打包后资源文件路径处理
def resource_path(relative_path):
    """获取打包后资源的绝对路径"""
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)


class PhotoProcessorApp:
    def __init__(self, root):
        self.root = root
        self.root.title(f"{APP_NAME} {APP_VERSION}")
        self.root.geometry("800x800")
        self.root.resizable(True, True)

        # 设置图标
        try:
            icon_path = resource_path("icon.ico")
            if os.path.exists(icon_path):
                self.root.iconbitmap(icon_path)
        except:
            pass

        # 初始化变量
        self.input_folder = tk.StringVar()
        self.output_folder = tk.StringVar()
        self.target_width = tk.IntVar(value=413)
        self.target_height = tk.IntVar(value=579)
        self.extend_ratio = tk.DoubleVar(value=2.0)
        self.output_format = tk.StringVar(value="JPG")
        self.limit_file_size = tk.BooleanVar(value=False)
        self.max_file_size_kb = tk.IntVar(value=50)
        self.is_processing = False
        self.total_files = 0
        self.processed_files = 0
        # 在类初始化中添加可配置的阈值
        self.small_face_threshold = 0.05  # 小于5%视为小人像
        self.large_face_threshold = 0.3  # 大于30%视为大人像
        # 性能优化相关变量
        self.log_queue = []
        self.log_batch_size = 10
        self.log_counter = 0
        self.cancel_processing = False
        self._last_percentage = -1

        self.setup_fonts()
        self.setup_ui()

        # 初始化人脸检测器
        self.face_cascade = self.get_face_detector()
        if self.face_cascade is None or self.face_cascade.empty():
            messagebox.showwarning("警告", "人脸检测器加载失败,部分功能可能受限")

    def setup_fonts(self):
        """设置中文字体"""
        chinese_fonts = ["Microsoft YaHei", "SimHei", "SimSun", "KaiTi", "FangSong"]
        self.title_font = None
        self.normal_font = None

        test_label = ttk.Label(self.root, text="测试")
        for font in chinese_fonts:
            try:
                test_label.config(font=(font, 12))
                self.root.update()
                self.title_font = (font, 16, "bold")
                self.normal_font = (font, 9)
                break
            except:
                continue

        if self.title_font is None:
            self.title_font = ("TkDefaultFont", 16, "bold")
            self.normal_font = ("TkDefaultFont", 9)
        test_label.destroy()

    def setup_ui(self):
        # 创建主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)

        # 标题
        title_label = ttk.Label(main_frame, text=f"{APP_NAME} {APP_VERSION}", font=("Arial", 16, "bold"))
        title_label.grid(row=0, column=0, columnspan=3, pady=(0, 10))

        # 软件说明
        help_frame = ttk.LabelFrame(main_frame, text="CXG", padding="5")
        help_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
        help_frame.columnconfigure(0, weight=1)

        help_text = tk.Text(help_frame, height=4, wrap=tk.WORD, font=("Arial", 9))
        help_text.grid(row=0, column=0, sticky=(tk.W, tk.E))

        help_content = """使用说明:
1. 选择包含原始照片的输入文件夹
2. 选择处理后的照片保存的输出文件夹
3. 设置目标尺寸和输出格式
4. 如需限制文件大小,请启用并设置最大值
5. 点击"开始处理"按钮
6. 处理过程中可以点击"取消处理"中断操作
Cui2025
"""
        help_text.insert(tk.END, help_content)
        help_text.config(state=tk.DISABLED)

        help_scroll = ttk.Scrollbar(help_frame, command=help_text.yview)
        help_scroll.grid(row=0, column=1, sticky=(tk.N, tk.S))
        help_text.config(yscrollcommand=help_scroll.set)

        # 输入文件夹选择
        ttk.Label(main_frame, text="输入文件夹:").grid(row=2, column=0, sticky=tk.W, pady=5)
        ttk.Entry(main_frame, textvariable=self.input_folder, width=50).grid(row=2, column=1, sticky=(tk.W, tk.E),
                                                                             pady=5, padx=(5, 5))
        ttk.Button(main_frame, text="浏览", command=self.browse_input_folder).grid(row=2, column=2, pady=5)

        # 输出文件夹选择
        ttk.Label(main_frame, text="输出文件夹:").grid(row=3, column=0, sticky=tk.W, pady=5)
        ttk.Entry(main_frame, textvariable=self.output_folder, width=50).grid(row=3, column=1, sticky=(tk.W, tk.E),
                                                                              pady=5, padx=(5, 5))
        ttk.Button(main_frame, text="浏览", command=self.browse_output_folder).grid(row=3, column=2, pady=5)

        # 参数设置
        params_frame = ttk.LabelFrame(main_frame, text="处理参数", padding="10")
        params_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
        params_frame.columnconfigure(1, weight=1)

        # 目标尺寸
        ttk.Label(params_frame, text="输出宽度(像素):").grid(row=0, column=0, sticky=tk.W, pady=5)
        ttk.Entry(params_frame, textvariable=self.target_width, width=10).grid(row=0, column=1, sticky=tk.W, pady=5,
                                                                               padx=(5, 0))
        ttk.Label(params_frame, text="输出高度(像素):").grid(row=0, column=2, sticky=tk.W, pady=5, padx=(20, 0))
        ttk.Entry(params_frame, textvariable=self.target_height, width=10).grid(row=0, column=3, sticky=tk.W, pady=5,
                                                                                padx=(5, 0))

        # 扩展比例
        ttk.Label(params_frame, text="扩展比例:").grid(row=1, column=0, sticky=tk.W, pady=5)
        scale_frame = ttk.Frame(params_frame)
        scale_frame.grid(row=1, column=1, columnspan=3, sticky=(tk.W, tk.E), pady=5)
        ttk.Entry(scale_frame, textvariable=self.extend_ratio, width=10).grid(row=0, column=0, sticky=tk.W,
                                                                              padx=(0, 10))
        ttk.Label(scale_frame, text="(推荐值: 1.5-3.0)").grid(row=0, column=1, sticky=tk.W)

        # 输出格式选择
        ttk.Label(params_frame, text="输出格式:").grid(row=2, column=0, sticky=tk.W, pady=5)
        format_frame = ttk.Frame(params_frame)
        format_frame.grid(row=2, column=1, columnspan=3, sticky=(tk.W, tk.E), pady=5)
        format_combo = ttk.Combobox(format_frame, textvariable=self.output_format, values=["JPG", "PNG", "BMP"],
                                    state="readonly", width=10)
        format_combo.grid(row=0, column=0, sticky=tk.W)
        ttk.Label(format_frame, text="(选择输出图片的格式)").grid(row=0, column=1, sticky=tk.W, padx=(10, 0))

        # 文件大小限制设置
        ttk.Label(params_frame, text="文件大小限制:").grid(row=3, column=0, sticky=tk.W, pady=5)
        size_limit_frame = ttk.Frame(params_frame)
        size_limit_frame.grid(row=3, column=1, columnspan=3, sticky=(tk.W, tk.E), pady=5)
        ttk.Checkbutton(size_limit_frame, text="启用", variable=self.limit_file_size).grid(row=0, column=0, sticky=tk.W)
        ttk.Entry(size_limit_frame, textvariable=self.max_file_size_kb, width=8).grid(row=0, column=1, sticky=tk.W,
                                                                                      padx=(10, 5))
        ttk.Label(size_limit_frame, text="KB (仅对JPG/PNG有效)").grid(row=0, column=2, sticky=tk.W)

        # 处理按钮和取消按钮
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=5, column=0, columnspan=3, pady=20)
        button_frame.columnconfigure(0, weight=1)
        button_frame.columnconfigure(1, weight=1)

        self.process_btn = ttk.Button(button_frame, text="开始处理", command=self.start_processing)
        self.process_btn.grid(row=0, column=0, padx=(0, 10))

        self.cancel_btn = ttk.Button(button_frame, text="取消处理", command=self.cancel_processing_func, state="disabled")
        self.cancel_btn.grid(row=0, column=1)

        # 进度条
        self.progress = ttk.Progressbar(main_frame, mode='determinate')
        self.progress.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
        self.progress_label = ttk.Label(main_frame, text="0%")
        self.progress_label.grid(row=6, column=2, sticky=tk.E, padx=5)

        # 状态标签
        self.status_label = ttk.Label(main_frame, text="准备就绪")
        self.status_label.grid(row=7, column=0, columnspan=3, pady=5)

        # 日志文本框
        log_frame = ttk.LabelFrame(main_frame, text="处理日志", padding="5")
        log_frame.grid(row=8, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
        log_frame.columnconfigure(0, weight=1)
        log_frame.rowconfigure(0, weight=1)
        text_scroll = ttk.Scrollbar(log_frame)
        text_scroll.grid(row=0, column=1, sticky=(tk.N, tk.S))
        self.log_text = tk.Text(log_frame, height=10, yscrollcommand=text_scroll.set)
        self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        text_scroll.config(command=self.log_text.yview)
        main_frame.rowconfigure(8, weight=1)

    def browse_input_folder(self):
        folder = filedialog.askdirectory(title="选择输入文件夹")
        if folder:
            self.input_folder.set(folder)

    def browse_output_folder(self):
        folder = filedialog.askdirectory(title="选择输出文件夹")
        if folder:
            self.output_folder.set(folder)

    def log(self, message):
        """批量处理日志更新,减少UI刷新频率"""
        self.log_queue.append(message)
        self.log_counter += 1

        # 批量更新或重要消息立即更新
        if (self.log_counter >= self.log_batch_size or
                any(keyword in message for keyword in ["错误", "失败", "警告", "完成", "取消", "超时"])):
            self.flush_log_queue()

    def flush_log_queue(self):
        """将积压的日志一次性更新到UI"""
        if not self.log_queue:
            return

        # 一次性插入所有日志
        log_text = "\n".join(self.log_queue) + "\n"
        self.log_text.insert(tk.END, log_text)
        self.log_text.see(tk.END)

        # 清空队列和计数器
        self.log_queue = []
        self.log_counter = 0

        # 只在这里更新UI
        self.root.update_idletasks()

    def update_status(self, message):
        self.status_label.config(text=message)
        # 状态更新不频繁,可以直接更新UI
        self.root.update_idletasks()

    def update_progress(self, value):
        """批量更新进度,减少UI刷新"""
        self.progress['value'] = value

        # 只在进度变化较大时更新百分比
        percentage = int((value / self.total_files) * 100) if self.total_files > 0 else 0
        current_percentage = getattr(self, '_last_percentage', -1)

        if percentage != current_percentage or value == self.total_files or value == 0:
            self.progress_label.config(text=f"{percentage}%")
            self._last_percentage = percentage

            # 只在百分比变化时或处理完成时更新UI
            if percentage % 10 == 0 or value == self.total_files or value == 0:
                self.flush_log_queue()  # 顺便刷新日志

    def cancel_processing_func(self):
        """取消处理"""
        self.cancel_processing = True
        self.cancel_btn.config(state="disabled")
        self.log("正在取消处理,请等待...")
        self.flush_log_queue()

    def start_processing(self):
        if self.is_processing:
            return

        if not self.input_folder.get():
            messagebox.showerror("错误", "请选择输入文件夹")
            return
        if not self.output_folder.get():
            messagebox.showerror("错误", "请选择输出文件夹")
            return
        if not os.path.exists(self.input_folder.get()):
            messagebox.showerror("错误", "输入文件夹不存在")
            return

        if self.limit_file_size.get():
            if self.max_file_size_kb.get() <= 0:
                messagebox.showerror("错误", "文件大小限制必须大于0KB")
                return
            if self.output_format.get() == "BMP":
                messagebox.showwarning("警告", "BMP格式无法压缩")

        # 开始处理
        self.is_processing = True
        self.cancel_processing = False  # 重置取消标志
        self.process_btn.config(state="disabled")
        self.cancel_btn.config(state="normal")  # 启用取消按钮
        self.progress['value'] = 0
        self.progress_label.config(text="0%")
        self.log_text.delete(1.0, tk.END)
        self._last_percentage = -1  # 重置进度百分比

        thread = threading.Thread(target=self.process_images)
        thread.daemon = True
        thread.start()

    def get_face_detector(self):
        cascade_paths = []
        try:
            builtin_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
            if os.path.exists(builtin_path):
                cascade_paths.append(builtin_path)
        except:
            pass

        try:
            resource_cascade_path = resource_path('haarcascade_frontalface_default.xml')
            if os.path.exists(resource_cascade_path):
                cascade_paths.append(resource_cascade_path)
        except:
            pass

        try:
            current_dir_path = "haarcascade_frontalface_default.xml"
            if os.path.exists(current_dir_path):
                cascade_paths.append(current_dir_path)
        except:
            pass

        for cascade_path in cascade_paths:
            try:
                face_cascade = cv2.CascadeClassifier(cascade_path)
                if not face_cascade.empty():
                    self.log(f"成功加载人脸检测器: {cascade_path}")
                    return face_cascade
            except Exception as e:
                self.log(f"加载人脸检测器失败 {cascade_path}: {e}")

        try:
            self.log("尝试下载人脸检测模型...")
            url = "https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_default.xml"
            download_path = resource_path("haarcascade_frontalface_default.xml")
            if not os.path.exists(download_path):
                urllib.request.urlretrieve(url, download_path)
                self.log("人脸检测模型下载完成")
            face_cascade = cv2.CascadeClassifier(download_path)
            if not face_cascade.empty():
                self.log("使用下载的人脸检测模型")
                return face_cascade
        except Exception as e:
            self.log(f"下载人脸检测模型失败: {e}")

        self.log("警告: 无法加载任何人脸检测器")
        return None

    def get_background_color(self, image):
        """获取图像背景颜色,兼容彩色和灰度图像"""
        h, w = image.shape[:2]

        # 检查图像是彩色还是灰度
        is_color = len(image.shape) == 3 and image.shape[2] == 3

        # 确保有足够的边缘区域
        edge_size = min(5, h, w)

        try:
            # 获取各边缘区域
            top_edge = image[0:edge_size, :]
            bottom_edge = image[h - edge_size:h, :]
            left_edge = image[:, 0:edge_size]
            right_edge = image[:, w - edge_size:w]

            # 根据图像类型处理边缘
            if is_color:
                # 彩色图像 - 重塑为 (像素数, 3)
                edges_list = []

                if top_edge.size > 0:
                    top_flat = top_edge.reshape(-1, 3)
                    edges_list.append(top_flat)

                if bottom_edge.size > 0:
                    bottom_flat = bottom_edge.reshape(-1, 3)
                    edges_list.append(bottom_flat)

                if left_edge.size > 0:
                    left_flat = left_edge.reshape(-1, 3)
                    edges_list.append(left_flat)

                if right_edge.size > 0:
                    right_flat = right_edge.reshape(-1, 3)
                    edges_list.append(right_flat)
            else:
                # 灰度图像 - 重塑为 (像素数, 1)
                edges_list = []

                if top_edge.size > 0:
                    top_flat = top_edge.reshape(-1, 1)
                    edges_list.append(top_flat)

                if bottom_edge.size > 0:
                    bottom_flat = bottom_edge.reshape(-1, 1)
                    edges_list.append(bottom_flat)

                if left_edge.size > 0:
                    left_flat = left_edge.reshape(-1, 1)
                    edges_list.append(left_flat)

                if right_edge.size > 0:
                    right_flat = right_edge.reshape(-1, 1)
                    edges_list.append(right_flat)

            # 检查是否有有效的边缘数据
            if not edges_list:
                # 返回默认背景色(白色或灰色)
                return np.array([255, 255, 255], dtype=np.uint8) if is_color else np.array([255], dtype=np.uint8)

            # 合并所有边缘像素
            edges = np.vstack(edges_list)

            # 计算平均颜色
            avg_color = np.mean(edges, axis=0)

            # 确保返回正确的格式
            if is_color:
                return avg_color.astype(np.uint8)
            else:
                # 灰度图像返回单值,但转换为三通道用于彩色背景
                gray_value = avg_color.astype(np.uint8)[0]
                return np.array([gray_value, gray_value, gray_value], dtype=np.uint8)

        except Exception as e:
            self.log(f"获取背景颜色时出错: {e}, 使用默认白色背景")
            return np.array([255, 255, 255], dtype=np.uint8)

    def resize_to_target(self, image, face_rect, target_size):
        """根据人脸大小智能调整图像到目标尺寸"""
        target_width, target_height = target_size
        h, w = image.shape[:2]

        # 计算人脸在图像中的占比
        x, y, face_w, face_h = face_rect
        face_area = face_w * face_h
        image_area = w * h
        face_ratio = face_area / image_area

        self.log(f"人脸占比: {face_ratio:.3f} ({face_w}x{face_h} / {w}x{h})")

        # 根据人脸占比决定处理策略
        if face_ratio < 0.05:  # 人脸很小,需要放大
            # 计算放大比例,使人脸大小合适
            # 目标人脸大小约为图像的15-20%
            target_face_ratio = 0.18
            scale = (target_face_ratio / face_ratio) ** 0.5  # 开方使缩放更温和

            # 限制最大缩放倍数
            scale = min(scale, 3.0)  # 最多放大3倍

            new_w = int(w * scale)
            new_h = int(h * scale)

            # 使用高质量的插值方法放大图像
            enlarged = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_CUBIC)

            # 计算以人脸为中心的新裁剪区域
            face_center_x = int((x + face_w / 2) * scale)
            face_center_y = int((y + face_h / 2) * scale)

            # 从放大后的图像以人脸为中心裁剪目标尺寸
            start_x = max(0, face_center_x - target_width // 2)
            start_y = max(0, face_center_y - target_height // 2)
            end_x = start_x + target_width
            end_y = start_y + target_height

            # 确保不越界
            if end_x > new_w:
                end_x = new_w
                start_x = end_x - target_width
            if end_y > new_h:
                end_y = new_h
                start_y = end_y - target_height

            result = enlarged[start_y:end_y, start_x:end_x]
            self.log(f"小人像: 放大 {scale:.2f} 倍并以人脸为中心裁剪")

        elif face_ratio > 0.3:  # 人脸很大,可能需要缩小
            # 计算缩小比例
            scale = min(target_width / w, target_height / h)
            new_w = int(w * scale)
            new_h = int(h * scale)

            # 缩小图像
            resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA)

            # 使用背景色填充
            bg_color = self.get_background_color(image)
            result = np.full((target_height, target_width, 3), bg_color, dtype=np.uint8)
            y_offset = (target_height - new_h) // 2
            x_offset = (target_width - new_w) // 2
            result[y_offset:y_offset + new_h, x_offset:x_offset + new_w] = resized
            self.log(f"大人像: 缩小 {scale:.2f} 倍并填充背景")

        else:  # 人脸大小适中,使用标准处理
            scale = min(target_width / w, target_height / h)
            new_w = int(w * scale)
            new_h = int(h * scale)
            resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA)
            bg_color = self.get_background_color(image)
            result = np.full((target_height, target_width, 3), bg_color, dtype=np.uint8)
            y_offset = (target_height - new_h) // 2
            x_offset = (target_width - new_w) // 2
            result[y_offset:y_offset + new_h, x_offset:x_offset + new_w] = resized
            self.log(f"标准处理: 调整尺寸并居中")

        return result

    def get_original_file_size(self, file_path):
        try:
            return os.path.getsize(file_path) / 1024
        except:
            return None

    def compress_to_target_size(self, image, target_size_kb, output_format, original_size_kb=None):
        max_size_bytes = target_size_kb * 1024

        # 如果原始文件已经小于目标大小,直接使用高质量编码
        if original_size_kb is not None and original_size_kb <= target_size_kb:
            self.log(f"原始文件大小 {original_size_kb:.1f}KB 已满足要求,使用高质量编码")
            if output_format == "JPG":
                encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 95]
                success, encoded_image = cv2.imencode('.jpg', image, encode_param)
                if success:
                    return encoded_image, len(encoded_image) / 1024, 95, False
            elif output_format == "PNG":
                encode_param = [int(cv2.IMWRITE_PNG_COMPRESSION), 1]
                success, encoded_image = cv2.imencode('.png', image, encode_param)
                if success:
                    return encoded_image, len(encoded_image) / 1024, 1, False

        if output_format == "JPG":
            qualities = [95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10]
            for quality in qualities:
                encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
                success, encoded_image = cv2.imencode('.jpg', image, encode_param)
                if success and len(encoded_image) <= max_size_bytes:
                    file_size_kb = len(encoded_image) / 1024
                    self.log(f"JPG质量{quality}%,文件大小: {file_size_kb:.1f}KB")
                    return encoded_image, file_size_kb, quality, True
            encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 10]
            success, encoded_image = cv2.imencode('.jpg', image, encode_param)
            file_size_kb = len(encoded_image) / 1024
            self.log(f"使用最低质量10%,文件大小: {file_size_kb:.1f}KB")
            return encoded_image, file_size_kb, 10, True

        elif output_format == "PNG":
            compression_levels = [1, 3, 5, 7, 9]
            for level in compression_levels:
                encode_param = [int(cv2.IMWRITE_PNG_COMPRESSION), level]
                success, encoded_image = cv2.imencode('.png', image, encode_param)
                if success and len(encoded_image) <= max_size_bytes:
                    file_size_kb = len(encoded_image) / 1024
                    self.log(f"PNG压缩级别{level},文件大小: {file_size_kb:.1f}KB")
                    return encoded_image, file_size_kb, level, True
            encode_param = [int(cv2.IMWRITE_PNG_COMPRESSION), 9]
            success, encoded_image = cv2.imencode('.png', image, encode_param)
            file_size_kb = len(encoded_image) / 1024
            self.log(f"使用最高压缩级别9,文件大小: {file_size_kb:.1f}KB")
            return encoded_image, file_size_kb, 9, True

        else:
            success, encoded_image = cv2.imencode('.bmp', image)
            file_size_kb = len(encoded_image) / 1024 if success else 0
            self.log(f"BMP格式无法压缩,文件大小: {file_size_kb:.1f}KB")
            return encoded_image, file_size_kb, 0, False

    def detect_face_and_half_body(self, input_image_path, output_image_path, target_size, extend_ratio, output_format):
        try:
            # 检查是否取消处理
            if self.cancel_processing:
                return "cancelled"

            original_size_kb = self.get_original_file_size(input_image_path)
            if original_size_kb is not None:
                self.log(f"原始文件大小: {original_size_kb:.1f}KB")

            # 读取图像
            with open(input_image_path, 'rb') as f:
                img_data = np.frombuffer(f.read(), dtype=np.uint8)
            image = cv2.imdecode(img_data, cv2.IMREAD_COLOR)

            if image is None:
                self.log(f"无法读取图像: {input_image_path}")
                return "read_error"

            # 记录图像信息
            h, w = image.shape[:2]
            channels = image.shape[2] if len(image.shape) > 2 else 1
            self.log(f"图像尺寸: {w}x{h}, 通道数: {channels}")

            # 如果没有人脸检测器,直接调整尺寸
            if self.face_cascade is None or self.face_cascade.empty():
                self.log(f"无人脸检测器,直接调整尺寸: {os.path.basename(input_image_path)}")
                resized_image = self.resize_to_target(image, target_size)
                output_ext = self.get_extension(output_format)

                if self.limit_file_size.get() and output_format in ["JPG", "PNG"]:
                    max_size_kb = self.max_file_size_kb.get()
                    encoded_image, actual_size, quality, was_compressed = self.compress_to_target_size(
                        resized_image, max_size_kb, output_format, original_size_kb
                    )
                    if was_compressed:
                        self.log(f"文件已压缩,质量级别: {quality},实际大小: {actual_size:.1f}KB")
                    else:
                        self.log(f"文件未压缩,使用高质量保存,大小: {actual_size:.1f}KB")
                else:
                    success, encoded_image = self.encode_image(resized_image, output_format, high_quality=True)

                if encoded_image is not None:
                    output_dir = os.path.dirname(output_image_path)
                    output_name = os.path.splitext(os.path.basename(output_image_path))[0] + output_ext
                    final_output_path = os.path.join(output_dir, output_name)
                    with open(final_output_path, 'wb') as f:
                        f.write(encoded_image)
                    self.log(f"已调整图像尺寸: {final_output_path}")
                    return "success_no_face_detector"
                else:
                    return "save_error"

            # 创建灰度图像用于人脸检测
            gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
            self.log(f"处理图像: {os.path.basename(input_image_path)}")

            # 检查是否取消处理
            if self.cancel_processing:
                return "cancelled"

            # 使用更严格的人脸检测参数
            start_time = time.time()
            # 增加minNeighbors以减少误检,调整minSize过滤太小的人脸
            faces = self.face_cascade.detectMultiScale(
                gray_image,
                scaleFactor=1.1,
                minNeighbors=7,  # 从5增加到7,减少假阳性
                minSize=(50, 50),  # 从30x30增加到50x50,过滤太小的人脸
                flags=cv2.CASCADE_SCALE_IMAGE
            )
            detection_time = time.time() - start_time

            # 添加详细的调试信息
            self.log(f"检测参数: minNeighbors=7, minSize=(50,50)")
            self.log(f"检测到 {len(faces)} 个人脸,耗时 {detection_time:.2f}秒")

            if detection_time > 5:
                self.log(f"警告: 人脸检测耗时较长 ({detection_time:.1f}秒)")

            if len(faces) == 0:
                self.log(f"在 {os.path.basename(input_image_path)} 中未检测到人脸")
                return "no_face"

            # 改进的人脸选择逻辑
            if len(faces) > 1:
                self.log(f"检测到 {len(faces)} 个人脸,选择最佳人脸")

                # 计算每个人脸的评分
                face_scores = []
                img_height, img_width = gray_image.shape

                for i, (x, y, w, h) in enumerate(faces):
                    # 1. 面积得分 (越大越好)
                    area_score = w * h

                    # 2. 位置得分 (越靠近中心越好)
                    center_x = x + w / 2
                    center_y = y + h / 2
                    distance_from_center = np.sqrt(
                        (center_x - img_width / 2) ** 2 + (center_y - img_height / 2) ** 2
                    )
                    # 距离中心越近,位置得分越高
                    position_score = 1.0 / (1.0 + distance_from_center / min(img_width, img_height))

                    # 3. 宽高比得分 (接近标准人脸比例0.8-1.2最好)
                    aspect_ratio = w / h
                    if 0.8 <= aspect_ratio <= 1.2:  # 标准人脸宽高比范围
                        aspect_score = 1.0
                    else:
                        # 偏离标准比例越远,得分越低
                        aspect_score = 1.0 / (1.0 + abs(aspect_ratio - 1.0))

                    # 4. 边界检查得分 (避免选择太靠近边缘的人脸)
                    margin_ratio = 0.1  # 边界比例
                    margin_x = img_width * margin_ratio
                    margin_y = img_height * margin_ratio

                    if (x > margin_x and y > margin_y and
                            x + w < img_width - margin_x and
                            y + h < img_height - margin_y):
                        boundary_score = 1.0
                    else:
                        # 靠近边界的人脸得分降低
                        boundary_score = 0.5

                    # 综合得分 (权重可以调整)
                    total_score = (
                            area_score * 0.5 +  # 面积权重最高
                            position_score * 1000 * 0.3 +  # 位置权重
                            aspect_score * 1000 * 0.1 +  # 宽高比权重
                            boundary_score * 1000 * 0.1  # 边界权重
                    )

                    face_scores.append((total_score, i, (x, y, w, h)))

                    # 详细的调试信息
                    self.log(f"人脸 {i + 1}: 位置({x},{y}) 尺寸({w}x{h}) "
                             f"面积得分{area_score} 位置得分{position_score:.3f} "
                             f"宽高比{aspect_ratio:.2f}得分{aspect_score:.3f} "
                             f"边界得分{boundary_score} 总分: {total_score:.2f}")

                # 选择得分最高的人脸
                best_face = max(face_scores, key=lambda x: x[0])
                self.log(f"选择得分最高的人脸 {best_face[1] + 1}, 得分: {best_face[0]:.2f}")
                face = best_face[2]
            else:
                self.log(f"只检测到1个人脸,直接使用")
                face = faces[0]

            x, y, w, h = face
            self.log(f"最终选择的人脸: 位置({x},{y}) 尺寸({w}x{h})")
            self.log(f"使用的扩展比例: {extend_ratio}")

            center_x = x + w // 2
            center_y = y + h // 2
            target_width, target_height = target_size
            target_aspect_ratio = target_width / target_height

            # 重新设计裁剪区域计算逻辑
            # 基于人脸尺寸和扩展比例计算初始裁剪区域
            base_crop_width = int(w * extend_ratio)
            base_crop_height = int(h * extend_ratio)

            self.log(f"基础裁剪尺寸: {base_crop_width}x{base_crop_height} (基于扩展比例 {extend_ratio})")

            # 确保裁剪区域符合目标宽高比
            if base_crop_width / base_crop_height > target_aspect_ratio:
                # 太宽了,调整高度
                base_crop_height = int(base_crop_width / target_aspect_ratio)
            else:
                # 太高了,调整宽度
                base_crop_width = int(base_crop_height * target_aspect_ratio)

            self.log(f"调整后的裁剪尺寸: {base_crop_width}x{base_crop_height} (符合目标宽高比)")

            # 确保裁剪区域不小于目标尺寸
            crop_width = max(base_crop_width, target_width)
            crop_height = max(base_crop_height, target_height)

            self.log(f"最终裁剪尺寸: {crop_width}x{crop_height} (确保不小于目标尺寸)")

            # 计算裁剪边界
            crop_x1 = max(0, center_x - crop_width // 2)
            crop_y1 = max(0, center_y - crop_height // 2)
            crop_x2 = min(image.shape[1], center_x + crop_width // 2)
            crop_y2 = min(image.shape[0], center_y + crop_height // 2)

            # 如果裁剪区域超出图像边界,调整中心点
            if crop_x1 == 0:
                crop_x2 = min(image.shape[1], crop_width)
            if crop_y1 == 0:
                crop_y2 = min(image.shape[0], crop_height)
            if crop_x2 == image.shape[1]:
                crop_x1 = max(0, image.shape[1] - crop_width)
            if crop_y2 == image.shape[0]:
                crop_y1 = max(0, image.shape[0] - crop_height)

            self.log(f"裁剪区域: ({crop_x1}, {crop_y1}) 到 ({crop_x2}, {crop_y2})")
            self.log(f"实际裁剪尺寸: {crop_x2 - crop_x1}x{crop_y2 - crop_y1}")

            # 从原始彩色图像中裁剪半身区域
            half_body_image = image[crop_y1:crop_y2, crop_x1:crop_x2]
            actual_crop_height, actual_crop_width = half_body_image.shape[:2]
            crop_aspect_ratio = actual_crop_width / actual_crop_height

            self.log(f"裁剪后尺寸: {actual_crop_width}x{actual_crop_height}, 宽高比: {crop_aspect_ratio:.2f}")

            # 调整裁剪后的图像到目标尺寸
            if actual_crop_width >= target_width and actual_crop_height >= target_height:
                self.log("裁剪区域足够大,进行精确裁剪")
                if crop_aspect_ratio > target_aspect_ratio:
                    # 太宽,调整高度
                    new_height = target_height
                    new_width = int(target_height * crop_aspect_ratio)
                    resized = cv2.resize(half_body_image, (new_width, new_height), interpolation=cv2.INTER_AREA)
                    crop_x = (new_width - target_width) // 2
                    resized_image = resized[0:target_height, crop_x:crop_x + target_width]
                else:
                    # 太高,调整宽度
                    new_width = target_width
                    new_height = int(target_width / crop_aspect_ratio)
                    resized = cv2.resize(half_body_image, (new_width, new_height), interpolation=cv2.INTER_AREA)
                    crop_y = (new_height - target_height) // 2
                    resized_image = resized[crop_y:crop_y + target_height, 0:target_width]
            else:
                self.log("裁剪区域较小,使用智能调整方法")
                # 使用新的智能调整方法,基于人脸大小决定处理方式
                # 传递人脸位置信息给resize方法
                # 计算在裁剪图像中的人脸位置
                face_in_crop_x = x - crop_x1
                face_in_crop_y = y - crop_y1
                face_rect_in_crop = (face_in_crop_x, face_in_crop_y, w, h)

                resized_image = self.resize_to_target(half_body_image, face_rect_in_crop, target_size)

            output_ext = self.get_extension(output_format)

            if self.limit_file_size.get() and output_format in ["JPG", "PNG"]:
                max_size_kb = self.max_file_size_kb.get()
                encoded_image, actual_size, quality, was_compressed = self.compress_to_target_size(
                    resized_image, max_size_kb, output_format, original_size_kb
                )
                if was_compressed:
                    self.log(f"文件已压缩,质量级别: {quality},实际大小: {actual_size:.1f}KB")
                else:
                    self.log(f"文件未压缩,使用高质量保存,大小: {actual_size:.1f}KB")
            else:
                success, encoded_image = self.encode_image(resized_image, output_format, high_quality=True)

            if encoded_image is not None:
                output_dir = os.path.dirname(output_image_path)
                output_name = os.path.splitext(os.path.basename(output_image_path))[0] + output_ext
                final_output_path = os.path.join(output_dir, output_name)
                with open(final_output_path, 'wb') as f:
                    f.write(encoded_image)
                self.log(f"检测到人脸,半身图像已保存:{final_output_path}")
                return "success"
            else:
                self.log(f"保存图像失败:{output_image_path}")
                return "save_error"

        except Exception as e:
            self.log(f"处理图像 {input_image_path} 时出错: {e}")
            self.log(f"详细错误信息: {traceback.format_exc()}")
            return "process_error"

    def get_extension(self, format_name):
        format_map = {"JPG": ".jpg", "PNG": ".png", "BMP": ".bmp"}
        return format_map.get(format_name, ".jpg")

    def encode_image(self, image, format_name, high_quality=True):
        if format_name == "JPG":
            if high_quality:
                encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 95]
            else:
                encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 85]
            return cv2.imencode('.jpg', image, encode_param)
        elif format_name == "PNG":
            if high_quality:
                encode_param = [int(cv2.IMWRITE_PNG_COMPRESSION), 1]
            else:
                encode_param = [int(cv2.IMWRITE_PNG_COMPRESSION), 3]
            return cv2.imencode('.png', image, encode_param)
        elif format_name == "BMP":
            return cv2.imencode('.bmp', image)
        else:
            encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 95]
            return cv2.imencode('.jpg', image, encode_param)

    def process_images(self):
        try:
            input_folder = self.input_folder.get()
            output_folder = self.output_folder.get()
            target_size = (self.target_width.get(), self.target_height.get())
            extend_ratio = self.extend_ratio.get()
            output_format = self.output_format.get()
            limit_size = self.limit_file_size.get()
            max_size_kb = self.max_file_size_kb.get() if limit_size else 0

            self.log(f"开始处理...")
            self.log(f"输入文件夹: {input_folder}")
            self.log(f"输出文件夹: {output_folder}")
            self.log(f"目标尺寸: {target_size[0]}x{target_size[1]}")
            self.log(f"扩展比例: {extend_ratio}")
            self.log(f"输出格式: {output_format}")
            if limit_size:
                self.log(f"文件大小限制: {max_size_kb}KB")
            self.flush_log_queue()  # 立即显示重要信息

            result = self.batch_detect_face_and_half_body(input_folder, output_folder, target_size, extend_ratio,
                                                          output_format)

            self.log("处理完成!")
            self.update_status("处理完成")
            self.update_progress(self.total_files)

            if self.cancel_processing:
                messagebox.showinfo("取消", "处理已被用户取消")
            else:
                messagebox.showinfo("完成",
                                    f"处理完成!\n成功: {result['success']}张\n需人工核查: {result['manual_check']}张\n处理失败: {result['errors']}张")

        except Exception as e:
            self.log(f"处理过程中出错: {str(e)}")
            messagebox.showerror("错误", f"处理过程中出错: {str(e)}")
        finally:
            self.is_processing = False
            self.process_btn.config(state="normal")
            self.cancel_btn.config(state="disabled")
            self.flush_log_queue()  # 确保所有日志都显示

    def batch_detect_face_and_half_body(self, input_folder, output_folder, target_size, extend_ratio, output_format):
        if not os.path.exists(output_folder):
            os.makedirs(output_folder)
            self.log(f"创建输出文件夹: {output_folder}")

        # 统计信息
        success_count = 0
        manual_check_count = 0
        process_error_count = 0
        read_error_count = 0
        save_error_count = 0
        success_no_face_detector_count = 0
        cancelled_count = 0

        # 获取图片文件
        image_files = [f for f in os.listdir(input_folder) if
                       f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp'))]
        self.total_files = len(image_files)
        self.processed_files = 0

        self.log(f"找到 {self.total_files} 个图像文件")
        self.flush_log_queue()  # 立即显示重要信息

        self.progress['maximum'] = self.total_files

        for i, filename in enumerate(image_files):
            # 检查是否取消处理
            if self.cancel_processing:
                cancelled_count = self.total_files - i
                self.log(f"取消处理,跳过剩余 {cancelled_count} 个文件")
                break

            self.update_status(f"处理中: {i + 1}/{self.total_files}")
            input_image_path = os.path.join(input_folder, filename)

            # 处理图片
            name, _ = os.path.splitext(filename)
            output_ext = self.get_extension(output_format)
            output_filename = name + output_ext
            output_image_path = os.path.join(output_folder, output_filename)

            result = self.detect_face_and_half_body(input_image_path, output_image_path, target_size, extend_ratio,
                                                    output_format)

            # 检查是否取消处理
            if result == "cancelled":
                cancelled_count = self.total_files - i
                break

            # 更新统计
            if result == "success":
                success_count += 1
            elif result == "success_no_face_detector":
                success_no_face_detector_count += 1
            else:
                name, _ = os.path.splitext(filename)

                if result == "no_face":
                    manual_check_filename = f"{name}_需人工核查{output_ext}"
                    manual_check_path = os.path.join(output_folder, manual_check_filename)
                    manual_check_count += 1
                    mark = "需人工核查"
                    output_path = manual_check_path
                elif result == "read_error":
                    read_error_filename = f"{name}_处理失败_读取错误{output_ext}"
                    read_error_path = os.path.join(output_folder, read_error_filename)
                    read_error_count += 1
                    mark = "处理失败_读取错误"
                    output_path = read_error_path
                elif result == "save_error":
                    save_error_filename = f"{name}_处理失败_保存错误{output_ext}"
                    save_error_path = os.path.join(output_folder, save_error_filename)
                    save_error_count += 1
                    mark = "处理失败_保存错误"
                    output_path = save_error_path
                else:
                    process_error_filename = f"{name}_处理失败{output_ext}"
                    process_error_path = os.path.join(output_folder, process_error_filename)
                    process_error_count += 1
                    mark = "处理失败"
                    output_path = process_error_path

                self.log(f"处理失败: {filename}, 错误类型: {mark}, 将保存为: {os.path.basename(output_path)}")

                try:
                    shutil.copy2(input_image_path, output_path)
                    self.log(f"已复制原图并标记为{mark}: {os.path.basename(output_path)}")
                except Exception as e:
                    self.log(f"复制原图失败: {filename}, 错误: {e}")
                    try:
                        with open(input_image_path, 'rb') as f:
                            img_data = np.frombuffer(f.read(), dtype=np.uint8)
                        original_image = cv2.imdecode(img_data, cv2.IMREAD_COLOR)
                        if original_image is not None:
                            success, encoded_image = self.encode_image(original_image, output_format, high_quality=True)
                            if success:
                                with open(output_path, 'wb') as f:
                                    f.write(encoded_image)
                                self.log(f"已使用OpenCV保存原图并标记为{mark}: {os.path.basename(output_path)}")
                            else:
                                self.log(f"使用OpenCV保存失败: {os.path.basename(output_path)}")
                        else:
                            self.log(f"无法读取原图: {filename}")
                    except Exception as e2:
                        self.log(f"所有保存尝试都失败: {filename}, 错误: {e2}")

            # 更新进度
            self.processed_files += 1
            self.update_progress(self.processed_files)

            # 定期清理内存
            if i % 20 == 0 and i > 0:
                gc.collect()
                self.log(f"已处理 {i}/{self.total_files} 张图片,内存清理完成")

        # 最终刷新日志
        self.flush_log_queue()

        # 输出统计结果
        self.log("\n" + "=" * 50)
        self.log("处理完成!统计结果:")
        if self.cancel_processing:
            self.log(f"用户取消了处理")
            self.log(f"已处理: {self.processed_files} 张")
            self.log(f"已取消: {cancelled_count} 张")
        else:
            self.log(f"成功处理(人脸检测): {success_count} 张")
            self.log(f"成功处理(无检测器): {success_no_face_detector_count} 张")
            self.log(f"需人工核查: {manual_check_count} 张")
            self.log(f"读取错误: {read_error_count} 张")
            self.log(f"保存错误: {save_error_count} 张")
            self.log(f"其他处理错误: {process_error_count} 张")
            total_processed = success_count + success_no_face_detector_count + manual_check_count + read_error_count + save_error_count + process_error_count
            self.log(f"总计: {total_processed} 张")
        self.log("=" * 50)
        self.flush_log_queue()

        return {
            "success": success_count + success_no_face_detector_count,
            "manual_check": manual_check_count,
            "errors": read_error_count + save_error_count + process_error_count
        }


def main():
    root = tk.Tk()
    app = PhotoProcessorApp(root)
    root.mainloop()


if __name__ == "__main__":
    main()



输出的图片,基本可以完成自动校准人像

输出的图片,基本可以完成自动校准人像

输入的测试图片

输入的测试图片
5c49473e-d172-457e-9ffc-338a069a1879.png
e99ad6e2-53d4-4243-ae11-8db8395ab2b2.png

打包所需程序和源码.zip

142.75 KB, 下载次数: 349, 下载积分: 吾爱币 -1 CB

cv2;numpy

免费评分

参与人数 44吾爱币 +47 热心值 +42 收起 理由
ivan1031 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
苦逼的挨踢屌丝 + 1 + 1 谢谢@Thanks!
Ray2008Ray + 1 + 1 我很赞同!
googxy + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Carinx + 1 + 1 用心讨论,共获提升!
seven2024 + 1 谢谢@Thanks!
tte + 1 鼓励转贴优秀软件安全工具和文档!
zxsn + 1 + 1 刚好做证件照需要
alpylj + 1 + 1 热心回复!
tzb131 + 1 + 1 我很赞同!
dx163 + 1 + 1 谢谢@Thanks!
尛辉 + 1 + 1 我很赞同!
苦苦鬼步 + 1 + 1 谢谢@Thanks!
MS5608 + 1 + 1 用心讨论,共获提升!
luisls + 1 + 1 我很赞同!
hello95271 + 1 + 1 我很赞同!
Tiniaual + 1 + 1 我很赞同!
aabbcc123123 + 1 + 1 谢谢@Thanks!
drw168 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Issacclark1 + 1 谢谢@Thanks!
牧云寒江 + 1 + 1 我很赞同!
BeginForEnd + 1 + 1 热心回复!
coolfenny + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
shehui0927 + 1 + 1 厉害了,我的佬!
lyasima + 1 + 1 谢谢@Thanks!
bjliu + 1 + 1 用心讨论,共获提升!
blindcat + 1 + 1 谢谢@Thanks!
无尘浪子 + 1 谢谢@Thanks!
zj_tj + 1 + 1 我很赞同!
dahan531 + 1 + 1 我很赞同!
klmytwb + 1 + 1 谢谢@Thanks!
jinqiaoa1a + 1 + 1 谢谢@Thanks!
qiuku + 1 谢谢@Thanks!
fyz2007 + 1 + 1 谢谢@Thanks!
逆风£ + 1 + 1 还不错,个别不太标准的图像识别会出错,出错的返回不方便看,出错的能单独 ...
NikeSan + 1 + 1 谢谢@Thanks!
shenquanwusheng + 1 谢谢@Thanks!
hybcrp + 1 + 1 热心回复!
Love0912 + 1 + 1 看上去用心了,给予一定的鼓励
hrh123 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
naixubao + 1 + 1 谢谢@Thanks!
kingc138 + 1 + 1 我很赞同!
魔-沫 + 1 + 1 用心讨论,共获提升!
yhy123456 + 1 + 1 我很赞同!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

kingc138 发表于 2025-11-22 15:43
本帖最后由 kingc138 于 2025-11-22 15:45 编辑

换了一下布局。
[Python] 纯文本查看 复制代码
import sys
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import cv2
import numpy as np
import shutil
import threading
import urllib.request
import traceback
import gc
import time
 
# 版本信息
APP_VERSION = "Version0.1"  # 更新版本号
APP_NAME = "学籍照片批处理工具" 
 
# 打包后资源文件路径处理
def resource_path(relative_path):
    """获取打包后资源的绝对路径"""
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)
 
 
class PhotoProcessorApp:
    def __init__(self, root):
        self.root = root
        self.root.title(f"{APP_NAME}")
        self.root.geometry("1100x600")
        self.root.resizable(True, True)
 
        # 设置图标
        try:
            icon_path = resource_path("icon.ico")
            if os.path.exists(icon_path):
                self.root.iconbitmap(icon_path)
        except:
            pass
 
        # 初始化变量
        self.input_folder = tk.StringVar()
        self.output_folder = tk.StringVar()
        self.target_width = tk.IntVar(value=413)
        self.target_height = tk.IntVar(value=579)
        self.extend_ratio = tk.DoubleVar(value=2.0)
        self.output_format = tk.StringVar(value="JPG")
        self.limit_file_size = tk.BooleanVar(value=False)
        self.max_file_size_kb = tk.IntVar(value=50)
        self.is_processing = False
        self.total_files = 0
        self.processed_files = 0
        # 在类初始化中添加可配置的阈值
        self.small_face_threshold = 0.05  # 小于5%视为小人像
        self.large_face_threshold = 0.3  # 大于30%视为大人像
        # 性能优化相关变量
        self.log_queue = []
        self.log_batch_size = 10
        self.log_counter = 0
        self.cancel_processing = False
        self._last_percentage = -1
 
        self.setup_fonts()
        self.setup_ui()
 
        # 初始化人脸检测器
        self.face_cascade = self.get_face_detector()
        if self.face_cascade is None or self.face_cascade.empty():
            messagebox.showwarning("警告", "人脸检测器加载失败,部分功能可能受限")
 
    def setup_fonts(self):
        """设置中文字体"""
        chinese_fonts = ["Microsoft YaHei", "SimHei", "SimSun", "KaiTi", "FangSong"]
        self.title_font = None
        self.normal_font = None
 
        test_label = ttk.Label(self.root, text="测试")
        for font in chinese_fonts:
            try:
                test_label.config(font=(font, 12))
                self.root.update()
                self.title_font = (font, 16, "bold")
                self.normal_font = (font, 9)
                break
            except:
                continue
 
        if self.title_font is None:
            self.title_font = ("TkDefaultFont", 16, "bold")
            self.normal_font = ("TkDefaultFont", 9)
        test_label.destroy()
 
    def setup_ui(self):
        # 创建样式
        style = ttk.Style()
        
        # 修改标题字体样式
        style.configure("TLabel", font=('Microsoft YaHei', 10))
        style.configure("Header.TLabel", font=('Microsoft YaHei', 16, 'bold'), foreground='#333333')
        
        # 为按钮添加更明显的底色样式
        style.configure("Light.TButton", font=('Microsoft YaHei', 10),
                      padding=(10, 5), relief="flat")
        # 使用ttk.Style的map方法为不同状态设置不同背景色
        style.map("Light.TButton", 
                 background=[("!disabled", "#e0e0e0"), ("active", "#d0d0d0")],
                 foreground=[("!disabled", "#000000")])
        
        # 创建主框架
        main_frame = ttk.Frame(self.root, padding="15")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        
        # 配置主框架的列权重 - 改为2列布局,左侧控制面板,右侧日志面板
        main_frame.columnconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)  # 右侧日志框占据一半空间

        # 标题
        title_label = ttk.Label(main_frame, text=APP_NAME, style="Header.TLabel")
        title_label.grid(row=0, column=0, columnspan=2, pady=(0, 20)) 
         
        # 文件夹选择部分 - 使用更紧凑的布局
        folder_frame = ttk.LabelFrame(main_frame, text="文件路径", padding="10")
        folder_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        folder_frame.columnconfigure(1, weight=1)
        
        # 输入文件夹选择
        ttk.Label(folder_frame, text="输入文件夹:").grid(row=0, column=0, sticky=tk.W, pady=(5, 5))
        ttk.Entry(folder_frame, textvariable=self.input_folder).grid(row=0, column=1, sticky=(tk.W, tk.E),
                                                                     pady=(5, 5), padx=(5, 5))
        ttk.Button(folder_frame, text="浏览", command=self.browse_input_folder, style="Light.TButton").grid(row=0, column=2, pady=(5, 5))

        # 输出文件夹选择
        ttk.Label(folder_frame, text="输出文件夹:").grid(row=1, column=0, sticky=tk.W, pady=(5, 5))
        ttk.Entry(folder_frame, textvariable=self.output_folder).grid(row=1, column=1, sticky=(tk.W, tk.E),
                                                                      pady=(5, 5), padx=(5, 5))
        ttk.Button(folder_frame, text="浏览", command=self.browse_output_folder, style="Light.TButton").grid(row=1, column=2, pady=(5, 5))

        # 参数设置 - 使用网格布局使参数更整齐
        params_frame = ttk.LabelFrame(main_frame, text="处理参数", padding="10")
        params_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=10)
        
        # 配置参数框架的列
        for i in range(4):
            params_frame.columnconfigure(i, weight=1 if i in (1, 3) else 0)
        
        # 输出尺寸 - 垂直布局
        ttk.Label(params_frame, text="输出尺寸:").grid(row=0, column=0, sticky=tk.W, pady=(0, 10))
        size_frame = ttk.Frame(params_frame)
        size_frame.grid(row=0, column=1, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
        
        width_frame = ttk.Frame(size_frame)
        width_frame.pack(side=tk.LEFT, padx=(0, 20))
        ttk.Label(width_frame, text="宽度: ").pack(side=tk.LEFT, padx=(0, 5))
        ttk.Entry(width_frame, textvariable=self.target_width, width=8).pack(side=tk.LEFT)
        ttk.Label(width_frame, text="像素").pack(side=tk.LEFT, padx=(5, 0))
        
        height_frame = ttk.Frame(size_frame)
        height_frame.pack(side=tk.LEFT)
        ttk.Label(height_frame, text="高度: ").pack(side=tk.LEFT, padx=(0, 5))
        ttk.Entry(height_frame, textvariable=self.target_height, width=8).pack(side=tk.LEFT)
        ttk.Label(height_frame, text="像素").pack(side=tk.LEFT, padx=(5, 0))

        # 扩展比例 - 垂直布局(输出尺寸下方)
        ttk.Label(params_frame, text="扩展比例:").grid(row=1, column=0, sticky=tk.W, pady=(0, 10))
        scale_frame = ttk.Frame(params_frame)
        scale_frame.grid(row=1, column=1, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
        ttk.Entry(scale_frame, textvariable=self.extend_ratio, width=10).pack(side=tk.LEFT)
        ttk.Label(scale_frame, text="(推荐值: 1.5-3.0)").pack(side=tk.LEFT, padx=(10, 0))

        # 输出格式 - 垂直布局
        ttk.Label(params_frame, text="输出格式:").grid(row=2, column=0, sticky=tk.W, pady=(0, 10))
        format_frame = ttk.Frame(params_frame)
        format_frame.grid(row=2, column=1, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
        format_combo = ttk.Combobox(format_frame, textvariable=self.output_format, values=["JPG", "PNG", "BMP"],
                                    state="readonly", width=10, font=('Microsoft YaHei', 9))
        format_combo.pack(side=tk.LEFT)
        ttk.Label(format_frame, text="(选择输出图片的格式)").pack(side=tk.LEFT, padx=(10, 0))

        # 文件大小限制 - 垂直布局(输出格式下方)
        ttk.Label(params_frame, text="大小限制:").grid(row=3, column=0, sticky=tk.W, pady=(0, 10))
        size_limit_frame = ttk.Frame(params_frame)
        size_limit_frame.grid(row=3, column=1, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
        ttk.Checkbutton(size_limit_frame, text="启用", variable=self.limit_file_size).pack(side=tk.LEFT)
        
        size_entry_frame = ttk.Frame(size_limit_frame)
        size_entry_frame.pack(side=tk.LEFT, padx=(10, 0))
        ttk.Entry(size_entry_frame, textvariable=self.max_file_size_kb, width=8).pack(side=tk.LEFT)
        ttk.Label(size_entry_frame, text="KB").pack(side=tk.LEFT, padx=(5, 0))

        # 处理按钮和取消按钮 - 居中布局
        button_frame = ttk.Frame(main_frame)
        button_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=15)
        button_frame.grid_columnconfigure(0, weight=1)
        button_frame.grid_columnconfigure(1, weight=0)
        button_frame.grid_columnconfigure(2, weight=0)
        button_frame.grid_columnconfigure(3, weight=1)
        
        self.process_btn = ttk.Button(button_frame, text="开始处理", command=self.start_processing, style="Light.TButton")
        self.process_btn.grid(row=0, column=1, padx=(0, 10))

        self.cancel_btn = ttk.Button(button_frame, text="取消处理", command=self.cancel_processing_func, state="disabled", style="Light.TButton")
        self.cancel_btn.grid(row=0, column=2)

        # 进度条 - 改进布局
        progress_frame = ttk.Frame(main_frame)
        progress_frame.grid(row=4, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
        progress_frame.columnconfigure(0, weight=1)
        
        self.progress = ttk.Progressbar(progress_frame, mode='determinate')
        self.progress.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 10))
        self.progress_label = ttk.Label(progress_frame, text="0%", width=5)
        self.progress_label.grid(row=0, column=1, sticky=tk.W)

        # 日志文本框 - 移至右侧并占据整个右侧窗口
        log_frame = ttk.LabelFrame(main_frame, text="处理日志", padding="8")
        log_frame.grid(row=1, column=1, rowspan=4, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10), padx=(10, 0))
        log_frame.columnconfigure(0, weight=1)
        log_frame.rowconfigure(0, weight=1)
        
        text_scroll = ttk.Scrollbar(log_frame)
        text_scroll.grid(row=0, column=1, sticky=(tk.N, tk.S))
        self.log_text = tk.Text(log_frame, yscrollcommand=text_scroll.set, font=('Microsoft YaHei', 9))
        self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        text_scroll.config(command=self.log_text.yview)
        
        # 配置主框架各行权重
        main_frame.rowconfigure(4, weight=1)  # 确保左侧面板可适当拉伸
        main_frame.rowconfigure(1, weight=0)  # 控制行权重
        
        # 底部行 - 放置准备标签和版本号
        bottom_frame = ttk.Frame(main_frame)
        bottom_frame.grid(row=5, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 5))
        bottom_frame.columnconfigure(0, weight=1)
        
        # 状态标签 - 加粗显示
        self.status_label = ttk.Label(bottom_frame, text="准备就绪", font=('Microsoft YaHei', 10, 'bold'))
        self.status_label.grid(row=0, column=0, sticky=tk.W, padx=(0, 10))
        
        # 版本号放在右下角
        version_label = ttk.Label(bottom_frame, text=APP_VERSION, font=('Microsoft YaHei', 8))
        version_label.grid(row=0, column=1, sticky=tk.E, padx=(0, 10))
 
    def browse_input_folder(self):
        folder = filedialog.askdirectory(title="选择输入文件夹")
        if folder:
            self.input_folder.set(folder)
 
    def browse_output_folder(self):
        folder = filedialog.askdirectory(title="选择输出文件夹")
        if folder:
            self.output_folder.set(folder)
 
    def log(self, message):
        """批量处理日志更新,减少UI刷新频率"""
        self.log_queue.append(message)
        self.log_counter += 1
 
        # 批量更新或重要消息立即更新
        if (self.log_counter >= self.log_batch_size or
                any(keyword in message for keyword in ["错误", "失败", "警告", "完成", "取消", "超时"])):
            self.flush_log_queue()
 
    def flush_log_queue(self):
        """将积压的日志一次性更新到UI"""
        if not self.log_queue:
            return
 
        # 一次性插入所有日志
        log_text = "\n".join(self.log_queue) + "\n"
        self.log_text.insert(tk.END, log_text)
        self.log_text.see(tk.END)
 
        # 清空队列和计数器
        self.log_queue = []
        self.log_counter = 0
 
        # 只在这里更新UI
        self.root.update_idletasks()
 
    def update_status(self, message):
        self.status_label.config(text=message)
        # 状态更新不频繁,可以直接更新UI
        self.root.update_idletasks()
 
    def update_progress(self, value):
        """批量更新进度,减少UI刷新"""
        self.progress['value'] = value
 
        # 只在进度变化较大时更新百分比
        percentage = int((value / self.total_files) * 100) if self.total_files > 0 else 0
        current_percentage = getattr(self, '_last_percentage', -1)
 
        if percentage != current_percentage or value == self.total_files or value == 0:
            self.progress_label.config(text=f"{percentage}%")
            self._last_percentage = percentage
 
            # 只在百分比变化时或处理完成时更新UI
            if percentage % 10 == 0 or value == self.total_files or value == 0:
                self.flush_log_queue()  # 顺便刷新日志
 
    def cancel_processing_func(self):
        """取消处理"""
        self.cancel_processing = True
        self.cancel_btn.config(state="disabled")
        self.log("正在取消处理,请等待...")
        self.flush_log_queue()
 
    def start_processing(self):
        if self.is_processing:
            return
 
        if not self.input_folder.get():
            messagebox.showerror("错误", "请选择输入文件夹")
            return
        if not self.output_folder.get():
            messagebox.showerror("错误", "请选择输出文件夹")
            return
        if not os.path.exists(self.input_folder.get()):
            messagebox.showerror("错误", "输入文件夹不存在")
            return
 
        if self.limit_file_size.get():
            if self.max_file_size_kb.get() <= 0:
                messagebox.showerror("错误", "文件大小限制必须大于0KB")
                return
            if self.output_format.get() == "BMP":
                messagebox.showwarning("警告", "BMP格式无法压缩")
 
        # 开始处理
        self.is_processing = True
        self.cancel_processing = False  # 重置取消标志
        self.process_btn.config(state="disabled")
        self.cancel_btn.config(state="normal")  # 启用取消按钮
        self.progress['value'] = 0
        self.progress_label.config(text="0%")
        self.log_text.delete(1.0, tk.END)
        self._last_percentage = -1  # 重置进度百分比
 
        thread = threading.Thread(target=self.process_images)
        thread.daemon = True
        thread.start()
 
    def get_face_detector(self):
        cascade_paths = []
        try:
            builtin_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
            if os.path.exists(builtin_path):
                cascade_paths.append(builtin_path)
        except:
            pass
 
        try:
            resource_cascade_path = resource_path('haarcascade_frontalface_default.xml')
            if os.path.exists(resource_cascade_path):
                cascade_paths.append(resource_cascade_path)
        except:
            pass
 
        try:
            current_dir_path = "haarcascade_frontalface_default.xml"
            if os.path.exists(current_dir_path):
                cascade_paths.append(current_dir_path)
        except:
            pass
 
        for cascade_path in cascade_paths:
            try:
                face_cascade = cv2.CascadeClassifier(cascade_path)
                if not face_cascade.empty():
                    self.log(f"成功加载人脸检测器: {cascade_path}")
                    return face_cascade
            except Exception as e:
                self.log(f"加载人脸检测器失败 {cascade_path}: {e}")
 
        try:
            self.log("尝试下载人脸检测模型...")
            url = "https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_default.xml"
            download_path = resource_path("haarcascade_frontalface_default.xml")
            if not os.path.exists(download_path):
                urllib.request.urlretrieve(url, download_path)
                self.log("人脸检测模型下载完成")
            face_cascade = cv2.CascadeClassifier(download_path)
            if not face_cascade.empty():
                self.log("使用下载的人脸检测模型")
                return face_cascade
        except Exception as e:
            self.log(f"下载人脸检测模型失败: {e}")
 
        self.log("警告: 无法加载任何人脸检测器")
        return None
 
    def get_background_color(self, image):
        """获取图像背景颜色,兼容彩色和灰度图像"""
        h, w = image.shape[:2]
 
        # 检查图像是彩色还是灰度
        is_color = len(image.shape) == 3 and image.shape[2] == 3
 
        # 确保有足够的边缘区域
        edge_size = min(5, h, w)
 
        try:
            # 获取各边缘区域
            top_edge = image[0:edge_size, :]
            bottom_edge = image[h - edge_size:h, :]
            left_edge = image[:, 0:edge_size]
            right_edge = image[:, w - edge_size:w]
 
            # 根据图像类型处理边缘
            if is_color:
                # 彩色图像 - 重塑为 (像素数, 3)
                edges_list = []
 
                if top_edge.size > 0:
                    top_flat = top_edge.reshape(-1, 3)
                    edges_list.append(top_flat)
 
                if bottom_edge.size > 0:
                    bottom_flat = bottom_edge.reshape(-1, 3)
                    edges_list.append(bottom_flat)
 
                if left_edge.size > 0:
                    left_flat = left_edge.reshape(-1, 3)
                    edges_list.append(left_flat)
 
                if right_edge.size > 0:
                    right_flat = right_edge.reshape(-1, 3)
                    edges_list.append(right_flat)
            else:
                # 灰度图像 - 重塑为 (像素数, 1)
                edges_list = []
 
                if top_edge.size > 0:
                    top_flat = top_edge.reshape(-1, 1)
                    edges_list.append(top_flat)
 
                if bottom_edge.size > 0:
                    bottom_flat = bottom_edge.reshape(-1, 1)
                    edges_list.append(bottom_flat)
 
                if left_edge.size > 0:
                    left_flat = left_edge.reshape(-1, 1)
                    edges_list.append(left_flat)
 
                if right_edge.size > 0:
                    right_flat = right_edge.reshape(-1, 1)
                    edges_list.append(right_flat)
 
            # 检查是否有有效的边缘数据
            if not edges_list:
                # 返回默认背景色(白色或灰色)
                return np.array([255, 255, 255], dtype=np.uint8) if is_color else np.array([255], dtype=np.uint8)
 
            # 合并所有边缘像素
            edges = np.vstack(edges_list)
 
            # 计算平均颜色
            avg_color = np.mean(edges, axis=0)
 
            # 确保返回正确的格式
            if is_color:
                return avg_color.astype(np.uint8)
            else:
                # 灰度图像返回单值,但转换为三通道用于彩色背景
                gray_value = avg_color.astype(np.uint8)[0]
                return np.array([gray_value, gray_value, gray_value], dtype=np.uint8)
 
        except Exception as e:
            self.log(f"获取背景颜色时出错: {e}, 使用默认白色背景")
            return np.array([255, 255, 255], dtype=np.uint8)
 
    def resize_to_target(self, image, face_rect, target_size):
        """根据人脸大小智能调整图像到目标尺寸"""
        target_width, target_height = target_size
        h, w = image.shape[:2]
 
        # 计算人脸在图像中的占比
        x, y, face_w, face_h = face_rect
        face_area = face_w * face_h
        image_area = w * h
        face_ratio = face_area / image_area
 
        self.log(f"人脸占比: {face_ratio:.3f} ({face_w}x{face_h} / {w}x{h})")
 
        # 根据人脸占比决定处理策略
        if face_ratio < 0.05:  # 人脸很小,需要放大
            # 计算放大比例,使人脸大小合适
            # 目标人脸大小约为图像的15-20%
            target_face_ratio = 0.18
            scale = (target_face_ratio / face_ratio) ** 0.5  # 开方使缩放更温和
 
            # 限制最大缩放倍数
            scale = min(scale, 3.0)  # 最多放大3倍
 
            new_w = int(w * scale)
            new_h = int(h * scale)
 
            # 使用高质量的插值方法放大图像
            enlarged = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_CUBIC)
 
            # 计算以人脸为中心的新裁剪区域
            face_center_x = int((x + face_w / 2) * scale)
            face_center_y = int((y + face_h / 2) * scale)
 
            # 从放大后的图像以人脸为中心裁剪目标尺寸
            start_x = max(0, face_center_x - target_width // 2)
            start_y = max(0, face_center_y - target_height // 2)
            end_x = start_x + target_width
            end_y = start_y + target_height
 
            # 确保不越界
            if end_x > new_w:
                end_x = new_w
                start_x = end_x - target_width
            if end_y > new_h:
                end_y = new_h
                start_y = end_y - target_height
 
            result = enlarged[start_y:end_y, start_x:end_x]
            self.log(f"小人像: 放大 {scale:.2f} 倍并以人脸为中心裁剪")
 
        elif face_ratio > 0.3:  # 人脸很大,可能需要缩小
            # 计算缩小比例
            scale = min(target_width / w, target_height / h)
            new_w = int(w * scale)
            new_h = int(h * scale)
 
            # 缩小图像
            resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA)
 
            # 使用背景色填充
            bg_color = self.get_background_color(image)
            result = np.full((target_height, target_width, 3), bg_color, dtype=np.uint8)
            y_offset = (target_height - new_h) // 2
            x_offset = (target_width - new_w) // 2
            result[y_offset:y_offset + new_h, x_offset:x_offset + new_w] = resized
            self.log(f"大人像: 缩小 {scale:.2f} 倍并填充背景")
 
        else:  # 人脸大小适中,使用标准处理
            scale = min(target_width / w, target_height / h)
            new_w = int(w * scale)
            new_h = int(h * scale)
            resized = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA)
            bg_color = self.get_background_color(image)
            result = np.full((target_height, target_width, 3), bg_color, dtype=np.uint8)
            y_offset = (target_height - new_h) // 2
            x_offset = (target_width - new_w) // 2
            result[y_offset:y_offset + new_h, x_offset:x_offset + new_w] = resized
            self.log(f"标准处理: 调整尺寸并居中")
 
        return result
 
    def get_original_file_size(self, file_path):
        try:
            return os.path.getsize(file_path) / 1024
        except:
            return None
 
    def compress_to_target_size(self, image, target_size_kb, output_format, original_size_kb=None):
        max_size_bytes = target_size_kb * 1024
 
        # 如果原始文件已经小于目标大小,直接使用高质量编码
        if original_size_kb is not None and original_size_kb <= target_size_kb:
            self.log(f"原始文件大小 {original_size_kb:.1f}KB 已满足要求,使用高质量编码")
            if output_format == "JPG":
                encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 95]
                success, encoded_image = cv2.imencode('.jpg', image, encode_param)
                if success:
                    return encoded_image, len(encoded_image) / 1024, 95, False
            elif output_format == "PNG":
                encode_param = [int(cv2.IMWRITE_PNG_COMPRESSION), 1]
                success, encoded_image = cv2.imencode('.png', image, encode_param)
                if success:
                    return encoded_image, len(encoded_image) / 1024, 1, False
 
        if output_format == "JPG":
            qualities = [95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10]
            for quality in qualities:
                encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
                success, encoded_image = cv2.imencode('.jpg', image, encode_param)
                if success and len(encoded_image) <= max_size_bytes:
                    file_size_kb = len(encoded_image) / 1024
                    self.log(f"JPG质量{quality}%,文件大小: {file_size_kb:.1f}KB")
                    return encoded_image, file_size_kb, quality, True
            encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 10]
            success, encoded_image = cv2.imencode('.jpg', image, encode_param)
            file_size_kb = len(encoded_image) / 1024
            self.log(f"使用最低质量10%,文件大小: {file_size_kb:.1f}KB")
            return encoded_image, file_size_kb, 10, True
 
        elif output_format == "PNG":
            compression_levels = [1, 3, 5, 7, 9]
            for level in compression_levels:
                encode_param = [int(cv2.IMWRITE_PNG_COMPRESSION), level]
                success, encoded_image = cv2.imencode('.png', image, encode_param)
                if success and len(encoded_image) <= max_size_bytes:
                    file_size_kb = len(encoded_image) / 1024
                    self.log(f"PNG压缩级别{level},文件大小: {file_size_kb:.1f}KB")
                    return encoded_image, file_size_kb, level, True
            encode_param = [int(cv2.IMWRITE_PNG_COMPRESSION), 9]
            success, encoded_image = cv2.imencode('.png', image, encode_param)
            file_size_kb = len(encoded_image) / 1024
            self.log(f"使用最高压缩级别9,文件大小: {file_size_kb:.1f}KB")
            return encoded_image, file_size_kb, 9, True
 
        else:
            success, encoded_image = cv2.imencode('.bmp', image)
            file_size_kb = len(encoded_image) / 1024 if success else 0
            self.log(f"BMP格式无法压缩,文件大小: {file_size_kb:.1f}KB")
            return encoded_image, file_size_kb, 0, False
 
    def detect_face_and_half_body(self, input_image_path, output_image_path, target_size, extend_ratio, output_format):
        try:
            # 检查是否取消处理
            if self.cancel_processing:
                return "cancelled"
 
            original_size_kb = self.get_original_file_size(input_image_path)
            if original_size_kb is not None:
                self.log(f"原始文件大小: {original_size_kb:.1f}KB")
 
            # 读取图像
            with open(input_image_path, 'rb') as f:
                img_data = np.frombuffer(f.read(), dtype=np.uint8)
            image = cv2.imdecode(img_data, cv2.IMREAD_COLOR)
 
            if image is None:
                self.log(f"无法读取图像: {input_image_path}")
                return "read_error"
 
            # 记录图像信息
            h, w = image.shape[:2]
            channels = image.shape[2] if len(image.shape) > 2 else 1
            self.log(f"图像尺寸: {w}x{h}, 通道数: {channels}")
 
            # 如果没有人脸检测器,直接调整尺寸
            if self.face_cascade is None or self.face_cascade.empty():
                self.log(f"无人脸检测器,直接调整尺寸: {os.path.basename(input_image_path)}")
                resized_image = self.resize_to_target(image, target_size)
                output_ext = self.get_extension(output_format)
 
                if self.limit_file_size.get() and output_format in ["JPG", "PNG"]:
                    max_size_kb = self.max_file_size_kb.get()
                    encoded_image, actual_size, quality, was_compressed = self.compress_to_target_size(
                        resized_image, max_size_kb, output_format, original_size_kb
                    )
                    if was_compressed:
                        self.log(f"文件已压缩,质量级别: {quality},实际大小: {actual_size:.1f}KB")
                    else:
                        self.log(f"文件未压缩,使用高质量保存,大小: {actual_size:.1f}KB")
                else:
                    success, encoded_image = self.encode_image(resized_image, output_format, high_quality=True)
 
                if encoded_image is not None:
                    output_dir = os.path.dirname(output_image_path)
                    output_name = os.path.splitext(os.path.basename(output_image_path))[0] + output_ext
                    final_output_path = os.path.join(output_dir, output_name)
                    with open(final_output_path, 'wb') as f:
                        f.write(encoded_image)
                    self.log(f"已调整图像尺寸: {final_output_path}")
                    return "success_no_face_detector"
                else:
                    return "save_error"
 
            # 创建灰度图像用于人脸检测
            gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
            self.log(f"处理图像: {os.path.basename(input_image_path)}")
 
            # 检查是否取消处理
            if self.cancel_processing:
                return "cancelled"
 
            # 使用更严格的人脸检测参数
            start_time = time.time()
            # 增加minNeighbors以减少误检,调整minSize过滤太小的人脸
            faces = self.face_cascade.detectMultiScale(
                gray_image,
                scaleFactor=1.1,
                minNeighbors=7,  # 从5增加到7,减少假阳性
                minSize=(50, 50),  # 从30x30增加到50x50,过滤太小的人脸
                flags=cv2.CASCADE_SCALE_IMAGE
            )
            detection_time = time.time() - start_time
 
            # 添加详细的调试信息
            self.log(f"检测参数: minNeighbors=7, minSize=(50,50)")
            self.log(f"检测到 {len(faces)} 个人脸,耗时 {detection_time:.2f}秒")
 
            if detection_time > 5:
                self.log(f"警告: 人脸检测耗时较长 ({detection_time:.1f}秒)")
 
            if len(faces) == 0:
                self.log(f"在 {os.path.basename(input_image_path)} 中未检测到人脸")
                return "no_face"
 
            # 改进的人脸选择逻辑
            if len(faces) > 1:
                self.log(f"检测到 {len(faces)} 个人脸,选择最佳人脸")
 
                # 计算每个人脸的评分
                face_scores = []
                img_height, img_width = gray_image.shape
 
                for i, (x, y, w, h) in enumerate(faces):
                    # 1. 面积得分 (越大越好)
                    area_score = w * h
 
                    # 2. 位置得分 (越靠近中心越好)
                    center_x = x + w / 2
                    center_y = y + h / 2
                    distance_from_center = np.sqrt(
                        (center_x - img_width / 2) ** 2 + (center_y - img_height / 2) ** 2
                    )
                    # 距离中心越近,位置得分越高
                    position_score = 1.0 / (1.0 + distance_from_center / min(img_width, img_height))
 
                    # 3. 宽高比得分 (接近标准人脸比例0.8-1.2最好)
                    aspect_ratio = w / h
                    if 0.8 <= aspect_ratio <= 1.2:  # 标准人脸宽高比范围
                        aspect_score = 1.0
                    else:
                        # 偏离标准比例越远,得分越低
                        aspect_score = 1.0 / (1.0 + abs(aspect_ratio - 1.0))
 
                    # 4. 边界检查得分 (避免选择太靠近边缘的人脸)
                    margin_ratio = 0.1  # 边界比例
                    margin_x = img_width * margin_ratio
                    margin_y = img_height * margin_ratio
 
                    if (x > margin_x and y > margin_y and
                            x + w < img_width - margin_x and
                            y + h < img_height - margin_y):
                        boundary_score = 1.0
                    else:
                        # 靠近边界的人脸得分降低
                        boundary_score = 0.5
 
                    # 综合得分 (权重可以调整)
                    total_score = (
                            area_score * 0.5 +  # 面积权重最高
                            position_score * 1000 * 0.3 +  # 位置权重
                            aspect_score * 1000 * 0.1 +  # 宽高比权重
                            boundary_score * 1000 * 0.1  # 边界权重
                    )
 
                    face_scores.append((total_score, i, (x, y, w, h)))
 
                    # 详细的调试信息
                    self.log(f"人脸 {i + 1}: 位置({x},{y}) 尺寸({w}x{h}) "
                             f"面积得分{area_score} 位置得分{position_score:.3f} "
                             f"宽高比{aspect_ratio:.2f}得分{aspect_score:.3f} "
                             f"边界得分{boundary_score} 总分: {total_score:.2f}")
 
                # 选择得分最高的人脸
                best_face = max(face_scores, key=lambda x: x[0])
                self.log(f"选择得分最高的人脸 {best_face[1] + 1}, 得分: {best_face[0]:.2f}")
                face = best_face[2]
            else:
                self.log(f"只检测到1个人脸,直接使用")
                face = faces[0]
 
            x, y, w, h = face
            self.log(f"最终选择的人脸: 位置({x},{y}) 尺寸({w}x{h})")
            self.log(f"使用的扩展比例: {extend_ratio}")
 
            center_x = x + w // 2
            center_y = y + h // 2
            target_width, target_height = target_size
            target_aspect_ratio = target_width / target_height
 
            # 重新设计裁剪区域计算逻辑
            # 基于人脸尺寸和扩展比例计算初始裁剪区域
            base_crop_width = int(w * extend_ratio)
            base_crop_height = int(h * extend_ratio)
 
            self.log(f"基础裁剪尺寸: {base_crop_width}x{base_crop_height} (基于扩展比例 {extend_ratio})")
 
            # 确保裁剪区域符合目标宽高比
            if base_crop_width / base_crop_height > target_aspect_ratio:
                # 太宽了,调整高度
                base_crop_height = int(base_crop_width / target_aspect_ratio)
            else:
                # 太高了,调整宽度
                base_crop_width = int(base_crop_height * target_aspect_ratio)
 
            self.log(f"调整后的裁剪尺寸: {base_crop_width}x{base_crop_height} (符合目标宽高比)")
 
            # 确保裁剪区域不小于目标尺寸
            crop_width = max(base_crop_width, target_width)
            crop_height = max(base_crop_height, target_height)
 
            self.log(f"最终裁剪尺寸: {crop_width}x{crop_height} (确保不小于目标尺寸)")
 
            # 计算裁剪边界
            crop_x1 = max(0, center_x - crop_width // 2)
            crop_y1 = max(0, center_y - crop_height // 2)
            crop_x2 = min(image.shape[1], center_x + crop_width // 2)
            crop_y2 = min(image.shape[0], center_y + crop_height // 2)
 
            # 如果裁剪区域超出图像边界,调整中心点
            if crop_x1 == 0:
                crop_x2 = min(image.shape[1], crop_width)
            if crop_y1 == 0:
                crop_y2 = min(image.shape[0], crop_height)
            if crop_x2 == image.shape[1]:
                crop_x1 = max(0, image.shape[1] - crop_width)
            if crop_y2 == image.shape[0]:
                crop_y1 = max(0, image.shape[0] - crop_height)
 
            self.log(f"裁剪区域: ({crop_x1}, {crop_y1}) 到 ({crop_x2}, {crop_y2})")
            self.log(f"实际裁剪尺寸: {crop_x2 - crop_x1}x{crop_y2 - crop_y1}")
 
            # 从原始彩色图像中裁剪半身区域
            half_body_image = image[crop_y1:crop_y2, crop_x1:crop_x2]
            actual_crop_height, actual_crop_width = half_body_image.shape[:2]
            crop_aspect_ratio = actual_crop_width / actual_crop_height
 
            self.log(f"裁剪后尺寸: {actual_crop_width}x{actual_crop_height}, 宽高比: {crop_aspect_ratio:.2f}")
 
            # 调整裁剪后的图像到目标尺寸
            if actual_crop_width >= target_width and actual_crop_height >= target_height:
                self.log("裁剪区域足够大,进行精确裁剪")
                if crop_aspect_ratio > target_aspect_ratio:
                    # 太宽,调整高度
                    new_height = target_height
                    new_width = int(target_height * crop_aspect_ratio)
                    resized = cv2.resize(half_body_image, (new_width, new_height), interpolation=cv2.INTER_AREA)
                    crop_x = (new_width - target_width) // 2
                    resized_image = resized[0:target_height, crop_x:crop_x + target_width]
                else:
                    # 太高,调整宽度
                    new_width = target_width
                    new_height = int(target_width / crop_aspect_ratio)
                    resized = cv2.resize(half_body_image, (new_width, new_height), interpolation=cv2.INTER_AREA)
                    crop_y = (new_height - target_height) // 2
                    resized_image = resized[crop_y:crop_y + target_height, 0:target_width]
            else:
                self.log("裁剪区域较小,使用智能调整方法")
                # 使用新的智能调整方法,基于人脸大小决定处理方式
                # 传递人脸位置信息给resize方法
                # 计算在裁剪图像中的人脸位置
                face_in_crop_x = x - crop_x1
                face_in_crop_y = y - crop_y1
                face_rect_in_crop = (face_in_crop_x, face_in_crop_y, w, h)
 
                resized_image = self.resize_to_target(half_body_image, face_rect_in_crop, target_size)
 
            output_ext = self.get_extension(output_format)
 
            if self.limit_file_size.get() and output_format in ["JPG", "PNG"]:
                max_size_kb = self.max_file_size_kb.get()
                encoded_image, actual_size, quality, was_compressed = self.compress_to_target_size(
                    resized_image, max_size_kb, output_format, original_size_kb
                )
                if was_compressed:
                    self.log(f"文件已压缩,质量级别: {quality},实际大小: {actual_size:.1f}KB")
                else:
                    self.log(f"文件未压缩,使用高质量保存,大小: {actual_size:.1f}KB")
            else:
                success, encoded_image = self.encode_image(resized_image, output_format, high_quality=True)
 
            if encoded_image is not None:
                output_dir = os.path.dirname(output_image_path)
                output_name = os.path.splitext(os.path.basename(output_image_path))[0] + output_ext
                final_output_path = os.path.join(output_dir, output_name)
                with open(final_output_path, 'wb') as f:
                    f.write(encoded_image)
                self.log(f"检测到人脸,半身图像已保存:{final_output_path}")
                return "success"
            else:
                self.log(f"保存图像失败:{output_image_path}")
                return "save_error"
 
        except Exception as e:
            self.log(f"处理图像 {input_image_path} 时出错: {e}")
            self.log(f"详细错误信息: {traceback.format_exc()}")
            return "process_error"
 
    def get_extension(self, format_name):
        format_map = {"JPG": ".jpg", "PNG": ".png", "BMP": ".bmp"}
        return format_map.get(format_name, ".jpg")
 
    def encode_image(self, image, format_name, high_quality=True):
        if format_name == "JPG":
            if high_quality:
                encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 95]
            else:
                encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 85]
            return cv2.imencode('.jpg', image, encode_param)
        elif format_name == "PNG":
            if high_quality:
                encode_param = [int(cv2.IMWRITE_PNG_COMPRESSION), 1]
            else:
                encode_param = [int(cv2.IMWRITE_PNG_COMPRESSION), 3]
            return cv2.imencode('.png', image, encode_param)
        elif format_name == "BMP":
            return cv2.imencode('.bmp', image)
        else:
            encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 95]
            return cv2.imencode('.jpg', image, encode_param)
 
    def process_images(self):
        try:
            input_folder = self.input_folder.get()
            output_folder = self.output_folder.get()
            target_size = (self.target_width.get(), self.target_height.get())
            extend_ratio = self.extend_ratio.get()
            output_format = self.output_format.get()
            limit_size = self.limit_file_size.get()
            max_size_kb = self.max_file_size_kb.get() if limit_size else 0
 
            self.log(f"开始处理...")
            self.log(f"输入文件夹: {input_folder}")
            self.log(f"输出文件夹: {output_folder}")
            self.log(f"目标尺寸: {target_size[0]}x{target_size[1]}")
            self.log(f"扩展比例: {extend_ratio}")
            self.log(f"输出格式: {output_format}")
            if limit_size:
                self.log(f"文件大小限制: {max_size_kb}KB")
            self.flush_log_queue()  # 立即显示重要信息
 
            result = self.batch_detect_face_and_half_body(input_folder, output_folder, target_size, extend_ratio,
                                                          output_format)
 
            self.log("处理完成!")
            self.update_status("处理完成")
            self.update_progress(self.total_files)
 
            if self.cancel_processing:
                messagebox.showinfo("取消", "处理已被用户取消")
            else:
                messagebox.showinfo("完成",
                                    f"处理完成!\n成功: {result['success']}张\n需人工核查: {result['manual_check']}张\n处理失败: {result['errors']}张")
 
        except Exception as e:
            self.log(f"处理过程中出错: {str(e)}")
            messagebox.showerror("错误", f"处理过程中出错: {str(e)}")
        finally:
            self.is_processing = False
            self.process_btn.config(state="normal")
            self.cancel_btn.config(state="disabled")
            self.flush_log_queue()  # 确保所有日志都显示
 
    def batch_detect_face_and_half_body(self, input_folder, output_folder, target_size, extend_ratio, output_format):
        if not os.path.exists(output_folder):
            os.makedirs(output_folder)
            self.log(f"创建输出文件夹: {output_folder}")
 
        # 统计信息
        success_count = 0
        manual_check_count = 0
        process_error_count = 0
        read_error_count = 0
        save_error_count = 0
        success_no_face_detector_count = 0
        cancelled_count = 0
 
        # 获取图片文件
        image_files = [f for f in os.listdir(input_folder) if
                       f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp'))]
        self.total_files = len(image_files)
        self.processed_files = 0
 
        self.log(f"找到 {self.total_files} 个图像文件")
        self.flush_log_queue()  # 立即显示重要信息
 
        self.progress['maximum'] = self.total_files
 
        for i, filename in enumerate(image_files):
            # 检查是否取消处理
            if self.cancel_processing:
                cancelled_count = self.total_files - i
                self.log(f"取消处理,跳过剩余 {cancelled_count} 个文件")
                break
 
            self.update_status(f"处理中: {i + 1}/{self.total_files}")
            input_image_path = os.path.join(input_folder, filename)
 
            # 处理图片
            name, _ = os.path.splitext(filename)
            output_ext = self.get_extension(output_format)
            output_filename = name + output_ext
            output_image_path = os.path.join(output_folder, output_filename)
 
            result = self.detect_face_and_half_body(input_image_path, output_image_path, target_size, extend_ratio,
                                                    output_format)
 
            # 检查是否取消处理
            if result == "cancelled":
                cancelled_count = self.total_files - i
                break
 
            # 更新统计
            if result == "success":
                success_count += 1
            elif result == "success_no_face_detector":
                success_no_face_detector_count += 1
            else:
                name, _ = os.path.splitext(filename)
 
                if result == "no_face":
                    manual_check_filename = f"{name}_需人工核查{output_ext}"
                    manual_check_path = os.path.join(output_folder, manual_check_filename)
                    manual_check_count += 1
                    mark = "需人工核查"
                    output_path = manual_check_path
                elif result == "read_error":
                    read_error_filename = f"{name}_处理失败_读取错误{output_ext}"
                    read_error_path = os.path.join(output_folder, read_error_filename)
                    read_error_count += 1
                    mark = "处理失败_读取错误"
                    output_path = read_error_path
                elif result == "save_error":
                    save_error_filename = f"{name}_处理失败_保存错误{output_ext}"
                    save_error_path = os.path.join(output_folder, save_error_filename)
                    save_error_count += 1
                    mark = "处理失败_保存错误"
                    output_path = save_error_path
                else:
                    process_error_filename = f"{name}_处理失败{output_ext}"
                    process_error_path = os.path.join(output_folder, process_error_filename)
                    process_error_count += 1
                    mark = "处理失败"
                    output_path = process_error_path
 
                self.log(f"处理失败: {filename}, 错误类型: {mark}, 将保存为: {os.path.basename(output_path)}")
 
                try:
                    shutil.copy2(input_image_path, output_path)
                    self.log(f"已复制原图并标记为{mark}: {os.path.basename(output_path)}")
                except Exception as e:
                    self.log(f"复制原图失败: {filename}, 错误: {e}")
                    try:
                        with open(input_image_path, 'rb') as f:
                            img_data = np.frombuffer(f.read(), dtype=np.uint8)
                        original_image = cv2.imdecode(img_data, cv2.IMREAD_COLOR)
                        if original_image is not None:
                            success, encoded_image = self.encode_image(original_image, output_format, high_quality=True)
                            if success:
                                with open(output_path, 'wb') as f:
                                    f.write(encoded_image)
                                self.log(f"已使用OpenCV保存原图并标记为{mark}: {os.path.basename(output_path)}")
                            else:
                                self.log(f"使用OpenCV保存失败: {os.path.basename(output_path)}")
                        else:
                            self.log(f"无法读取原图: {filename}")
                    except Exception as e2:
                        self.log(f"所有保存尝试都失败: {filename}, 错误: {e2}")
 
            # 更新进度
            self.processed_files += 1
            self.update_progress(self.processed_files)
 
            # 定期清理内存
            if i % 20 == 0 and i > 0:
                gc.collect()
                self.log(f"已处理 {i}/{self.total_files} 张图片,内存清理完成")
 
        # 最终刷新日志
        self.flush_log_queue()
 
        # 输出统计结果
        self.log("\n" + "=" * 50)
        self.log("处理完成!统计结果:")
        if self.cancel_processing:
            self.log(f"用户取消了处理")
            self.log(f"已处理: {self.processed_files} 张")
            self.log(f"已取消: {cancelled_count} 张")
        else:
            self.log(f"成功处理(人脸检测): {success_count} 张")
            self.log(f"成功处理(无检测器): {success_no_face_detector_count} 张")
            self.log(f"需人工核查: {manual_check_count} 张")
            self.log(f"读取错误: {read_error_count} 张")
            self.log(f"保存错误: {save_error_count} 张")
            self.log(f"其他处理错误: {process_error_count} 张")
            total_processed = success_count + success_no_face_detector_count + manual_check_count + read_error_count + save_error_count + process_error_count
            self.log(f"总计: {total_processed} 张")
        self.log("=" * 50)
        self.flush_log_queue()
 
        return {
            "success": success_count + success_no_face_detector_count,
            "manual_check": manual_check_count,
            "errors": read_error_count + save_error_count + process_error_count
        }
 
 
def main():
    root = tk.Tk()
    app = PhotoProcessorApp(root)
    root.mainloop()
 
 
if __name__ == "__main__":
    main()
 楼主| ORZtester 发表于 2025-11-22 18:52
大家可以使用置顶的9楼大佬优化过的源码~~我感觉很NICE~~在下面打包放网盘啦
https://www.123865.com/s/OV5A-VWkAd?pwd=52pj#
kingc138 发表于 2025-11-22 15:54
我也有一个学籍处理工具
PixPin_2025-11-22_15-54-32.png

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
majic + 1 + 1 求一个大哥

查看全部评分

songbing490 发表于 2025-11-22 17:48
[Asm] 纯文本查看 复制代码
# filename: 学籍照片批处理工具_优化终极版.py
# 作者:原作者 + Grok 深度重构
# 版本:Version 1.0(2025 重构版)

import sys
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import cv2
import numpy as np
import shutil
import threading
import urllib.request
import traceback
import gc
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

# ====================== 全局人脸检测器单例 ======================
_FACE_CASCADE = None


def get_global_face_cascade():
    global _FACE_CASCADE
    if _FACE_CASCADE is not None and not _FACE_CASCADE.empty():
        return _FACE_CASCADE

    paths = [
        cv2.data.haarcascades + 'haarcascade_frontalface_default.xml',
        resource_path('haarcascade_frontalface_default.xml'),
        'haarcascade_frontalface_default.xml'
    ]

    for p in paths:
        if os.path.exists(p):
            cascade = cv2.CascadeClassifier(p)
            if not cascade.empty():
                _FACE_CASCADE = cascade
                return cascade

    # 下载
    try:
        url = "https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_default.xml"
        os.makedirs(resource_path("."), exist_ok=True)
        download_path = resource_path("haarcascade_frontalface_default.xml")
        if not os.path.exists(download_path):
            urllib.request.urlretrieve(url, download_path)
        cascade = cv2.CascadeClassifier(download_path)
        if not cascade.empty():
            _FACE_CASCADE = cascade
            return cascade
    except:
        pass

    return None


def resource_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)


class PhotoProcessorApp:
    def __init__(self, root):
        self.root = root
        self.root.title("学籍照片批处理工具 - 极速版")
        self.root.geometry("1180x680")
        self.root.resizable(True, True)

        try:
            icon_path = resource_path("icon.ico")
            if os.path.exists(icon_path):
                self.root.iconbitmap(icon_path)
        except:
            pass

        # 变量
        self.input_folder = tk.StringVar()
        self.output_folder = tk.StringVar()
        self.target_width = tk.IntVar(value=413)
        self.target_height = tk.IntVar(value=579)
        self.extend_ratio = tk.DoubleVar(value=2.3)      # 更适合半身
        self.output_format = tk.StringVar(value="JPG")
        self.limit_file_size = tk.BooleanVar(value=True)
        self.max_file_size_kb = tk.IntVar(value=40)
        self.change_background = tk.BooleanVar(value=True)
        self.bg_color_choice = tk.StringVar(value="白色")

        self.is_processing = False
        self.cancel_processing = False
        self.total_files = 0
        self.processed_files = 0

        self.face_cascade = get_global_face_cascade()

        self.setup_ui()
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

    def setup_ui(self):
        style = ttk.Style()
        style.configure("Header.TLabel", font=('Microsoft YaHei', 18, 'bold'))
        style.configure("TLabel", font=('Microsoft YaHei', 10))

        main_frame = ttk.Frame(self.root, padding="15")
        main_frame.grid(row=0, column=0, sticky="nsew")
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(2, weight=1)

        # 标题
        ttk.Label(main_frame, text="学籍照片一键批处理工具", style="Header.TLabel").grid(row=0, column=0, columnspan=2, pady=(0, 15))

        # 左侧控制区
        left_frame = ttk.LabelFrame(main_frame, text=" 设置 ", padding="12")
        left_frame.grid(row=1, column=0, sticky="ew", padx=(0, 8))

        # 文件夹
        for i, (text, var) in enumerate([("输入文件夹:", self.input_folder), ("输出文件夹:", self.output_folder)]):
            ttk.Label(left_frame, text=text).grid(row=i, column=0, sticky="w", pady=4)
            ttk.Entry(left_frame, textvariable=var, width=50).grid(row=i, column=1, padx=5, sticky="ew")
            ttk.Button(left_frame, text="浏览", command=lambda v=var: self.browse_folder(v)).grid(row=i, column=2, padx=5)

        left_frame.columnconfigure(1, weight=1)

        # 参数区
        row = 2
        for label, var, values, width in [
            ("目标尺寸:", None, None, None),
            ("宽度", self.target_width, None, 8),
            ("高度", self.target_height, None, 8),
            ("扩展比例:", self.extend_ratio, None, 10),
            ("输出格式:", self.output_format, ["JPG", "PNG", "BMP"], 10),
            ("大小限制:", self.limit_file_size, None, None),
            ("最大KB:", self.max_file_size_kb, None, 8),
            ("背景替换:", self.change_background, None, None),
            ("目标底色:", self.bg_color_choice, ["白色", "蓝色", "红色"], 10),
        ]:
            ttk.Label(left_frame, text=label).grid(row=row, column=0, sticky="w", pady=4)
            if isinstance(var, tk.BooleanVar):
                ttk.Checkbutton(left_frame, variable=var).grid(row=row, column=1, sticky="w", padx=5)
            elif values:
                ttk.Combobox(left_frame, textvariable=var, values=values, state="readonly", width=width or 15).grid(row=row, column=1, sticky="w", padx=5)
            elif var:
                ttk.Entry(left_frame, textvariable=var, width=width or 15).grid(row=row, column=1, sticky="w", padx=5)
            row += 1

        # 按钮
        btn_frame = ttk.Frame(main_frame)
        btn_frame.grid(row=2, column=0, pady=15, sticky="ew")
        btn_frame.columnconfigure(0, weight=1)
        btn_frame.columnconfigure(3, weight=1)

        self.start_btn = ttk.Button(btn_frame, text="开始处理", command=self.start_processing)
        self.start_btn.grid(row=0, column=1, padx=10)
        self.cancel_btn = ttk.Button(btn_frame, text="取消", command=self.cancel_processing_func, state="disabled")
        self.cancel_btn.grid(row=0, column=2)

        # 进度条
        self.progress = ttk.Progressbar(main_frame, mode='determinate')
        self.progress.grid(row=3, column=0, sticky="ew", pady=8, padx=(0,8))
        self.progress_label = ttk.Label(main_frame, text="就绪")
        self.progress_label.grid(row=3, column=0, pady=(0,5))

        # 右侧日志区
        log_frame = ttk.LabelFrame(main_frame, text=" 处理日志 ")
        log_frame.grid(row=1, column=1, rowspan=3, sticky="nsew", padx=(8,0))
        log_frame.columnconfigure(0, weight=1)
        log_frame.rowconfigure(0, weight=1)

        self.log_text = tk.Text(log_frame, font=('Consolas', 9))
        scrollbar = ttk.Scrollbar(log_frame, command=self.log_text.yview)
        self.log_text.config(yscrollcommand=scrollbar.set)
        self.log_text.grid(row=0, column=0, sticky="nsew")
        scrollbar.grid(row=0, column=1, sticky="ns")

        self.status_label = ttk.Label(main_frame, text="准备就绪", font=('Microsoft YaHei', 10, 'bold'))
        self.status_label.grid(row=4, column=0, columnspan=2, sticky="w", pady=5)

    def browse_folder(self, var):
        folder = filedialog.askdirectory()
        if folder:
            var.set(folder)

    def log(self, msg):
        self.log_text.insert(tk.END, msg + "\n")
        self.log_text.see(tk.END)
        self.root.update_idletasks()

    def start_processing(self):
        if self.is_processing:
            return
        if not all([self.input_folder.get(), self.output_folder.get()]):
            messagebox.showerror("错误", "请填写输入输出文件夹")
            return

        self.is_processing = True
        self.cancel_processing = False
        self.start_btn.config(state="disabled")
        self.cancel_btn.config(state="normal")
        self.log_text.delete(1.0, tk.END)

        threading.Thread(target=self.process_all, daemon=True).start()

    def cancel_processing_func(self):
        self.cancel_processing = True
        self.cancel_btn.config(state="disabled")
        self.log("正在取消,请等待当前图片完成...")

    def process_all(self):
        try:
            input_dir = self.input_folder.get()
            output_dir = self.output_folder.get()
            os.makedirs(output_dir, exist_ok=True)

            files = [f for f in os.listdir(input_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp'))]
            self.total_files = len(files)
            self.progress['maximum'] = self.total_files

            self.log(f"发现 {self.total_files} 张图片,开始多线程处理(4线程)")

            success = fail = manual = 0
            with ThreadPoolExecutor(max_workers=4) as executor:
                futures = {
                    executor.submit(self.process_single, os.path.join(input_dir, f), output_dir): f
                    for f in files
                }

                for future in as_completed(futures):
                    if self.cancel_processing:
                        break
                    result = future.result()
                    if result == "success":
                        success += 1
                    elif result == "manual":
                        manual += 1
                    else:
                        fail += 1

                    self.processed_files += 1
                    self.progress['value'] = self.processed_files
                    self.progress_label.config(text=f"{self.processed_files}/{self.total_files}  成功{success}  需检查{manual}  失败{fail}")

            self.log("所有图片处理完成!")
            messagebox.showinfo("完成", f"成功:{success}\n需人工检查:{manual}\n失败:{fail}")

        except Exception as e:
            self.log(f"错误:{e}")
            messagebox.showerror("错误", str(e))
        finally:
            self.is_processing = False
            self.start_btn.config(state="normal")
            self.cancel_btn.config(state="disabled")

    def replace_background_simple(self, img):
        if not self.change_background.get():
            return img
        target = {"白色": (255,255,255), "蓝色": (255,0,0), "红色": (0,0,255)}[self.bg_color_choice.get()]
        hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
        if self.bg_color_choice.get() == "蓝色":
            lower = np.array([100, 50, 50])
            upper = np.array([130, 255, 255])
        else:  # 白/红用亮度+饱和度低
            lower = np.array([0, 0, 200])
            upper = np.array([180, 50, 255])
        mask = cv2.inRange(hsv, lower, upper)
        img[mask > 0] = target
        return img

    def compress_jpg_fast(self, img, target_kb):
        low, high = 5, 95
        best = None
        best_q = 95
        while low <= high:
            mid = (low + high) // 2
            success, buf = cv2.imencode('.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, mid])
            size_kb = len(buf) / 1024
            if size_kb <= target_kb + 2:
                best = buf
                best_q = mid
                low = mid + 1
            else:
                high = mid - 1
        return best or buf, best_q

    def process_single(self, src_path, output_dir):
        if self.cancel_processing:
            return "cancelled"

        try:
            filename = os.path.basename(src_path)
            name, ext = os.path.splitext(filename)
            img = cv2.imdecode(np.fromfile(src_path, dtype=np.uint8), cv2.IMREAD_COLOR)
            if img is None:
                shutil.copy2(src_path, os.path.join(output_dir, f"{name}_读取失败{ext}"))
                return "fail"

            h, w = img.shape[:2]
            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

            faces = []
            if self.face_cascade and not self.face_cascade.empty():
                min_size = max(50, min(w, h)//10)
                faces = self.face_cascade.detectMultiScale(
                    gray if max(w,h)<=1200 else cv2.resize(gray, (w//2, h//2)),
                    scaleFactor=1.1,
                    minNeighbors=5,
                    minSize=(min_size//2 if max(w,h)>1200 else min_size, )*2,
                    flags=cv2.CASCADE_SCALE_IMAGE
                )
                if len(faces) and max(w,h)>1200:
                    faces[:, :4] *= 2

            if len(faces):
                x, y, fw, fh = sorted(faces, key=lambda f: f[2]*f[3], reverse=True)[0]
                center_x, center_y = x + fw//2, y + fh//2
                crop_w = int(fw * self.extend_ratio.get())
                crop_h = int(crop_w * self.target_width.get() / self.target_height.get())
                x1 = max(0, center_x - crop_w//2)
                y1 = max(0, center_y - crop_h//2)
                x2 = min(w, x1 + crop_w)
                y2 = min(h, y1 + crop_h)
                if x2 - x1 < crop_w: x1 = max(0, x2 - crop_w)
                if y2 - y1 < crop_h: y1 = max(0, y2 - crop_h)
                cropped = img[y1:y2, x1:x2]
            else:
                cropped = img
                self.log(f"未检测到人脸:{filename}")

            resized = cv2.resize(cropped, (self.target_width.get(), self.target_height.get()), interpolation=cv2.INTER_AREA)
            resized = self.replace_background_simple(resized)

            ext_map = {"JPG": ".jpg", "PNG": ".png", "BMP": ".bmp"}
            out_ext = ext_map[self.output_format.get()]

            if self.limit_file_size.get() and self.output_format.get() == "JPG":
                buf, _ = self.compress_jpg_fast(resized, self.max_file_size_kb.get())
            else:
                _, buf = cv2.imencode(out_ext, resized, [cv2.IMWRITE_JPEG_QUALITY, 95] if out_ext==".jpg" else [])

            out_path = os.path.join(output_dir, name + out_ext)
            if len(faces) == 0:
                out_path = os.path.join(output_dir, f"{name}_需人工核查{out_ext}")

            with open(out_path, "wb") as f:
                f.write(buf.tobytes())

            return "success" if len(faces) else "manual"

        except Exception as e:
            self.log(f"处理失败 {os.path.basename(src_path)}: {e}")
            return "fail"

    def on_closing(self):
        if self.is_processing:
            if messagebox.askokcancel("退出", "正在处理,确定退出?"):
                self.cancel_processing = True
                self.root.destroy()
        else:
            self.root.destroy()


if __name__ == "__main__":
    root = tk.Tk()
    app = PhotoProcessorApp(root)
    root.mainloop()

点评

这个感觉不错,还带了改底色功能  发表于 2025-12-2 16:07
songbing490 发表于 2025-11-22 17:49
songbing490 发表于 2025-11-22 17:48
[mw_shl_code=asm,true]# filename: 学籍照片批处理工具_优化终极版.py
# 作者:原作者 + Grok 深度重构
...

那位大佬帮忙转下EXE,这个是用GROK优化后不  不知道好用不好用
 楼主| ORZtester 发表于 2025-11-22 15:49
kingc138 发表于 2025-11-22 15:43
换了一下布局。[mw_shl_code=python,true]import sys
import os
import tkinter as tk

感谢大佬帮忙完善
zorua 发表于 2025-11-22 12:46
厉害!!!
ga826 发表于 2025-11-22 13:48
这个实用,感谢了
afaty 发表于 2025-11-22 14:06
厉害了,兄弟
jw8519888 发表于 2025-11-22 14:11
很实用的软件
yhy123456 发表于 2025-11-22 14:26
牛,感谢分享!
 楼主| ORZtester 发表于 2025-11-22 15:57
kingc138 发表于 2025-11-22 15:54
我也有一个学籍处理工具

真好看~~~
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-6-6 07:05

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表