活动介绍

文件结构:只有.jpg和.json的文件,全在一个文件夹中

时间: 2025-07-28 11:06:35 浏览: 12
<think>我们面临两个主要问题:1.在使用`train_test_split`时出现的`ValueError`,具体是`test_size`参数设置不当以及样本数量为0。2.数据集包含.jpg图像和.json标注文件,需要正确划分并生成相应的训练集和测试集JSON文件(类似引用[1]中的做法)。根据错误信息,核心原因有两个:-数据集为空(n_samples=0),导致无法划分。-当`test_size`为整数时,必须小于样本总数,且大于0。解决方案步骤:###步骤1:检查数据集路径和文件读取确保数据集路径正确,并且能够正确读取到图像文件(.jpg)和对应的标注文件(.json)。假设数据集结构如下:```data_root/├──images/#存放所有.jpg图像└──annotations/#存放所有.json标注文件```我们需要从`images`文件夹中读取所有.jpg文件,并确保每个图像在`annotations`文件夹中有对应的.json文件。###步骤2:获取有效文件列表为了避免样本数量为0,我们需要确认:-图像文件存在-每个图像文件都有对应的标注文件示例代码:```pythonimportosdata_root='path/to/your/dataset'image_dir=os.path.join(data_root,'images')annotation_dir=os.path.join(data_root,'annotations')#获取所有.jpg图像文件名(不带路径)all_image_files=[fforfinos.listdir(image_dir)iff.endswith('.jpg')]#过滤掉没有对应标注文件的图像valid_image_files=[]forimg_fileinall_image_files:#假设标注文件与图像文件同名,只是扩展名为.jsonbase_name=os.path.splitext(img_file)[0]annotation_file=os.path.join(annotation_dir,base_name+'.json')ifos.path.exists(annotation_file):valid_image_files.append(img_file)#检查有效图像数量num_samples=len(valid_image_files)ifnum_samples==0:raiseValueError("Novalidsamplesfound!Checkyourdatasetpathsandfilecorrespondence.")```###步骤3:正确划分数据集使用`train_test_split`时,如果测试集样本数很小(比如2个),则`test_size`应设置为一个浮点数比例(如2/num_samples)而不是整数2,以避免当样本数变化时出错。示例:```pythonfromsklearn.model_selectionimporttrain_test_split#划分训练集和测试集(先分测试集)test_ratio=0.15#或者用具体比例:2/num_samplestrain_files,test_files=train_test_split(valid_image_files,test_size=test_ratio,random_state=42)#如果需要进一步划分验证集,可以从训练集中再分train_files,val_files=train_test_split(train_files,test_size=0.15,random_state=42)print(f"Totalsamples:{num_samples}")print(f"Train:{len(train_files)},Validation:{len(val_files)},Test:{len(test_files)}")```###步骤4:生成JSON文件(类似引用[1])我们需要生成两个JSON文件(train.json和test.json),每个JSON文件应包含对应集合中每个图像的路径和标注信息。假设每个JSON标注文件包含该图像的标注信息(如边界框、关键点等),我们需要将这些信息整合到一个大的JSON文件中。根据引用[1](clean-pvnet处理自定义数据集)的做法,生成的JSON文件可能包含以下结构:```json[{"image_path":"path/to/image1.jpg","annotations":{"bbox":[x,y,width,height],"keypoints":[[x1,y1],[x2,y2],...],"class":"yourclass"}},...]```但是,具体结构取决于你的模型需求。这里我们假设每个原始JSON文件已经包含了所需的标注信息,我们只需将图像路径和对应的标注文件内容关联起来。示例代码生成train.json:```pythonimportjsondefgenerate_json_file(image_files,image_dir,annotation_dir,output_json_path):data_list=[]forimg_fileinimage_files:base_name=os.path.splitext(img_file)[0]annotation_file=os.path.join(annotation_dir,base_name+'.json')#读取标注文件内容withopen(annotation_file,'r')asf:annotation_data=json.load(f)#构建数据项item={"image_path":os.path.join(image_dir,img_file),#或者只保留文件名,根据模型要求"annotations":annotation_data}data_list.append(item)#写入JSON文件withopen(output_json_path,'w')asf:json.dump(data_list,f,indent=2)#生成训练集和测试集的JSON文件generate_json_file(train_files,image_dir,annotation_dir,os.path.join(data_root,'train.json'))generate_json_file(test_files,image_dir,annotation_dir,os.path.join(data_root,'test.json'))```###完整代码示例```pythonimportosimportjsonfromsklearn.model_selectionimporttrain_test_split#设置数据集根目录data_root='D:/dataset/vessel'#请替换为你的实际路径image_dir=os.path.join(data_root,'images')annotation_dir=os.path.join(data_root,'annotations')#步骤1:获取所有图像文件并过滤无效标注all_image_files=[fforfinos.listdir(image_dir)iff.endswith('.jpg')]valid_image_files=[]forimg_fileinall_image_files:base_name=os.path.splitext(img_file)[0]annotation_file=os.path.join(annotation_dir,base_name+'.json')ifos.path.exists(annotation_file):valid_image_files.append(img_file)num_samples=len(valid_image_files)ifnum_samples==0:raiseValueError("Novalidsamplesfound!")#步骤2:划分数据集test_ratio=0.15#测试集比例train_files,test_files=train_test_split(valid_image_files,test_size=test_ratio,random_state=42)#可选:划分验证集val_ratio=0.15#验证集占训练集的比例train_files,val_files=train_test_split(train_files,test_size=val_ratio,random_state=42)print(f"数据集总数:{num_samples}")print(f"训练集:{len(train_files)}")print(f"验证集:{len(val_files)}")print(f"测试集:{len(test_files)}")#步骤3:生成JSON文件defgenerate_json_file(image_files,image_dir,annotation_dir,output_json_path):data_list=[]forimg_fileinimage_files:base_name=os.path.splitext(img_file)[0]annotation_file=os.path.join(annotation_dir,base_name+'.json')withopen(annotation_file,'r')asf:ann_data=json.load(f)#注意:这里存储的图像路径是绝对路径,如果模型需要相对路径,可以调整item={"image_path":os.path.join(image_dir,img_file),"annotations":ann_data}data_list.append(item)withopen(output_json_path,'w')asf:json.dump(data_list,f,indent=2)#生成训练集、验证集和测试集的JSON文件(根据需求生成)generate_json_file(train_files,image_dir,annotation_dir,os.path.join(data_root,'train.json'))generate_json_file(val_files,image_dir,annotation_dir,os.path.join(data_root,'val.json'))generate_json_file(test_files,image_dir,annotation_dir,os.path.join(data_root,'test.json'))```###注意事项1.**路径格式**:在Windows系统中使用正斜杠`/`或反斜杠`\\`,避免单反斜杠(因为它是转义字符)。上面的代码使用`os.path.join`会自动处理。2.**标注文件结构**:上述代码假设每个标注文件的内容可以直接作为`annotations`字段。如果标注文件结构不同,需要调整解析方式。3.**图像路径存储**:存储绝对路径可能在不同机器上运行有问题,可以考虑存储相对于`data_root`的相对路径。###引用文献说明上述方法参考了clean-pvnet处理自定义数据集的方式[^1],通过生成JSON文件来组织训练和测试数据,便于后续模型读取。如果后续遇到其他问题,如样本不均衡或标注文件格式不匹配,需要进一步调整。§§相关问题§§1.如何解决数据集样本不均衡问题?2.标注文件格式与模型要求不一致怎么办?3.除了JSON格式,还有哪些常用的数据集组织方式?4.如何在数据划分时保持类别分布平衡?</think>## 解决 train_test_split 中的 test_size 错误及数据集处理 ### 问题分析 您遇到的 `ValueError` 主要由两个原因引起: 1. **数据集为空**:错误显示 `number of samples 0`,表明 `all_imgs` 为空列表 2. **test_size 参数无效**:整数型 `test_size` 必须小于样本总数 3. **文件结构问题**:数据集包含 .jpg 图像和 .json 标注文件,需要特殊处理 ### 完整解决方案 #### 步骤 1:验证数据集路径和文件读取 ```python import os from sklearn.model_selection import train_test_split # 确认路径存在 data_root = 'D:/dataset/vessel' print(f"数据集根目录存在: {os.path.exists(data_root)}") # 应为 True # 获取所有有效图像文件 image_dir = os.path.join(data_root, 'images') all_images = [f for f in os.listdir(image_dir) if f.endswith(('.jpg', '.png', '.jpeg'))] # 支持多种图像格式 print(f"找到 {len(all_images)} 张图像文件") ``` #### 步骤 2:处理 test_size 参数错误 ```python # 检查样本数量 if len(all_images) == 0: raise ValueError("未找到任何图像文件!请检查路径和文件扩展名") # 使用浮点数比例代替整数 total_samples = len(all_images) test_ratio = 0.15 # 或根据需求计算:test_size = 2/total_samples # 正确划分数据集 train_imgs, test_imgs = train_test_split( all_images, test_size=test_ratio, # 使用比例而非绝对值 random_state=42 ) ``` #### 步骤 3:处理 .jpg 和 .json 文件的数据集 ```python import json # 创建自定义数据集类 class CustomDataset: def __init__(self, data_root, file_list): self.image_dir = os.path.join(data_root, 'images') self.annotation_dir = os.path.join(data_root, 'annotations') self.file_list = file_list def __len__(self): return len(self.file_list) def __getitem__(self, idx): img_name = self.file_list[idx] img_path = os.path.join(self.image_dir, img_name) # 获取对应的 JSON 标注文件 json_name = os.path.splitext(img_name)[0] + '.json' json_path = os.path.join(self.annotation_dir, json_name) # 读取图像和标注 image = load_image(img_path) # 您的图像加载函数 with open(json_path, 'r') as f: annotations = json.load(f) return image, annotations # 创建数据集实例 train_dataset = CustomDataset(data_root, train_imgs) test_dataset = CustomDataset(data_root, test_imgs) ``` #### 步骤 4:生成训练/测试 JSON 文件(参考引用[1]) ```python def generate_json_files(data_root, file_lists, output_names): """ 生成数据集JSON文件 :param data_root: 数据集根目录 :param file_lists: 文件列表字典 {'train': [...], 'test': [...]} :param output_names: 输出文件名 {'train': 'train.json', 'test': 'test.json'} """ for set_name in file_lists: dataset_info = [] for img_name in file_lists[set_name]: json_name = os.path.splitext(img_name)[0] + '.json' json_path = os.path.join(data_root, 'annotations', json_name) # 验证标注文件存在 if not os.path.exists(json_path): print(f"警告: 缺少标注文件 {json_path}") continue # 添加到数据集信息 dataset_info.append({ 'image_path': os.path.join('images', img_name), 'annotation_path': os.path.join('annotations', json_name) }) # 写入JSON文件 output_path = os.path.join(data_root, output_names[set_name]) with open(output_path, 'w') as f: json.dump(dataset_info, f, indent=2) print(f"已生成 {output_path} 包含 {len(dataset_info)} 个样本") # 使用示例 generate_json_files( data_root, file_lists={'train': train_imgs, 'test': test_imgs}, output_names={'train': 'train.json', 'test': 'test.json'} ) ``` ### 关键注意事项 1. **路径验证**: ```python print(os.path.exists('D:/dataset/vessel/images')) # 必须返回 True ``` 使用 `os.path.join` 构建路径确保跨平台兼容性 2. **文件匹配**: ```python # 检查每个图像是否有对应标注 for img in all_images: json_file = os.path.splitext(img)[0] + '.json' if not os.path.exists(os.path.join(annotation_dir, json_file)): print(f"缺失标注: {img}") ``` 3. **小数据集处理**: - 当样本量少时(如引用[2]的情况),使用绝对数量而非比例: ```python if total_samples < 50: test_size = max(1, int(total_samples * 0.1)) # 至少保留1个测试样本 ``` 4. **随机种子**: ```python train_test_split(..., random_state=42) # 确保可复现结果 ``` ### 文件结构建议 ``` D:/dataset/vessel/ ├── images/ │ ├── image1.jpg │ ├── image2.jpg │ └── ... ├── annotations/ │ ├── image1.json │ ├── image2.json │ └── ... ├── train.json # 自动生成 └── test.json # 自动生成 ``` 此解决方案解决了 `test_size` 参数错误问题,同时处理了包含图像和JSON标注的自定义数据集,并生成训练/测试集描述文件,符合 clean-pvnet 的数据格式要求[^1]。
阅读全文

相关推荐

#CSDN YOLOv8+指定“三轮车”(GPU) from pycocotools.coco import COCO from ultralytics import YOLO import os import json # 关键修改:指定桌面annotation文件夹中的JSON文件路径 desktop_path = os.path.expanduser("~/Desktop") # 获取桌面路径 annotation_dir = os.path.join(desktop_path, "annotation") # 拼接annotation文件夹路径 json_path = os.path.join(annotation_dir, "image_info_test2014.json") # 假设JSON文件名为annotations.json # 验证路径是否存在 if not os.path.exists(json_path): print(f"错误:在 {annotation_dir} 中未找到image_info_test2014.json文件") print(f"请检查以下路径:{json_path}") exit(1) # 加载JSON文件 try: with open(json_path, 'r') as f: data = json.load(f) print(f"成功加载JSON文件:{json_path}") except Exception as e: print(f"加载JSON文件失败:{e}") # 验证COCO类别ID coco = COCO('path/to/annotations/instances_train2017.json') tricycle_id = coco.getCatIds(catNms=['tricycle'])[0] # 通常为第65类 print(f"三轮车类别ID: {tricycle_id}") # 三轮车检测专用配置 path: /datasets/coco train: train2017.txt val: val2017.txt test: test2017.txt # 只保留三轮车类别 names: ['tricycle'] nc: 1 # 类别数改为1 # 加载预训练模型 model = YOLO('yolov8s.pt') # 基础模型选择 # 微调训练(关键修改点) results = model.train( data='tricycle.yaml', # 使用自定义配置 epochs=50, # 减少训练轮次 imgsz=640, batch=16, lr0=0.001, # 降低初始学习率 optimizer='Adam', # 优化器选择 single_cls=True, # 单类别训练模式 device='cuda', # GPU加速 pretrained=True, # 使用预训练权重 cache=True # 缓存数据集加速训练 ) def contains_tricycle(image_path, model, conf_thresh=0.5): results = model(image_path) for result in results: # 检查三轮车检测结果 if result.boxes.cls == 0: # 三轮车类别ID confidences = result.boxes.conf if any(conf > conf_thresh for conf in confidences): return True return False # 使用示例 trained_model = YOLO('runs/detect/train/weights/best.pt') print(contains_tricycle('test.jpg', trained_model)) # 验证集性能测试 metrics = model.val(data='tricycle.yaml') print(f"三轮车检测mAP: {metrics.box.map}") # 应关注[email protected] 修改代码为CPU的版本

import os import time import shutil import json from pathlib import Path from ultralytics import YOLO from PIL import Image import watchdog.observers import watchdog.events # 加载预训练的YOLOv8模型 model = YOLO("yolov8s.pt") # 使用yolov8n.pt/yolov8s.pt等不同模型 # 配置路径 INPUT_DIR = "path/to/input/folder" # 监控的图片输入目录 OUTPUT_DIR = "path/to/output/folder" # 检测结果输出目录 PROCESSED_DIR = "path/to/processed/folder" # 已处理图片存储目录 STATE_FILE = "processed_files.json" # 记录已处理文件的状态文件 # 创建必要的目录 Path(INPUT_DIR).mkdir(parents=True, exist_ok=True) Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True) Path(PROCESSED_DIR).mkdir(parents=True, exist_ok=True) # 加载已处理文件记录 processed_files = set() if os.path.exists(STATE_FILE): with open(STATE_FILE, 'r') as f: processed_files = set(json.load(f)) def detect_and_save(image_path): """使用YOLOv8检测图片并分类保存结果""" try: # 执行检测 results = model(image_path) # 获取文件名和扩展名 filename = Path(image_path).name base_name, ext = os.path.splitext(filename) # 处理每个检测结果 for i, result in enumerate(results): # 获取检测到的类别 detected_classes = set() for box in result.boxes: cls_id = int(box.cls) detected_classes.add(model.names[cls_id]) # 如果没有检测到对象,使用'unknown'类别 if not detected_classes: detected_classes.add('unknown') # 为每个检测到的类别保存结果 for cls_name in detected_classes: # 创建分类目录 class_dir = Path(OUTPUT_DIR) / cls_name class_dir.mkdir(parents=True, exist_ok=True) # 保存带检测框的图像 annotated_img = result.plot() # 获取带标注的图像 pil_img = Image.fromarray(annotated_img[..., ::-1]) # BGR转RGB output_path = class_dir / f"{base_name}_{i}{ext}" pil_img.save(str(output_path)) # 保存检测结果为文本文件 result.save_txt(class_dir / f"{base_name}_{i}.txt") return True except Exception as e: print(f"处理 {image_path} 时出错: {e}") return False def process_image(image_path): """处理单个图片文件""" filename = Path(image_path).name if filename in processed_files: print(f"跳过已处理文件: {filename}") return print(f"开始处理: {filename}") if detect_and_save(image_path): # 移动原始文件到已处理目录 processed_path = Path(PROCESSED_DIR) / filename shutil.move(image_path, processed_path) # 更新处理记录 processed_files.add(filename) with open(STATE_FILE, 'w') as f: json.dump(list(processed_files), f) print(f"完成处理: {filename}") class ImageHandler(watchdog.events.FileSystemEventHandler): """监控文件系统事件的处理器""" def on_created(self, event): if not event.is_directory: file_path = event.src_path if file_path.lower().endswith(('.png', '.jpg', '.jpeg')): # 等待文件完全写入 time.sleep(0.5) process_image(file_path) def start_monitoring(): """启动文件夹监控""" observer = watchdog.observers.Observer() event_handler = ImageHandler() observer.schedule(event_handler, INPUT_DIR, recursive=False) observer.start() print(f"开始监控文件夹: {INPUT_DIR}") print("按 Ctrl+C 停止监控...") try: # 处理启动时已存在的文件 for filename in os.listdir(INPUT_DIR): if filename.lower().endswith(('.png', '.jpg', '.jpeg')): file_path = os.path.join(INPUT_DIR, filename) process_image(file_path) # 保持监控运行 while True: time.sleep(1) except KeyboardInterrupt: observer.stop() observer.join() if __name__ == "__main__": start_monitoring() 以上代码有错误请改

### 一、项目背景与目标 - 使用Python爬取数据。 - 目标网站:百度百科《乘风破浪的姐姐》页面。数据获取:https://baike.baidu.com/item/乘风破浪的姐姐 - 目的:获取参赛嘉宾信息,并进行可视化分析。 ### 二、爬虫流程与工具 1. **发送请求**: - 使用requests模块发送HTTP GET请求。 - 设置请求头中的User-Agent以模拟浏览器。 2. **获取响应数据**: - 服务器返回HTML文档。 3. **解析并提取数据**: - 使用BeautifulSoup库解析HTML。 - 查找并提取所需数据(如选手姓名、链接等)。 4. **保存数据**: - 将提取的数据保存为JSON文件。 ### 三、数据爬取与解析 1. **爬取嘉宾信息**: - 从百度百科《乘风破浪的姐姐》页面爬取嘉宾信息表格。 - 查找包含“按姓氏首字母排序”的表格并返回其HTML。 2. **解析嘉宾信息**: - 解析嘉宾信息表格,提取选手姓名和个人百度百科页面链接。 - 将提取的信息保存为stars.json文件。 3. **爬取选手详细信息**: - 遍历stars.json文件中的选手链接。 - 从每个选手的百度百科页面爬取详细信息(如民族、星座、血型、身高、体重、出生日期等)。 - 下载选手图片并保存在以选手姓名命名的文件夹中。 - 将选手的详细信息保存为stars_info.json文件。 ### 四、数据可视化分析 1. **绘制选手年龄分布柱状图**: - 从stars_info.json文件中读取选手的出生日期。 - 计算选手年龄并统计各年龄段的选手数量。 - 使用matplotlib库绘制柱状图。 2. **绘制选手体重饼状图**: - 从stars_info.json文件中读取选手的体重信息。 - 根据体重将选手划分为不同的区间。 - 统计各区间内的选手数量并使用matplotlib库绘制饼状图。 3. **绘制选手身高饼状图**: - 从stars_info.json文件中读取选手的身高信息。 - 根据身高将选手划分为不同的区间。 - 统计各区间内的选手数量并使用matplotlib库绘制饼状图。 ### 五、其他注意事项 - 在进行可视化分析前,需要下载并配置中文字体以确保图表中的中文能够正确显示。 - 文档中提供了两种绘制图表的方法:一种是直接使用matplotlib库;另一种是先将数据读入pandas DataFrame,然后再使用matplotlib库进行绘制。

dorm_face_recognition_gui.py代码如下: import pickle import sys import os import cv2 import numpy as np import torch from PyQt5.QtWidgets import QListWidget, QProgressDialog from facenet_pytorch import MTCNN, InceptionResnetV1 from PIL import Image from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QComboBox, QSlider, QMessageBox, QTextEdit, QGroupBox, QScrollArea, QDialog, QDialogButtonBox, QTableWidget, QTableWidgetItem, QHeaderView, QGridLayout) from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QImage, QPixmap, QIcon, QFont, QColor import joblib import logging import json from datetime import datetime 在 dorm_face_recognition_gui.py 顶部添加导入 from face_recognition import FaceRecognition 配置日志 logging.basicConfig(level=logging.INFO, format=‘%(asctime)s - %(levelname)s - %(message)s’) logger = logging.getLogger(name) class FeedbackDialog(QDialog): “”“反馈对话框”“” def __init__(self, parent=None, last_results=None, dorm_members=None): super().__init__(parent) self.setWindowTitle("识别错误反馈") self.setFixedSize(500, 400) self.last_results = last_results or [] self.dorm_members = dorm_members or [] self.init_ui() def init_ui(self): layout = QVBoxLayout(self) # 添加当前识别结果 result_label = QLabel("当前识别结果:") layout.addWidget(result_label) # 使用表格显示结果 self.results_table = QTableWidget() self.results_table.setColumnCount(4) self.results_table.setHorizontalHeaderLabels(["ID", "识别结果", "置信度", "位置和大小"]) self.results_table.setSelectionBehavior(QTableWidget.SelectRows) self.results_table.setEditTriggers(QTableWidget.NoEditTriggers) # 填充表格数据 self.results_table.setRowCount(len(self.last_results)) for i, result in enumerate(self.last_results): x, y, w, h = result["box"] self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1))) self.results_table.setItem(i, 1, QTableWidgetItem(result["label"])) self.results_table.setItem(i, 2, QTableWidgetItem(f"{result['confidence']:.2f}")) self.results_table.setItem(i, 3, QTableWidgetItem(f"({x}, {y}) - {w}x{h}")) # 设置表格样式 self.results_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.results_table.verticalHeader().setVisible(False) layout.addWidget(self.results_table) # 添加正确身份选择 correct_layout = QGridLayout() correct_label = QLabel("正确身份:") correct_layout.addWidget(correct_label, 0, 0) self.correct_combo = QComboBox() self.correct_combo.addItem("选择正确身份", None) for member in self.dorm_members: self.correct_combo.addItem(member, member) self.correct_combo.addItem("陌生人", "stranger") self.correct_combo.addItem("不在列表中", "unknown") correct_layout.addWidget(self.correct_combo, 0, 1) # 添加备注 note_label = QLabel("备注:") correct_layout.addWidget(note_label, 1, 0) self.note_text = QTextEdit() self.note_text.setPlaceholderText("可添加额外说明...") self.note_text.setMaximumHeight(60) correct_layout.addWidget(self.note_text, 1, 1) layout.addLayout(correct_layout) # 添加按钮 button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) def get_selected_result(self): """获取选择的识别结果""" selected_row = self.results_table.currentRow() if selected_row >= 0 and selected_row < len(self.last_results): return self.last_results[selected_row] return None def get_feedback_data(self): """获取反馈数据""" selected_result = self.get_selected_result() if not selected_result: return None return { "timestamp": datetime.now().isoformat(), "original_label": selected_result["label"], "correct_label": self.correct_combo.currentData(), "confidence": selected_result["confidence"], "box": selected_result["box"], # 保存完整的框信息 "note": self.note_text.toPlainText().strip() } class FaceRecognitionSystem(QMainWindow): def init(self): super().init() self.setWindowTitle(“寝室人脸识别系统”) self.setGeometry(100, 100, 1200, 800) # 初始化变量 self.model_loaded = False self.camera_active = False self.video_capture = None self.timer = QTimer() self.current_image = None self.last_results = [] # 存储上次识别结果 self.dorm_members = [] # 寝室成员列表 # 创建主界面 self.main_widget = QWidget() self.setCentralWidget(self.main_widget) self.layout = QHBoxLayout(self.main_widget) # 左侧控制面板 - 占40%宽度 self.control_panel = QWidget() self.control_layout = QVBoxLayout(self.control_panel) self.control_layout.setAlignment(Qt.AlignTop) self.control_panel.setMaximumWidth(400) self.layout.addWidget(self.control_panel, 40) # 40%宽度 # 右侧图像显示区域 - 占60%宽度 self.image_panel = QWidget() self.image_layout = QVBoxLayout(self.image_panel) self.image_label = QLabel() self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setMinimumSize(800, 600) self.image_label.setStyleSheet("background-color: #333; border: 1px solid #555;") self.image_layout.addWidget(self.image_label) self.layout.addWidget(self.image_panel, 60) # 60%宽度 # 状态栏 self.status_bar = self.statusBar() self.status_bar.showMessage("系统初始化中...") # 初始化人脸识别器 - 关键修复 self.face_recognition = FaceRecognition() # 初始化UI组件 self.init_ui() # 添加工具栏(必须在UI初始化后) self.toolbar = self.addToolBar('工具栏') # 添加反馈按钮 self.add_feedback_button() # 初始化模型 self.init_models() def init_ui(self): """初始化用户界面组件""" # 标题 title_label = QLabel("寝室人脸识别系统") title_label.setFont(QFont("Arial", 18, QFont.Bold)) title_label.setAlignment(Qt.AlignCenter) title_label.setStyleSheet("color: #2c3e50; padding: 10px;") self.control_layout.addWidget(title_label) # 模型加载 model_group = QGroupBox("模型设置") model_layout = QVBoxLayout(model_group) self.load_model_btn = QPushButton("加载模型") self.load_model_btn.setIcon(QIcon.fromTheme("document-open")) self.load_model_btn.setStyleSheet("background-color: #3498db;") self.load_model_btn.clicked.connect(self.load_model) model_layout.addWidget(self.load_model_btn) self.model_status = QLabel("模型状态: 未加载") model_layout.addWidget(self.model_status) self.control_layout.addWidget(model_group) # 在模型设置部分添加重新训练按钮 self.retrain_btn = QPushButton("重新训练模型") self.retrain_btn.setIcon(QIcon.fromTheme("view-refresh")) self.retrain_btn.setStyleSheet("background-color: #f39c12;") self.retrain_btn.clicked.connect(self.retrain_model) self.retrain_btn.setEnabled(False) # 初始不可用 model_layout.addWidget(self.retrain_btn) # 识别设置 settings_group = QGroupBox("识别设置") settings_layout = QVBoxLayout(settings_group) # 置信度阈值 threshold_layout = QHBoxLayout() threshold_label = QLabel("置信度阈值:") threshold_layout.addWidget(threshold_label) self.threshold_slider = QSlider(Qt.Horizontal) self.threshold_slider.setRange(0, 100) self.threshold_slider.setValue(70) self.threshold_slider.valueChanged.connect(self.update_threshold) threshold_layout.addWidget(self.threshold_slider) self.threshold_value = QLabel("0.70") threshold_layout.addWidget(self.threshold_value) settings_layout.addLayout(threshold_layout) # 显示选项 display_layout = QHBoxLayout() display_label = QLabel("显示模式:") display_layout.addWidget(display_label) self.display_combo = QComboBox() self.display_combo.addItems(["原始图像", "检测框", "识别结果"]) self.display_combo.setCurrentIndex(2) display_layout.addWidget(self.display_combo) settings_layout.addLayout(display_layout) self.control_layout.addWidget(settings_group) # 识别功能 recognition_group = QGroupBox("识别功能") recognition_layout = QVBoxLayout(recognition_group) # 图片识别 self.image_recognition_btn = QPushButton("图片识别") self.image_recognition_btn.setIcon(QIcon.fromTheme("image-x-generic")) self.image_recognition_btn.setStyleSheet("background-color: #9b59b6;") self.image_recognition_btn.clicked.connect(self.open_image) self.image_recognition_btn.setEnabled(False) recognition_layout.addWidget(self.image_recognition_btn) # 摄像头识别 self.camera_recognition_btn = QPushButton("启动摄像头识别") self.camera_recognition_btn.setIcon(QIcon.fromTheme("camera-web")) self.camera_recognition_btn.setStyleSheet("background-color: #e74c3c;") self.camera_recognition_btn.clicked.connect(self.toggle_camera) self.camera_recognition_btn.setEnabled(False) recognition_layout.addWidget(self.camera_recognition_btn) self.control_layout.addWidget(recognition_group) # 结果展示区域 - 使用QTextEdit替代QLabel results_group = QGroupBox("识别结果") results_layout = QVBoxLayout(results_group) self.results_text = QTextEdit() self.results_text.setReadOnly(True) self.results_text.setFont(QFont("Microsoft YaHei", 12)) # 使用支持中文的字体 self.results_text.setStyleSheet("background-color: #f8f9fa; border: 1px solid #ddd; padding: 10px;") self.results_text.setPlaceholderText("识别结果将显示在这里") # 添加滚动区域 scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setWidget(self.results_text) results_layout.addWidget(scroll_area) self.control_layout.addWidget(results_group, 1) # 占据剩余空间 # 系统信息 info_group = QGroupBox("系统信息") info_layout = QVBoxLayout(info_group) self.device_label = QLabel(f"计算设备: {'GPU' if torch.cuda.is_available() else 'CPU'}") info_layout.addWidget(self.device_label) self.model_info = QLabel("加载模型以显示信息") info_layout.addWidget(self.model_info) self.control_layout.addWidget(info_group) # 退出按钮 exit_btn = QPushButton("退出系统") exit_btn.setIcon(QIcon.fromTheme("application-exit")) exit_btn.clicked.connect(self.close) exit_btn.setStyleSheet("background-color: #ff6b6b; color: white;") self.control_layout.addWidget(exit_btn) def add_feedback_button(self): """添加反馈按钮到界面""" # 创建反馈按钮 self.feedback_button = QPushButton("提供反馈", self) self.feedback_button.setFixedSize(120, 40) # 设置固定大小 self.feedback_button.setStyleSheet( "QPushButton {" " background-color: #4CAF50;" " color: white;" " border-radius: 5px;" " font-weight: bold;" "}" "QPushButton:hover {" " background-color: #45a049;" "}" ) # 连接按钮点击事件 self.feedback_button.clicked.connect(self.open_feedback_dialog) # 添加到工具栏 self.toolbar.addWidget(self.feedback_button) def open_feedback_dialog(self): """打开反馈对话框""" if not self.last_results: QMessageBox.warning(self, "无法反馈", "没有可反馈的识别结果") return dialog = FeedbackDialog( self, last_results=self.last_results, dorm_members=self.dorm_members ) if dialog.exec_() == QDialog.Accepted: feedback_data = dialog.get_feedback_data() if feedback_data: # 修复:调用 FaceRecognition 实例的 save_feedback 方法 selected_result = dialog.get_selected_result() if selected_result: # 获取检测框 detected_box = [ selected_result["box"][0], selected_result["box"][1], selected_result["box"][0] + selected_result["box"][2], selected_result["box"][1] + selected_result["box"][3] ] # 调用保存反馈方法 self.face_recognition.save_feedback( self.current_image, detected_box, feedback_data["original_label"], feedback_data["correct_label"] ) QMessageBox.information(self, "反馈提交", "感谢您的反馈!数据已保存用于改进模型") else: QMessageBox.warning(self, "反馈错误", "未选择要反馈的人脸结果") def init_models(self): """初始化模型组件""" # 设置设备 self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') self.device_label.setText(f"计算设备: {'GPU' if torch.cuda.is_available() else 'CPU'}") # 初始化人脸检测器 try: self.detector = MTCNN( keep_all=True, post_process=False, device=self.device ) self.status_bar.showMessage("MTCNN 检测器初始化完成") logger.info("MTCNN 检测器初始化完成") except Exception as e: self.status_bar.showMessage(f"MTCNN 初始化失败: {str(e)}") logger.error(f"MTCNN 初始化失败: {str(e)}") return # 初始化人脸特征提取器 try: self.embedder = InceptionResnetV1( pretrained='vggface2', classify=False, device=self.device ).eval() self.status_bar.showMessage("FaceNet 特征提取器初始化完成") logger.info("FaceNet 特征提取器初始化完成") except Exception as e: self.status_bar.showMessage(f"FaceNet 初始化失败: {str(e)}") logger.error(f"FaceNet 初始化失败: {str(e)}") def load_model(self): """加载预训练的SVM分类器""" options = QFileDialog.Options() file_path, _ = QFileDialog.getOpenFileName( self, "选择模型文件", "", "模型文件 (*.pkl);;所有文件 (*)", options=options ) if file_path: try: # 加载模型 model_data = joblib.load(file_path) self.classifier = model_data['classifier'] self.label_encoder = model_data['label_encoder'] self.dorm_members = model_data['dorm_members'] # 启用重新训练按钮 self.retrain_btn.setEnabled(True) # 更新UI状态 self.model_loaded = True self.model_status.setText("模型状态: 已加载") self.model_info.setText(f"寝室成员: {', '.join(self.dorm_members)}") self.image_recognition_btn.setEnabled(True) self.camera_recognition_btn.setEnabled(True) # 状态栏消息 self.status_bar.showMessage(f"模型加载成功: {os.path.basename(file_path)}") # 显示成功消息 QMessageBox.information( self, "模型加载", f"模型加载成功!\n识别成员: {len(self.dorm_members)}人\n置信度阈值: {self.threshold_slider.value() / 100:.2f}" ) except Exception as e: QMessageBox.critical(self, "加载错误", f"模型加载失败: {str(e)}") self.status_bar.showMessage(f"模型加载失败: {str(e)}") def update_threshold(self, value): """更新置信度阈值""" threshold = value / 100 self.threshold_value.setText(f"{threshold:.2f}") self.status_bar.showMessage(f"置信度阈值更新为: {threshold:.2f}") def open_image(self): """打开图片文件进行识别""" if not self.model_loaded: QMessageBox.warning(self, "警告", "请先加载模型!") return options = QFileDialog.Options() file_path, _ = QFileDialog.getOpenFileName( self, "选择识别图片", "", "图片文件 (*.jpg *.jpeg *.png);;所有文件 (*)", options=options ) if file_path: # 读取图片 image = cv2.imread(file_path) if image is None: QMessageBox.critical(self, "错误", "无法读取图片文件!") return # 保存当前图片 self.current_image = image.copy() # 进行识别 self.recognize_faces(image) def toggle_camera(self): """切换摄像头状态""" if not self.model_loaded: QMessageBox.warning(self, "警告", "请先加载模型!") return if not self.camera_active: # 尝试打开摄像头 self.video_capture = cv2.VideoCapture(0) if not self.video_capture.isOpened(): QMessageBox.critical(self, "错误", "无法打开摄像头!") return # 启动摄像头 self.camera_active = True self.camera_recognition_btn.setText("停止摄像头识别") self.camera_recognition_btn.setIcon(QIcon.fromTheme("media-playback-stop")) self.timer.timeout.connect(self.process_camera_frame) self.timer.start(30) # 约33 FPS self.status_bar.showMessage("摄像头已启动") else: # 停止摄像头 self.camera_active = False self.camera_recognition_btn.setText("启动摄像头识别") self.camera_recognition_btn.setIcon(QIcon.fromTheme("camera-web")) self.timer.stop() if self.video_capture: self.video_capture.release() self.status_bar.showMessage("摄像头已停止") def process_camera_frame(self): """处理摄像头帧""" ret, frame = self.video_capture.read() if ret: # 保存当前帧 self.current_image = frame.copy() # 进行识别 self.recognize_faces(frame) def retrain_model(self): """使用反馈数据重新训练模型""" # 获取所有反馈数据 feedback_dir = os.path.join(os.getcwd(), "data", "feedback_data") # 修复1:支持多种文件扩展名 feedback_files = [] for f in os.listdir(feedback_dir): filepath = os.path.join(feedback_dir, f) if os.path.isfile(filepath) and (f.endswith('.pkl') or f.endswith('.json')): feedback_files.append(f) # 修复2:添加目录存在性检查 if not os.path.exists(feedback_dir): QMessageBox.warning(self, "目录不存在", f"反馈数据目录不存在: {feedback_dir}") return if not feedback_files: QMessageBox.information(self, "无反馈数据", "没有找到反馈数据,无法重新训练") return # 确认对话框 reply = QMessageBox.question( self, '确认重新训练', f"将使用 {len(feedback_files)} 条反馈数据重新训练模型。此操作可能需要几分钟时间,确定继续吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply != QMessageBox.Yes: return try: # 创建进度对话框 progress = QProgressDialog("正在重新训练模型...", "取消", 0, len(feedback_files), self) progress.setWindowTitle("模型重新训练") progress.setWindowModality(Qt.WindowModal) progress.setMinimumDuration(0) progress.setValue(0) # 收集所有反馈数据 feedback_data = [] for i, filename in enumerate(feedback_files): filepath = os.path.join(feedback_dir, filename) # 修复3:根据文件扩展名使用不同的加载方式 if filename.endswith('.pkl'): with open(filepath, 'rb') as f: # 二进制模式读取 data = pickle.load(f) elif filename.endswith('.json'): with open(filepath, 'r', encoding='utf-8') as f: data = json.load(f) else: continue # 跳过不支持的文件类型 feedback_data.append(data) progress.setValue(i + 1) QApplication.processEvents() # 保持UI响应 if progress.wasCanceled(): return progress.setValue(len(feedback_files)) # 重新训练模型 self.status_bar.showMessage("正在重新训练模型...") # 修复4:添加详细的日志记录 logger.info(f"开始重新训练,使用 {len(feedback_data)} 条反馈数据") # 调用重新训练方法 success = self.face_recognition.retrain_with_feedback(feedback_data) if success: # 更新UI状态 self.model_status.setText("模型状态: 已重新训练") self.dorm_members = self.face_recognition.dorm_members self.model_info.setText(f"寝室成员: {', '.join(self.dorm_members)}") # 保存更新后的模型 model_path = os.path.join("models", "updated_model.pkl") self.face_recognition.save_updated_model(model_path) QMessageBox.information(self, "训练完成", "模型已成功使用反馈数据重新训练!") else: QMessageBox.warning(self, "训练失败", "重新训练过程中出现问题") except Exception as e: logger.error(f"重新训练失败: {str(e)}") QMessageBox.critical(self, "训练错误", f"重新训练模型时出错: {str(e)}") def recognize_faces(self, image): """识别人脸并在图像上标注结果""" # 清空上次结果 self.last_results = [] # 转换为 PIL 图像 pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) # 检测人脸 boxes, probs, _ = self.detector.detect(pil_image, landmarks=True) # 获取显示选项 display_mode = self.display_combo.currentIndex() # 准备显示图像 display_image = image.copy() # 如果没有检测到人脸 if boxes is None: if display_mode == 2: # 识别结果模式 cv2.putText(display_image, "未检测到人脸", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) self.results_text.setText("未检测到人脸") else: # 提取每个人脸 faces = [] for box in boxes: x1, y1, x2, y2 = box face = pil_image.crop((x1, y1, x2, y2)) faces.append(face) # 提取特征 embeddings = [] if faces and self.model_loaded: # 批量处理所有人脸 face_tensors = [self.preprocess_face(face) for face in faces] if face_tensors: face_tensors = torch.stack(face_tensors).to(self.device) with torch.no_grad(): embeddings = self.embedder(face_tensors).cpu().numpy() # 处理每个人脸 for i, (box, prob) in enumerate(zip(boxes, probs)): x1, y1, x2, y2 = box w, h = x2 - x1, y2 - y1 # 在图像上绘制结果 if display_mode == 0: # 原始图像 # 不绘制任何内容 pass elif display_mode == 1: # 检测框 # 绘制人脸框 cv2.rectangle(display_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) elif display_mode == 2: # 识别结果 # 绘制人脸框 color = (0, 255, 0) # 绿色 # 如果有嵌入向量,则进行识别 if i < len(embeddings): # 预测 probabilities = self.classifier.predict_proba([embeddings[i]])[0] max_prob = np.max(probabilities) pred_class = self.classifier.predict([embeddings[i]])[0] pred_label = self.label_encoder.inverse_transform([pred_class])[0] # 获取置信度阈值 threshold = self.threshold_slider.value() / 100 # 判断是否为陌生人 if max_prob < threshold or pred_label == 'stranger': label = "陌生人" color = (0, 0, 255) # 红色 else: label = pred_label color = (0, 255, 0) # 绿色 # 保存结果用于文本显示 - 修复:保存完整的框信息 result = { "box": [int(x1), int(y1), int(x2 - x1), int(y2 - y1)], # [x, y, width, height] "label": label, "confidence": max_prob } self.last_results.append(result) # 绘制标签 cv2.rectangle(display_image, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) cv2.putText(display_image, f"{label} ({max_prob:.2f})", (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) else: # 无法识别的处理 cv2.rectangle(display_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 165, 255), 2) cv2.putText(display_image, "处理中...", (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 165, 255), 2) # 更新结果文本 self.update_results_text() # 在图像上显示FPS(摄像头模式下) if self.camera_active: fps = self.timer.interval() if fps > 0: cv2.putText(display_image, f"FPS: {1000 / fps:.1f}", (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 2) # 显示图像 self.display_image(display_image) def update_results_text(self): """更新结果文本区域""" if not self.last_results: self.results_text.setText("未识别到任何人脸") return # 构建结果文本 result_text = "识别结果:" for i, result in enumerate(self.last_results, 1): x, y, w, h = result["box"] label = result["label"] confidence = result["confidence"] # 处理中文显示问题 if label in self.dorm_members: result_text += ( f"人脸 #{i}: " f"寝室成员 - {label}
" f"位置: ({x}, {y}), 大小: {w}x{h}, 置信度: {confidence:.2f}
" ) else: result_text += ( f"人脸 #{i}: " f"陌生人
" f"位置: ({x}, {y}), 大小: {w}x{h}, 置信度: {confidence:.2f}
" ) self.results_text.setHtml(result_text) def preprocess_face(self, face_img): """预处理人脸图像""" # 调整大小 face_img = face_img.resize((160, 160)) # 转换为张量并归一化 face_img = np.array(face_img).astype(np.float32) / 255.0 face_img = (face_img - 0.5) / 0.5 # 归一化到[-1, 1] face_img = torch.tensor(face_img).permute(2, 0, 1) # HWC to CHW return face_img def display_image(self, image): """在QLabel中显示图像""" # 将OpenCV图像转换为Qt格式 height, width, channel = image.shape bytes_per_line = 3 * width q_img = QImage(image.data, width, height, bytes_per_line, QImage.Format_RGB888).rgbSwapped() # 缩放图像以适应标签 pixmap = QPixmap.fromImage(q_img) self.image_label.setPixmap(pixmap.scaled( self.image_label.width(), self.image_label.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation )) def closeEvent(self, event): """关闭事件处理""" if self.camera_active: self.timer.stop() if self.video_capture: self.video_capture.release() # 确认退出 reply = QMessageBox.question( self, '确认退出', "确定要退出系统吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: event.accept() else: event.ignore() if name == “main”: app = QApplication(sys.argv) # 设置全局异常处理 def handle_exception(exc_type, exc_value, exc_traceback): """全局异常处理""" import traceback error_msg = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback)) print(f"未捕获的异常:\n{error_msg}") # 记录到文件 with open("error.log", "a") as f: f.write(f"\n\n{datetime.now()}:\n{error_msg}") # 显示给用户 QMessageBox.critical(None, "系统错误", f"发生未处理的异常:\n{str(exc_value)}") sys.exit(1) sys.excepthook = handle_exception window = FaceRecognitionSystem() window.show() sys.exit(app.exec_()) face_model.py代码如下:import os os.environ[‘TF_CPP_MIN_LOG_LEVEL’] = ‘3’ # 禁用 TensorFlow 日志(如果仍有依赖) import cv2 import numpy as np import time import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader from torchvision import transforms from sklearn.svm import SVC from sklearn.preprocessing import LabelEncoder from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score import joblib import logging import sys import glob from facenet_pytorch import MTCNN, InceptionResnetV1 from PIL import Image import gc 配置日志 logging.basicConfig(level=logging.INFO, format=‘%(asctime)s - %(levelname)s - %(message)s’) logger = logging.getLogger(name) def check_gpu_environment(): “”“检查 GPU 环境”“” print(“=” * 60) print(“GPU 环境检查”) print(“=” * 60) # 检查 CUDA 是否可用 print(f"PyTorch 版本: {torch.__version__}") print(f"CUDA 可用: {torch.cuda.is_available()}") if torch.cuda.is_available(): print(f"GPU 数量: {torch.cuda.device_count()}") for i in range(torch.cuda.device_count()): print(f"GPU {i}: {torch.cuda.get_device_name(i)}") print(f" 显存总量: {torch.cuda.get_device_properties(i).total_memory / 1024 ** 3:.2f} GB") print("=" * 60) class FaceDataset(Dataset): “”“人脸数据集类”“” def __init__(self, data_dir, min_samples=10, transform=None): self.data_dir = data_dir self.transform = transform self.faces = [] self.labels = [] self.label_map = {} self.dorm_members = [] self._load_dataset(min_samples) def _load_dataset(self, min_samples): """加载数据集""" # 遍历每个成员文件夹 for member_dir in os.listdir(self.data_dir): member_path = os.path.join(self.data_dir, member_dir) if not os.path.isdir(member_path): continue # 记录寝室成员 self.dorm_members.append(member_dir) self.label_map[member_dir] = len(self.label_map) # 遍历成员的所有照片 member_faces = [] for img_file in os.listdir(member_path): img_path = os.path.join(member_path, img_file) try: # 使用 PIL 加载图像 img = Image.open(img_path).convert('RGB') member_faces.append(img) except Exception as e: logger.warning(f"无法加载图像 {img_path}: {str(e)}") # 确保每个成员有足够样本 if len(member_faces) < min_samples: logger.warning(f"{member_dir} 只有 {len(member_faces)} 个有效样本,至少需要 {min_samples} 个") continue # 添加成员数据 self.faces.extend(member_faces) self.labels.extend([self.label_map[member_dir]] * len(member_faces)) # 添加陌生人样本 stranger_faces = self._generate_stranger_samples(len(self.faces) // 4) self.faces.extend(stranger_faces) self.labels.extend([len(self.label_map)] * len(stranger_faces)) self.label_map['stranger'] = len(self.label_map) logger.info(f"数据集加载完成: {len(self.faces)} 个样本, {len(self.dorm_members)} 个成员") def _generate_stranger_samples(self, num_samples): """生成陌生人样本""" stranger_faces = [] # 使用公开数据集的人脸作为陌生人 # 这里使用 LFW 数据集作为示例(实际项目中应使用真实数据) for _ in range(num_samples): # 生成随机噪声图像(实际应用中应使用真实陌生人照片) random_face = Image.fromarray(np.uint8(np.random.rand(160, 160, 3) * 255)) stranger_faces.append(random_face) return stranger_faces def __len__(self): return len(self.faces) def __getitem__(self, idx): face = self.faces[idx] label = self.labels[idx] if self.transform: face = self.transform(face) return face, label class DormFaceRecognizer: “”“寝室人脸识别系统 (PyTorch 实现)”“” def __init__(self, threshold=0.7, device=None): # 设置设备 self.device = device or ('cuda' if torch.cuda.is_available() else 'cpu') logger.info(f"使用设备: {self.device}") # 初始化人脸检测器 self.detector = MTCNN( keep_all=True, post_process=False, device=self.device ) logger.info("MTCNN 检测器初始化完成") # 初始化人脸特征提取器 self.embedder = InceptionResnetV1( pretrained='vggface2', classify=False, device=self.device ).eval() # 设置为评估模式 logger.info("FaceNet 特征提取器初始化完成") # 初始化其他组件 self.classifier = None self.label_encoder = None self.threshold = threshold self.dorm_members = [] # 数据预处理 self.transform = transforms.Compose([ transforms.Resize((160, 160)), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) ]) def create_dataset(self, data_dir, min_samples=10, batch_size=32, num_workers=4): """创建数据集""" dataset = FaceDataset( data_dir, min_samples=min_samples, transform=self.transform ) # 保存成员信息 self.dorm_members = dataset.dorm_members self.label_encoder = LabelEncoder().fit( list(dataset.label_map.keys()) + ['stranger'] ) # 创建数据加载器 dataloader = DataLoader( dataset, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True ) return dataset, dataloader def extract_features(self, dataloader): """提取人脸特征向量""" embeddings = [] labels = [] logger.info("开始提取特征...") start_time = time.time() with torch.no_grad(): for batch_idx, (faces, batch_labels) in enumerate(dataloader): # 移动到设备 faces = faces.to(self.device) # 提取特征 batch_embeddings = self.embedder(faces) # 保存结果 embeddings.append(batch_embeddings.cpu().numpy()) labels.append(batch_labels.numpy()) # 每10个批次打印一次进度 if (batch_idx + 1) % 10 == 0: elapsed = time.time() - start_time logger.info(f"已处理 {batch_idx + 1}/{len(dataloader)} 批次, 耗时: {elapsed:.2f}秒") # 合并结果 embeddings = np.vstack(embeddings) labels = np.hstack(labels) logger.info(f"特征提取完成: {embeddings.shape[0]} 个样本, 耗时: {time.time() - start_time:.2f}秒") return embeddings, labels def train_classifier(self, embeddings, labels): """训练 SVM 分类器""" logger.info("开始训练分类器...") start_time = time.time() # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split( embeddings, labels, test_size=0.2, random_state=42 ) # 创建并训练 SVM 分类器 self.classifier = SVC(kernel='linear', probability=True, C=1.0) self.classifier.fit(X_train, y_train) # 评估模型 y_pred = self.classifier.predict(X_test) accuracy = accuracy_score(y_test, y_pred) logger.info(f"分类器训练完成, 准确率: {accuracy:.4f}, 耗时: {time.time() - start_time:.2f}秒") return accuracy def recognize_face(self, image): """识别单张图像中的人脸""" # 转换为 PIL 图像 if isinstance(image, np.ndarray): image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) # 检测人脸 boxes, probs, landmarks = self.detector.detect(image, landmarks=True) recognitions = [] if boxes is not None: # 提取每个人脸 faces = [] for box in boxes: x1, y1, x2, y2 = box face = image.crop((x1, y1, x2, y2)) faces.append(face) # 预处理人脸 face_tensors = torch.stack([self.transform(face) for face in faces]).to(self.device) # 提取特征 with torch.no_grad(): embeddings = self.embedder(face_tensors).cpu().numpy() # 预测 probabilities = self.classifier.predict_proba(embeddings) pred_classes = self.classifier.predict(embeddings) for i, (box, prob) in enumerate(zip(boxes, probs)): max_prob = np.max(probabilities[i]) pred_label = self.label_encoder.inverse_transform([pred_classes[i]])[0] # 判断是否为陌生人 if max_prob < self.threshold or pred_label == 'stranger': recognitions.append(("陌生人", max_prob, box)) else: recognitions.append((pred_label, max_prob, box)) return recognitions def save_model(self, file_path): """保存模型""" model_data = { 'classifier': self.classifier, 'label_encoder': self.label_encoder, 'threshold': self.threshold, 'dorm_members': self.dorm_members } joblib.dump(model_data, file_path) logger.info(f"模型已保存至: {file_path}") def load_model(self, file_path): """加载模型""" model_data = joblib.load(file_path) self.classifier = model_data['classifier'] self.label_encoder = model_data['label_encoder'] self.threshold = model_data['threshold'] self.dorm_members = model_data['dorm_members'] logger.info(f"模型已加载,寝室成员: {', '.join(self.dorm_members)}") def main(): “”“主函数”“” print(f"[{time.strftime(‘%H:%M:%S’)}] 程序启动") # 检查 GPU 环境 check_gpu_environment() # 检查并创建必要的目录 os.makedirs('data/dorm_faces', exist_ok=True) # 初始化识别器 try: recognizer = DormFaceRecognizer(threshold=0.6) logger.info("人脸识别器初始化成功") except Exception as e: logger.error(f"初始化失败: {str(e)}") print("程序将在10秒后退出...") time.sleep(10) return # 数据集路径 data_dir = "data/dorm_faces" # 检查数据集是否存在 if not os.path.exists(data_dir) or not os.listdir(data_dir): logger.warning(f"数据集目录 '{data_dir}' 不存在或为空") print("请创建以下结构的目录:") print("dorm_faces/") print("├── 成员1/") print("│ ├── 照片1.jpg") print("│ ├── 照片2.jpg") print("│ └── ...") print("├── 成员2/") print("│ └── ...") print("└── ...") print("\n程序将在10秒后退出...") time.sleep(10) return # 步骤1: 创建数据集 try: dataset, dataloader = recognizer.create_dataset( data_dir, min_samples=10, batch_size=64, num_workers=4 ) except Exception as e: logger.error(f"数据集创建失败: {str(e)}") return # 步骤2: 提取特征 try: embeddings, labels = recognizer.extract_features(dataloader) except Exception as e: logger.error(f"特征提取失败: {str(e)}") return # 步骤3: 训练分类器 try: accuracy = recognizer.train_classifier(embeddings, labels) except Exception as e: logger.error(f"分类器训练失败: {str(e)}") return # 保存模型 model_path = "models/dorm_face_model_pytorch.pkl" try: recognizer.save_model(model_path) except Exception as e: logger.error(f"模型保存失败: {str(e)}") # 测试识别 test_image_path = "test_photo.jpg" if not os.path.exists(test_image_path): logger.warning(f"测试图片 '{test_image_path}' 不存在,跳过识别测试") else: logger.info(f"正在测试识别: {test_image_path}") try: test_image = cv2.imread(test_image_path) if test_image is None: logger.error(f"无法读取图片: {test_image_path}") else: recognitions = recognizer.recognize_face(test_image) if not recognitions: logger.info("未检测到人脸") else: # 在图像上绘制结果 for name, confidence, box in recognitions: x1, y1, x2, y2 = box label = f"{name} ({confidence:.2f})" color = (0, 255, 0) if name != "陌生人" else (0, 0, 255) # 绘制矩形框 cv2.rectangle(test_image, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) # 绘制标签 cv2.putText(test_image, label, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) # 显示结果 cv2.imshow("人脸识别结果", test_image) cv2.waitKey(0) cv2.destroyAllWindows() # 保存结果图像 result_path = "recognition_result_pytorch.jpg" cv2.imwrite(result_path, test_image) logger.info(f"识别结果已保存至: {result_path}") except Exception as e: logger.error(f"人脸识别失败: {str(e)}") logger.info("程序执行完成") if name == “main”: main() face_recognition.py代码如下:import json import cv2 import numpy as np import torch import insightface from insightface.app import FaceAnalysis from facenet_pytorch import InceptionResnetV1 from PIL import Image import joblib import os import pickle from datetime import datetime import random import torch.nn as nn import torch.optim as optim from sklearn.preprocessing import LabelEncoder from sklearn.svm import SVC from torch.utils.data import Dataset, DataLoader class FaceRecognition: def init(self, device=None): self.device = device or torch.device(‘cuda’ if torch.cuda.is_available() else ‘cpu’) self.model_loaded = False self.training_data = {} # 初始化 training_data 属性 self.dorm_members = [] # 初始化 dorm_members 属性 self.label_encoder = LabelEncoder() # 初始化标签编码器 self.init_models() def init_models(self): """初始化人脸识别模型""" try: # 初始化ArcFace模型 - 使用正确的方法 self.arcface_model = FaceAnalysis(providers=['CPUExecutionProvider']) self.arcface_model.prepare(ctx_id=0, det_size=(640, 640)) # 初始化FaceNet模型作为备选 self.facenet_model = InceptionResnetV1( pretrained='vggface2', classify=False, device=self.device ).eval() # 状态标记 self.models_initialized = True print("模型初始化完成") except Exception as e: print(f"模型初始化失败: {str(e)}") self.models_initialized = False def load_classifier(self, model_path): """加载分类器模型""" try: model_data = joblib.load(model_path) self.classifier = model_data['classifier'] self.label_encoder = model_data['label_encoder'] self.dorm_members = model_data['dorm_members'] # 确保加载training_data self.training_data = model_data.get('training_data', {}) self.model_loaded = True print(f"分类器加载成功,成员: {', '.join(self.dorm_members)}") print(f"训练数据包含 {len(self.training_data)} 个类别") return True except Exception as e: print(f"分类器加载失败: {str(e)}") self.model_loaded = False return False def extract_features(self, face_img): """使用ArcFace提取人脸特征""" try: if face_img.size == 0: print("错误:空的人脸图像") return None # 将图像从BGR转换为RGB rgb_img = cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) faces = self.arcface_model.get(rgb_img) if faces: return faces[0].embedding print("未检测到人脸特征") return None except Exception as e: print(f"特征提取失败: {str(e)}") return None def extract_features_facenet(self, face_img): """使用FaceNet提取人脸特征(备选)""" try: # 转换为PIL图像并预处理 face_pil = Image.fromarray(cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB)) face_tensor = self.preprocess_face(face_pil).to(self.device) with torch.no_grad(): features = self.facenet_model(face_tensor.unsqueeze(0)).cpu().numpy()[0] return features except Exception as e: print(f"FaceNet特征提取失败: {str(e)}") return None def preprocess_face(self, face_img): """预处理人脸图像""" # 调整大小 face_img = face_img.resize((160, 160)) # 转换为张量并归一化 face_img = np.array(face_img).astype(np.float32) / 255.0 face_img = (face_img - 0.5) / 0.5 # 归一化到[-1, 1] face_img = torch.tensor(face_img).permute(2, 0, 1) # HWC to CHW return face_img def retrain_with_feedback(self, feedback_data): """使用反馈数据重新训练模型""" # 检查是否有原始训练数据 if not self.training_data: print("错误:没有可用的原始训练数据") return False # 收集原始训练数据 original_features = [] original_labels = [] # 收集特征和标签 for member, embeddings in self.training_data.items(): for emb in embeddings: original_features.append(emb) original_labels.append(member) # 收集反馈数据 feedback_features = [] feedback_labels = [] for feedback in feedback_data: # 获取正确标签 correct_label = feedback.get("correct_label") if not correct_label or correct_label == "unknown": continue # 获取原始图像和人脸位置 image_path = feedback.get("image_path", "") if not image_path or not os.path.exists(image_path): print(f"图像路径无效: {image_path}") continue box = feedback.get("box", []) if len(box) != 4: print(f"无效的人脸框: {box}") continue # 处理图像 image = cv2.imread(image_path) if image is None: print(f"无法读取图像: {image_path}") continue # 裁剪人脸区域 x1, y1, x2, y2 = map(int, box) face_img = image[y1:y2, x1:x2] if face_img.size == 0: print(f"裁剪后的人脸图像为空: {image_path}") continue # 提取特征 embedding = self.extract_features(face_img) if embedding is None: print(f"无法提取特征: {image_path}") continue # 添加到训练数据 feedback_features.append(embedding) feedback_labels.append(correct_label) print(f"添加反馈数据: {correct_label} - {image_path}") # 检查是否有有效的反馈数据 if not feedback_features: print("错误:没有有效的反馈数据") return False # 合并数据 all_features = np.vstack([original_features, feedback_features]) all_labels = original_labels + feedback_labels # 重新训练分类器 self.classifier = SVC(kernel='linear', probability=True) self.classifier.fit(all_features, all_labels) # 更新标签编码器 self.label_encoder = LabelEncoder() self.label_encoder.fit(all_labels) # 更新寝室成员列表 self.dorm_members = list(self.label_encoder.classes_) # 更新训练数据 self.training_data = {} for label, feature in zip(all_labels, all_features): if label not in self.training_data: self.training_data[label] = [] self.training_data[label].append(feature) print(f"重新训练完成! 新模型包含 {len(self.dorm_members)} 个成员") return True def recognize(self, image, threshold=0.7): """识别人脸""" if not self.model_loaded or not self.models_initialized: return [], image.copy() # 使用ArcFace检测人脸 rgb_img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) faces = self.arcface_model.get(rgb_img) results = [] display_img = image.copy() if faces: for face in faces: # 获取人脸框 x1, y1, x2, y2 = face.bbox.astype(int) # 提取特征 embedding = face.embedding # 预测 probabilities = self.classifier.predict_proba([embedding])[0] max_prob = np.max(probabilities) pred_class = self.classifier.predict([embedding])[0] pred_label = self.label_encoder.inverse_transform([pred_class])[0] # 判断是否为陌生人 if max_prob < threshold or pred_label == 'stranger': label = "陌生人" color = (0, 0, 255) # 红色 else: label = pred_label color = (0, 255, 0) # 绿色 # 保存结果 results.append({ "box": [x1, y1, x2, y2], "label": label, "confidence": max_prob }) # 在图像上绘制结果 cv2.rectangle(display_img, (x1, y1), (x2, y2), color, 2) cv2.putText(display_img, f"{label} ({max_prob:.2f})", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) return results, display_img def save_feedback(self, image, detected_box, incorrect_label, correct_label): """保存用户反馈数据 - 改进为保存图像路径而非完整图像""" feedback_dir = "data/feedback_data" os.makedirs(feedback_dir, exist_ok=True) # 创建唯一文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # 保存人脸图像 face_img_dir = os.path.join(feedback_dir, "faces") os.makedirs(face_img_dir, exist_ok=True) face_img_path = os.path.join(face_img_dir, f"face_{timestamp}.jpg") # 裁剪并保存人脸区域 x1, y1, x2, y2 = map(int, detected_box) # 修复1:确保裁剪区域有效 if y2 > y1 and x2 > x1: face_img = image[y1:y2, x1:x2] if face_img.size > 0: cv2.imwrite(face_img_path, face_img) else: logger.warning(f"裁剪的人脸区域无效: {detected_box}") face_img_path = None else: logger.warning(f"无效的检测框: {detected_box}") face_img_path = None # 保存反馈元数据 filename = f"feedback_{timestamp}.json" # 修复2:使用JSON格式 filepath = os.path.join(feedback_dir, filename) # 准备数据 feedback_data = { "image_path": face_img_path, # 保存路径而非完整图像 "detected_box": detected_box, "incorrect_label": incorrect_label, "correct_label": correct_label, "timestamp": timestamp } # 修复3:使用JSON保存便于阅读和调试 with open(filepath, 'w', encoding='utf-8') as f: json.dump(feedback_data, f, ensure_ascii=False, indent=2) return True def save_updated_model(self, output_path): """保存更新后的模型""" model_data = { 'classifier': self.classifier, 'label_encoder': self.label_encoder, 'dorm_members': self.dorm_members, 'training_data': self.training_data # 包含训练数据 } joblib.dump(model_data, output_path) print(f"更新后的模型已保存到: {output_path}") class TripletFaceDataset(Dataset): “”“三元组人脸数据集”“” def __init__(self, embeddings, labels): self.embeddings = embeddings self.labels = labels self.label_to_indices = {} # 创建标签到索引的映射 for idx, label in enumerate(labels): if label not in self.label_to_indices: self.label_to_indices[label] = [] self.label_to_indices[label].append(idx) def __getitem__(self, index): anchor_label = self.labels[index] # 随机选择正样本 positive_idx = index while positive_idx == index: positive_idx = random.choice(self.label_to_indices[anchor_label]) # 随机选择负样本 negative_label = random.choice([l for l in set(self.labels) if l != anchor_label]) negative_idx = random.choice(self.label_to_indices[negative_label]) return ( self.embeddings[index], self.embeddings[positive_idx], self.embeddings[negative_idx] ) def __len__(self): return len(self.embeddings) class TripletLoss(nn.Module): “”“三元组损失函数”“” def __init__(self, margin=1.0): super(TripletLoss, self).__init__() self.margin = margin def forward(self, anchor, positive, negative): distance_positive = (anchor - positive).pow(2).sum(1) distance_negative = (anchor - negative).pow(2).sum(1) losses = torch.relu(distance_positive - distance_negative + self.margin) return losses.mean() def train_triplet_model(embeddings, labels, epochs=100): “”“训练三元组模型”“” dataset = TripletFaceDataset(embeddings, labels) dataloader = DataLoader(dataset, batch_size=32, shuffle=True) model = nn.Sequential( nn.Linear(embeddings.shape[1], 256), nn.ReLU(), nn.Linear(256, 128) ) criterion = TripletLoss(margin=0.5) optimizer = optim.Adam(model.parameters(), lr=0.001) for epoch in range(epochs): total_loss = 0.0 for anchor, positive, negative in dataloader: optimizer.zero_grad() anchor_embed = model(anchor) positive_embed = model(positive) negative_embed = model(negative) loss = criterion(anchor_embed, positive_embed, negative_embed) loss.backward() optimizer.step() total_loss += loss.item() print(f"Epoch {epoch + 1}/{epochs}, Loss: {total_loss / len(dataloader):.4f}") return model main.py代码如下:import sys from dorm_face_recognition_gui import FaceRecognitionSystem from PyQt5.QtWidgets import QApplication if name == “main”: # 设置中文编码支持 if sys.platform == “win32”: import ctypes ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(“dorm.face.recognition”) app = QApplication(sys.argv) app.setStyle("Fusion") # 使用Fusion风格 # 设置应用样式 app.setStyleSheet(""" QMainWindow { background-color: #ecf0f1; } QGroupBox { border: 1px solid #bdc3c7; border-radius: 8px; margin-top: 20px; padding: 10px; font-weight: bold; background-color: #ffffff; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top center; padding: 0 5px; } QPushButton { background-color: #3498db; color: white; border: none; padding: 10px 15px; font-size: 14px; margin: 5px; border-radius: 5px; } QPushButton:hover { background-color: #2980b9; } QPushButton:pressed { background-color: #1c6ea4; } QPushButton:disabled { background-color: #bdc3c7; } QLabel { font-size: 14px; padding: 3px; } QComboBox, QSlider { padding: 4px; background-color: #ffffff; } QTextEdit { font-family: "Microsoft YaHei"; font-size: 12px; } """) window = FaceRecognitionSystem() window.show() sys.exit(app.exec_()) ui.py代码如下:from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QComboBox, QSlider, QMessageBox, QTextEdit, QGroupBox, QScrollArea, QDialog, QListWidget) from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QImage, QPixmap, QIcon, QFont from face_recognition import FaceRecognition class FaceRecognitionSystem(QMainWindow): def init(self): super().init() # … 原有初始化代码 … # 初始化人脸识别器 self.face_recognition = FaceRecognition() # 添加反馈按钮 self.add_feedback_button() def add_feedback_button(self): """添加反馈按钮到界面""" self.feedback_btn = QPushButton("反馈识别错误") self.feedback_btn.setIcon(QIcon.fromTheme("dialog-warning")) self.feedback_btn.setStyleSheet("background-color: #f39c12;") self.feedback_btn.clicked.connect(self.handle_feedback) # 找到识别功能组并添加按钮 for i in range(self.control_layout.count()): widget = self.control_layout.itemAt(i).widget() if isinstance(widget, QGroupBox) and widget.title() == "识别功能": layout = widget.layout() layout.addWidget(self.feedback_btn) break def handle_feedback(self): """处理用户反馈""" if not hasattr(self, 'last_results') or not self.last_results: QMessageBox.warning(self, "警告", "没有可反馈的识别结果") return # 创建反馈对话框 dialog = QDialog(self) dialog.setWindowTitle("识别错误反馈") dialog.setFixedSize(400, 300) layout = QVBoxLayout(dialog) # 添加当前识别结果 result_label = QLabel("当前识别结果:") layout.addWidget(result_label) self.feedback_list = QListWidget() for i, result in enumerate(self.last_results, 1): label = result["label"] confidence = result["confidence"] self.feedback_list.addItem(f"人脸 #{i}: {label} (置信度: {confidence:.2f})") layout.addWidget(self.feedback_list) # 添加正确身份选择 correct_label = QLabel("正确身份:") layout.addWidget(correct_label) self.correct_combo = QComboBox() self.correct_combo.addItems(["选择正确身份"] + self.face_recognition.dorm_members + ["陌生人", "不在列表中"]) layout.addWidget(self.correct_combo) # 添加按钮 btn_layout = QHBoxLayout() submit_btn = QPushButton("提交反馈") submit_btn.clicked.connect(lambda: self.submit_feedback(dialog)) btn_layout.addWidget(submit_btn) cancel_btn = QPushButton("取消") cancel_btn.clicked.connect(dialog.reject) btn_layout.addWidget(cancel_btn) layout.addLayout(btn_layout) dialog.exec_() def submit_feedback(self, dialog): """提交反馈并更新模型""" selected_index = self.feedback_list.currentRow() if selected_index < 0: QMessageBox.warning(self, "警告", "请选择一个识别结果") return result = self.last_results[selected_index] correct_identity = self.correct_combo.currentText() if correct_identity == "选择正确身份": QMessageBox.warning(self, "警告", "请选择正确身份") return # 保存反馈数据 self.face_recognition.save_feedback( self.current_image.copy(), result["box"], result["label"], correct_identity ) QMessageBox.information(self, "反馈提交", "感谢您的反馈!数据已保存用于改进模型") dialog.accept() def recognize_faces(self, image): """识别人脸并在图像上标注结果""" # 使用人脸识别器进行识别 self.last_results, display_image = self.face_recognition.recognize( image, threshold=self.threshold_slider.value() / 100 ) # 更新结果文本 self.update_results_text() # 显示图像 self.display_image(display_image) def update_results_text(self): """更新结果文本区域""" if not self.last_results: self.results_text.setText("未识别到任何人脸") return # 构建结果文本 result_text = "<span>识别结果:</span>" for i, result in enumerate(self.last_results, 1): x1, y1, x2, y2 = result["box"] label = result["label"] confidence = result["confidence"] # 处理中文显示问题 if label in self.face_recognition.dorm_members: result_text += ( f"人脸 #{i}: " f"寝室成员 - {label}
" f"位置: ({x1}, {y1}), 置信度: {confidence:.2f}
" ) else: result_text += ( f"人脸 #{i}: " f"陌生人
" f"位置: ({x1}, {y1}), 置信度: {confidence:.2f}
" ) self.results_text.setHtml(result_text) # ... 其余原有方法 ... 需要把重新训练模型部分和反馈部分全部删除

帮我增加一个功能,框选好的尺寸可以进行保存在主界面,方便下次点击就能调用 import os import tkinter as tk from tkinter import filedialog, messagebox, ttk from PIL import Image, ImageTk, UnidentifiedImageError class ImageCropperApp: def __init__(self, root): self.root = root self.root.title("批量图片裁剪工具") self.root.geometry("900x600") self.root.minsize(800, 500) # 设置中文字体支持 self.style = ttk.Style() self.style.configure('.', font=('SimHei', 10)) # 初始化变量 self.images = [] # 存储所有待处理图片路径 self.current_image_index = 0 self.current_image = None self.current_photo = None self.canvas_image = None self.selection_start = None self.selection_rect = None self.crop_box = None self.output_dir = "" # 创建界面 self.create_widgets() def create_widgets(self): # 顶部控制区 top_frame = ttk.Frame(self.root, padding=10) top_frame.pack(fill=tk.X) ttk.Button(top_frame, text="选择图片文件夹", command=self.select_image_folder).pack(side=tk.LEFT, padx=5) ttk.Button(top_frame, text="设置输出文件夹", command=self.set_output_folder).pack(side=tk.LEFT, padx=5) self.status_var = tk.StringVar(value="请选择图片文件夹") ttk.Label(top_frame, textvariable=self.status_var).pack(side=tk.LEFT, padx=10) # 中部画布区 self.canvas_frame = ttk.Frame(self.root) self.canvas_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) self.canvas = tk.Canvas(self.canvas_frame, bg="white", highlightthickness=1) self.canvas.pack(fill=tk.BOTH, expand=True) # 底部控制区 bottom_frame = ttk.Frame(self.root, padding=10) bottom_frame.pack(fill=tk.X) ttk.Button(bottom_frame, text="上一张", command=self.prev_image).pack(side=tk.LEFT, padx=5) ttk.Button(bottom_frame, text="下一张", command=self.next_image).pack(side=tk.LEFT, padx=5) ttk.Label(bottom_frame, text="裁剪尺寸:").pack(side=tk.LEFT, padx=5) self.width_var = tk.StringVar(value="200") self.height_var = tk.StringVar(value="200") ttk.Entry(bottom_frame, textvariable=self.width_var, width=5).pack(side=tk.LEFT, padx=2) ttk.Label(bottom_frame, text="x").pack(side=tk.LEFT, padx=2) ttk.Entry(bottom_frame, textvariable=self.height_var, width=5).pack(side=tk.LEFT, padx=2) ttk.Button(bottom_frame, text="应用裁剪尺寸", command=self.apply_custom_crop_size).pack(side=tk.LEFT, padx=5) ttk.Button(bottom_frame, text="批量裁剪全部图片", command=self.batch_crop_images).pack(side=tk.LEFT, padx=5) self.image_counter = tk.StringVar(value="0/0") ttk.Label(bottom_frame, textvariable=self.image_counter).pack(side=tk.RIGHT, padx=10) # 绑定画布事件 self.canvas.bind("<Button-1>", self.on_canvas_click) self.canvas.bind("<B1-Motion>", self.on_canvas_drag) self.canvas.bind("<ButtonRelease-1>", self.on_canvas_release) # 滚动支持 self.canvas_frame.bind("<Configure>", self.on_canvas_configure) self.canvas.bind("<MouseWheel>", self.on_mousewheel) def select_image_folder(self): folder = filedialog.askdirectory(title="选择图片文件夹") if folder: self.images = [] for filename in os.listdir(folder): if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')): self.images.append(os.path.join(folder, filename)) if self.images: self.current_image_index = 0 self.load_image() self.status_var.set(f"已加载 {len(self.images)} 张图片") self.image_counter.set(f"{self.current_image_index + 1}/{len(self.images)}") else: messagebox.showinfo("提示", "所选文件夹中没有找到支持的图片格式") def set_output_folder(self): folder = filedialog.askdirectory(title="设置输出文件夹") if folder: self.output_dir = folder self.status_var.set(f"输出文件夹已设置: {folder}") def load_image(self): if 0 <= self.current_image_index < len(self.images): try: self.current_image = Image.open(self.images[self.current_image_index]) self.display_image() self.crop_box = None self.selection_start = None self.selection_rect = None except UnidentifiedImageError: messagebox.showerror("错误", f"无法打开图片: {self.images[self.current_image_index]}") self.next_image() def display_image(self): # 清除画布 self.canvas.delete("all") # 调整图片大小以适应画布 canvas_width = self.canvas.winfo_width() canvas_height = self.canvas.winfo_height() if canvas_width <= 1 or canvas_height <= 1: # 画布尚未完全初始化 return img_width, img_height = self.current_image.size ratio = min(canvas_width / img_width, canvas_height / img_height) display_width = int(img_width * ratio) display_height = int(img_height * ratio) self.current_photo = ImageTk.PhotoImage( self.current_image.resize((display_width, display_height), Image.LANCZOS)) self.canvas_image = self.canvas.create_image(canvas_width // 2, canvas_height // 2, image=self.current_photo) # 如果有裁剪框,重新绘制 if self.crop_box: x1, y1, x2, y2 = self.crop_box # 转换为画布坐标 x1_canvas = x1 * ratio + canvas_width // 2 - display_width // 2 y1_canvas = y1 * ratio + canvas_height // 2 - display_height // 2 x2_canvas = x2 * ratio + canvas_width // 2 - display_width // 2 y2_canvas = y2 * ratio + canvas_height // 2 - display_height // 2 self.selection_rect = self.canvas.create_rectangle( x1_canvas, y1_canvas, x2_canvas, y2_canvas, outline="red", width=2 ) def on_canvas_click(self, event): if not self.current_image: return self.selection_start = (event.x, event.y) # 如果已经有选择框,删除它 if self.selection_rect: self.canvas.delete(self.selection_rect) self.selection_rect = None def on_canvas_drag(self, event): if not self.current_image or not self.selection_start: return # 如果已经有选择框,删除它 if self.selection_rect: self.canvas.delete(self.selection_rect) # 创建新的选择框 x1, y1 = self.selection_start x2, y2 = event.x, event.y self.selection_rect = self.canvas.create_rectangle(x1, y1, x2, y2, outline="red", width=2) def on_canvas_release(self, event): if not self.current_image or not self.selection_start: return # 获取选择框的坐标 x1, y1 = self.selection_start x2, y2 = event.x, event.y # 确保坐标按正确顺序排列 x1, x2 = min(x1, x2), max(x1, x2) y1, y2 = min(y1, y2), max(y1, y2) # 转换为图像坐标 canvas_width = self.canvas.winfo_width() canvas_height = self.canvas.winfo_height() img_width, img_height = self.current_image.size ratio = min(canvas_width / img_width, canvas_height / img_height) img_x1 = int((x1 - canvas_width // 2 + img_width * ratio // 2) / ratio) img_y1 = int((y1 - canvas_height // 2 + img_height * ratio // 2) / ratio) img_x2 = int((x2 - canvas_width // 2 + img_width * ratio // 2) / ratio) img_y2 = int((y2 - canvas_height // 2 + img_height * ratio // 2) / ratio) # 确保坐标在图像范围内 img_x1 = max(0, min(img_x1, img_width)) img_y1 = max(0, min(img_y1, img_height)) img_x2 = max(0, min(img_x2, img_width)) img_y2 = max(0, min(img_y2, img_height)) # 保存裁剪框 self.crop_box = (img_x1, img_y1, img_x2, img_y2) # 显示裁剪尺寸信息 width = img_x2 - img_x1 height = img_y2 - img_y1 self.status_var.set(f"已选择裁剪区域: {width}x{height} 像素") def apply_custom_crop_size(self): try: width = int(self.width_var.get()) height = int(self.height_var.get()) if width <= 0 or height <= 0: messagebox.showerror("错误", "裁剪尺寸必须为正整数") return if not self.crop_box: messagebox.showinfo("提示", "请先在图片上选择一个裁剪区域") return # 获取当前裁剪框的中心点 x1, y1, x2, y2 = self.crop_box center_x = (x1 + x2) // 2 center_y = (y1 + y2) // 2 # 计算新的裁剪框坐标 new_x1 = max(0, center_x - width // 2) new_y1 = max(0, center_y - height // 2) new_x2 = min(self.current_image.width, new_x1 + width) new_y2 = min(self.current_image.height, new_y1 + height) # 调整裁剪框大小,保持中心点不变 if new_x2 - new_x1 < width: diff = width - (new_x2 - new_x1) new_x1 = max(0, new_x1 - diff // 2) new_x2 = new_x1 + width if new_y2 - new_y1 < height: diff = height - (new_y2 - new_y1) new_y1 = max(0, new_y1 - diff // 2) new_y2 = new_y1 + height self.crop_box = (new_x1, new_y1, new_x2, new_y2) # 刷新显示 self.display_image() self.status_var.set(f"已应用自定义裁剪尺寸: {width}x{height} 像素") except ValueError: messagebox.showerror("错误", "请输入有效的整数尺寸") def crop_current_image(self): if not self.crop_box: messagebox.showinfo("提示", "请先选择裁剪区域") return if not self.output_dir: messagebox.showinfo("提示", "请先设置输出文件夹") return x1, y1, x2, y2 = self.crop_box cropped_img = self.current_image.crop((x1, y1, x2, y2)) # 保存裁剪后的图片 filename = os.path.basename(self.images[self.current_image_index]) name, ext = os.path.splitext(filename) output_path = os.path.join(self.output_dir, f"{name}_cropped{ext}") try: # 保存时保持原画质 if ext.lower() in ('.jpg', '.jpeg'): cropped_img.save(output_path, quality=100, subsampling=0) else: cropped_img.save(output_path) self.status_var.set(f"已裁剪并保存图片: {output_path}") return True except Exception as e: messagebox.showerror("错误", f"保存图片时出错: {str(e)}") return False def batch_crop_images(self): if not self.crop_box: messagebox.showinfo("提示", "请先在第一张图片上选择裁剪区域") return if not self.output_dir: messagebox.showinfo("提示", "请先设置输出文件夹") return total = len(self.images) success_count = 0 error_count = 0 # 保存当前状态 current_index = self.current_image_index original_crop_box = self.crop_box # 批量处理所有图片 for i, img_path in enumerate(self.images): try: # 更新状态 self.status_var.set(f"正在处理图片 {i + 1}/{total}...") self.root.update() # 打开图片 self.current_image = Image.open(img_path) # 使用相同的裁剪框 self.crop_box = original_crop_box # 裁剪并保存 x1, y1, x2, y2 = self.crop_box cropped_img = self.current_image.crop((x1, y1, x2, y2)) filename = os.path.basename(img_path) name, ext = os.path.splitext(filename) output_path = os.path.join(self.output_dir, f"{name}_cropped{ext}") if ext.lower() in ('.jpg', '.jpeg'): cropped_img.save(output_path, quality=100, subsampling=0) else: cropped_img.save(output_path) success_count += 1 except Exception as e: error_count += 1 print(f"处理图片 {img_path} 时出错: {str(e)}") # 恢复状态 self.current_image_index = current_index self.load_image() # 显示结果 result_msg = f"批量裁剪完成!\n成功: {success_count} 张\n失败: {error_count} 张" self.status_var.set(result_msg) messagebox.showinfo("批量裁剪结果", result_msg) def prev_image(self): if self.images and self.current_image_index > 0: self.current_image_index -= 1 self.load_image() self.image_counter.set(f"{self.current_image_index + 1}/{len(self.images)}") def next_image(self): if self.images and self.current_image_index < len(self.images) - 1: self.current_image_index += 1 self.load_image() self.image_counter.set(f"{self.current_image_index + 1}/{len(self.images)}") def on_canvas_configure(self, event): # 画布大小改变时重绘图像 if self.current_image: self.display_image() def on_mousewheel(self, event): # 处理鼠标滚轮缩放 if self.current_image: # 缩放逻辑可以在这里实现 pass if __name__ == "__main__": root = tk.Tk() app = ImageCropperApp(root) root.mainloop()

最新推荐

recommend-type

C#遍历文件夹及子目录下所有图片

在C#编程中,遍历文件夹及其子目录下的所有图片是一项常见的任务,尤其是在处理大量图像数据时。本文将详细讲解如何使用C#实现这一功能,并结合JavaScript展示这些图片。 首先,我们需要用到C#的`System.IO`命名...
recommend-type

三菱FX3U三轴伺服电机与威纶通触摸屏组合程序详解:轴点动、回零与定位控制及全流程解析

三菱FX3U三轴伺服电机与威纶通触摸屏的程序编写方法及其应用。主要内容涵盖伺服电机主控程序、触摸屏程序、轴点动、回零及定位程序、通讯模块程序以及威纶显示器程序的分析。通过对各个模块的深入探讨,帮助读者理解每个部分的功能和实现方式,确保机械运动控制的准确性、高效性和稳定性。此外,文章还提供了关于程序编写过程中可能遇到的问题及解决方案。 适合人群:从事自动化控制领域的工程师和技术人员,尤其是对三菱FX3U三轴伺服电机和威纶通触摸屏有实际操作需求的专业人士。 使用场景及目标:适用于工业自动化项目中,旨在提高对三菱FX3U三轴伺服电机和威纶通触摸屏的理解和应用能力,掌握模块化编程技巧,解决实际工程中的编程难题。 其他说明:文中不仅讲解了各模块的具体实现细节,还强调了程序的安全性和可靠性,为项目的成功实施提供了有力的支持。
recommend-type

Pansophica开源项目:智能Web搜索代理的探索

Pansophica开源项目是一个相对较新且具有创新性的智能Web搜索代理,它突破了传统搜索引擎的界限,提供了一种全新的交互方式。首先,我们来探讨“智能Web搜索代理”这一概念。智能Web搜索代理是一个软件程序或服务,它可以根据用户的查询自动执行Web搜索,并尝试根据用户的兴趣、历史搜索记录或其他输入来提供个性化的搜索结果。 Pansophica所代表的不仅仅是搜索结果的展示,它还强调了一个交互式的体验,在动态和交互式虚拟现实中呈现搜索结果。这种呈现方式与现有的搜索体验有着根本的不同。目前的搜索引擎,如Google、Bing和Baidu等,多以静态文本和链接列表的形式展示结果。而Pansophica通过提供一个虚拟现实环境,使得搜索者可以“扭转”视角,进行“飞行”探索,以及“弹网”来浏览不同的内容。这种多维度的交互方式使得信息的浏览变得更加快速和直观,有望改变用户与网络信息互动的方式。 接着,我们关注Pansophica的“开源”属性。所谓开源,指的是软件的源代码可以被公众获取,任何个人或组织都可以自由地使用、学习、修改和分发这些代码。开源软件通常由社区进行开发和维护,这样的模式鼓励了协作创新并减少了重复性劳动,因为全世界的开发者都可以贡献自己的力量。Pansophica项目作为开源软件,意味着其他开发者可以访问和使用其源代码,进一步改进和扩展其功能,甚至可以为Pansophica构建新的应用或服务。 最后,文件名称“Pansophica-src-1.3”表明了我们讨论的特定版本的Pansophica开源代码。数字“1.3”很可能指的是该版本号,表明这是Pansophica项目的第1.3个公开版本。这个版本号对于开发者来说很重要,因为它可以帮助他们追踪更新的进度、比较版本间的差异以及选择最适合其项目的版本。 综合以上信息,Pansophica开源项目不仅在搜索代理领域引入了创新的交互方式,而且通过开源的方式,提供了技术共享和协作的可能性。对于IT行业来说,开源项目是推动技术发展的重要力量,它激发了社区的活力,促进了知识的传播和技术的进步。通过构建一个开源社区,Pansophica可以吸引来自世界各地的开发者和研究人员,共同探索如何将搜索技术与交互式虚拟现实技术结合起来,为用户带来更加丰富和有效的信息检索体验。同时,Pansophica的开源性质也使其更容易适应未来技术的发展,随着社区的不断贡献和反馈,这一平台有望在功能和性能上得到持续的优化和增强。
recommend-type

跨平台内容提取无忧:coze工作流应对社交媒体挑战

# 1. 社交媒体内容提取的必要性与挑战 ## 1.1 内容提取的必要性 在数字时代,社交媒体内容是信息与数据流动的主干道,公司、个人和研究者都在寻求从这些信息中提取价值。无论是为了市场趋势分析、品牌监控、消费者洞察还是公共舆论监测,有效的社交媒体内容提取变得至关重要。这不仅有助于企业做出数据驱动的决策,也能使个人更好地理
recommend-type

vrrp主设备发送的免费arp

<think>好的,我现在需要回答用户关于VRRP主设备发送免费ARP的目的和工作原理的问题。首先,我得回忆一下VRRP的基本概念和工作机制。VRRP,虚拟路由冗余协议,主要用于在局域网中提供默认网关的冗余备份,确保网络的高可用性。主设备(Master)负责转发流量,而备用设备(Backup)则在主设备故障时接管工作。 用户的问题集中在主设备发送免费ARP的目的和机制上。根据引用[2],免费ARP用于通知下游设备虚拟MAC地址的变更。当主设备被选举出来后,它需要让局域网内的其他设备知道虚拟IP对应的MAC地址已经指向自己,这样流量才会被正确路由到主设备。免费ARP的作用应该就是更新这些设备的
recommend-type

为Ghost博客平台打造的Meteor流星包装使用指南

从给定文件信息中,我们可以提炼出以下IT知识点: ### 标题知识点:流星Ghost软件包 1. **流星Ghost软件包的用途**:流星Ghost软件包是专为Ghost博客平台设计的流星(Meteor)应用程序。流星是一个开源的全栈JavaScript平台,用于开发高性能和易于编写的Web应用程序。Ghost是一个开源博客平台,它提供了一个简单且专业的写作环境。 2. **软件包的作用**:流星Ghost软件包允许用户在流星平台上轻松集成Ghost博客。这样做的好处是可以利用流星的实时特性以及易于开发和部署的应用程序框架,同时还能享受到Ghost博客系统的便利和美观。 ### 描述知识点:流星Ghost软件包的使用方法 1. **软件包安装方式**:用户可以通过流星的命令行工具添加名为`mrt:ghost`的软件包。`mrt`是流星的一个命令行工具,用于添加、管理以及配置软件包。 2. **初始化Ghost服务器**:描述中提供了如何在服务器启动时运行Ghost的基本代码示例。这段代码使用了JavaScript的Promise异步操作,`ghost().then(function (ghostServer) {...})`这行代码表示当Ghost服务器初始化完成后,会在Promise的回调函数中提供一个Ghost服务器实例。 3. **配置Ghost博客**:在`then`方法中,首先会获取到Ghost服务器的配置对象`config`,用户可以在此处进行自定义设置,例如修改主题、配置等。 4. **启动Ghost服务器**:在配置完成之后,通过调用`ghostServer.start()`来启动Ghost服务,使其能够处理博客相关的请求。 5. **Web浏览器导航**:一旦流星服务器启动并运行,用户便可以通过Web浏览器访问Ghost博客平台。 ### 标签知识点:JavaScript 1. **JavaScript作为流星Ghost软件包的开发语言**:标签指出流星Ghost软件包是使用JavaScript语言开发的。JavaScript是一种在浏览器端广泛使用的脚本语言,它也是流星平台的基础编程语言。 2. **流星和Ghost共同使用的语言**:JavaScript同样也是Ghost博客平台的开发语言。这表明流星Ghost软件包可以无缝集成,因为底层技术栈相同。 ### 压缩包子文件的文件名称列表知识点:meteor-ghost-master 1. **版本控制和软件包结构**:文件名称`meteor-ghost-master`暗示了该软件包可能托管在像GitHub这样的版本控制系统上。文件名中的`master`通常指的是主分支或主版本。 2. **软件包的目录结构**:通过文件名称可以推断出该软件包可能拥有一个标准的流星软件包结构,包含了初始化、配置、运行等必要的模块和文件。 3. **软件包的维护状态**:由于文件名没有包含特定的版本号,我们无法直接得知软件包的最新更新情况。通常,软件包维护者会将最新的版本代码放在`master`分支上。 ### 总结 流星Ghost软件包提供了一个有效的解决方案,使得流星平台的开发者能够在他们的应用中添加Ghost博客功能。软件包的使用简便,通过流星的命令行工具安装,并通过JavaScript代码配置和启动Ghost服务。通过流星Ghost软件包,开发者能够享受流星的实时特性以及Ghost博客系统的便利性。此外,软件包的命名和结构也暗示了其维护和版本控制的模式,有助于开发者更好地理解如何使用和维护这一软件包。
recommend-type

抖音标题生成自动化:用coze工作流释放创意

# 1. 抖音标题生成自动化的重要性 随着社交媒体平台的崛起,内容的吸引力很大程度上取决于标题的创意与精准性。抖音作为一个日活亿级的短视频平台,高质量的标题能够有效提高视频的点击率,增加内容的传播。但是,人工撰写标题不仅耗时耗力,而且很难做到快速响应热点,自动化标题生成工具应运而生。coze工作流,作为一种实现自动化生成抖音标题的工具,其重要性不言而喻。它能够利用大数据分析和机器学习技术,提高标题的吸引
recommend-type

spss消费结构因子分析

<think>好的,我现在需要帮助用户在SPSS中进行消费结构的因子分析。首先,我要回忆一下因子分析的基本步骤和SPSS的操作流程。用户可能对SPSS不太熟悉,所以步骤要详细,同时需要结合他们提供的引用内容,特别是引用[2]中的适用条件和检验方法。 首先,用户提到了消费结构的数据,这可能包括多个变量,如食品、住房、交通等支出。因子分析适用于这种情况,可以降维并找出潜在因子。根据引用[2],需要检查样本量是否足够,变量间是否有相关性,以及KMO和Bartlett检验的结果。 接下来,我需要按照步骤组织回答:数据准备、适用性检验、因子提取、因子旋转、命名解释、计算得分。每个步骤都要简明扼要,说
recommend-type

OpenMediaVault的Docker映像:快速部署与管理指南

根据提供的文件信息,我们将详细讨论与标题和描述中提及的Docker、OpenMediaVault以及如何部署OpenMediaVault的Docker镜像相关的一系列知识点。 首先,Docker是一个开源的应用容器引擎,允许开发者打包应用及其依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app)。 OpenMediaVault是一个基于Debian的NAS(网络附加存储)解决方案。它专为家庭或小型办公室提供文件共享、网络附加存储以及打印服务。它提供了一个易用的Web界面,通过这个界面用户可以管理服务器配置、网络设置、用户权限、文件服务等。 在描述中提到了一些Docker命令行操作: 1. `git clone`:用于克隆仓库到本地,这里的仓库指的是“docker-images-openmedivault”。 2. `docker build -t omv`:这是一个构建Docker镜像的命令,其中`-t`参数用于标记镜像名称和标签,这里是标记为“omv”。 3. `docker run`:运行一个容器实例,`-t`参数用于分配一个伪终端,`-i`参数用于交互式操作,`-p 80:80`则是将容器的80端口映射到宿主机的80端口。 启动服务的部分涉及OpenMediaVault的配置和初始化: - ssh服务:用于远程登录到服务器的协议。 - php5-fpm:是PHP的一个FastCGI实现,用于加速PHP的运行。 - nginx:是一个高性能的HTTP和反向代理服务器,常用于优化静态内容的分发。 - openmediavault引擎:指的是OpenMediaVault的核心服务。 - rrdcached:用于收集和缓存性能数据,这些数据可以被rrdtool图形化工具读取。 - collectd:是一个守护进程,用于收集系统性能和提供各种存储方式和传输方式来存储所收集的数据。 为了访问服务,需要在浏览器中输入"http:// IP_OF_DOCKER",其中`IP_OF_DOCKER`指的是运行Docker容器的主机IP地址。 描述中还提到了一个步骤:“在System-> Network-> Interfaces中添加带有dhcp的eth0”,这指的是需要在OpenMediaVault的Web管理界面中配置网络接口。`eth0`是网络接口的名称,通常代表第一个以太网接口。DHCP(动态主机配置协议)是一种自动为网络中的设备分配IP地址的协议,这样设备就可以连接网络并开始通信,无需手动配置IP地址。 【压缩包子文件的文件名称列表】中的“docker-images-openmediavault-master”暗示了这是一个包含Docker镜像文件的代码仓库。通常,“master”分支是代码的主分支,包含了代码库中最新且通常是最稳定的版本。用户可以通过克隆该仓库到本地来获取所有相关的Dockerfile、配置脚本及依赖文件,以便能够自行构建和运行OpenMediaVault的Docker镜像。 综上所述,这些知识点涵盖了从基本的Docker概念、Docker命令行操作、OpenMediaVault服务启动和管理,到具体的网络配置及Docker仓库操作,都是进行Docker化OpenMediaVault部署的关键步骤。
recommend-type

小红书文案提取一步到位:coze工作流操作全攻略

# 1. coze工作流概述 工作流系统是企业信息化和数字化转型的核心组件之一,它通过自动化流程管理提升效率,确保业务流程的顺畅执行。coze工作流作为当前市场上较为先进的工作流解决方案,它不仅仅是一套软件工具,更是一个集成化的平台,旨在通过流程自动化和智能化提升企业运营效率。 coze工作流的引入不仅有助于标准化和优化企业的业务流程,还可以通过可配置的流程设计,满足不同部门的特定需求。在组织的业务流程中