简介:本资源是一套面向计算机视觉初学者与进阶研究者的Python工程实践方案,聚焦AffectNet面部表情识别数据集的预处理全流程,解决手动标注子集筛选、面部裁剪对齐及训练/验证集科学划分等关键问题。项目共11个文件,含7个核心Python脚本(如aligner.py实现关键点对齐、sava_crop_imgs_affectnet.py执行批量裁剪、cal_each_category_img_nums.py统计类别分布)及4份Markdown说明文档,总大小仅10KB,轻量易读、结构清晰,便于快速复现与二次开发。已有400人学习下载,适用于人脸分析课程实验、表情识别模型训练前的数据准备阶段。读者可直接获取完整可运行代码链:从原始标注解析(pickle_annotations_affectnet.py)、噪声标签生成(generate_noise_label.py),到最终生成283901张训练图与3500张均衡验证图的标准化流程,涵盖中性、开心等7类表情的精确数量统计与目录组织逻辑。
1. 项目缘起:为什么AffectNet数据预处理是个技术活
如果你正在做面部表情识别,尤其是基于深度学习的研究或应用,AffectNet这个名字你肯定不陌生。作为目前规模最大、标注最丰富的公开表情数据集之一,它包含了超过一百万张来自互联网的面部图像,并标注了离散的七类基本表情(如高兴、悲伤、愤怒等)和连续的维度值(效价和唤醒度)。然而,当你兴冲冲地从官网下载了那几十个G的压缩包,准备大干一场时,第一个拦路虎往往不是模型设计,而是数据预处理。原始AffectNet数据集的目录结构复杂,图像尺寸、人脸位置、光照条件千差万别,直接扔给模型训练,效果大概率会惨不忍睹。
这就是为什么一个专门针对AffectNet的数据预处理工具包如此重要。它要干的活儿,远不止简单的“读图片-打标签”那么简单。核心任务至少包括三块:数据划分、面部裁剪和面部对齐。数据划分要保证训练集、验证集、测试集的样本分布均衡,避免因划分不当引入模型偏差;面部裁剪需要精准地从原始图像中框出人脸区域,去除大量无关的背景噪声;面部对齐则通过关键点检测和仿射变换,将所有裁剪后的人脸“摆正”,消除姿态和角度的差异,让模型专注于表情本身的变化。
网上能找到的很多教程和代码,要么只解决了其中一环,要么耦合了特定的深度学习框架(如PyTorch或TensorFlow的数据加载器),通用性和灵活性不足。更常见的情况是,代码里隐藏着一些“坑”,比如对齐后图像质量下降、划分策略有漏洞、内存消耗过大等,这些都需要在实际操作中才能发现。因此,我把自己在多个表情识别项目中反复打磨、验证过的一套预处理流程整理成了这个Python项目。它力求模块清晰、配置灵活、结果可靠,目标是让你拿到AffectNet数据后,能快速、高质量地得到一份“模型友好型”的数据,把精力真正投入到模型调优上。
2. 环境搭建与项目结构解析
工欲善其事,必先利其器。这个项目虽然核心逻辑不复杂,但依赖的几个关键库需要正确安装,项目的目录结构也决定了后续操作的流畅度。
2.1 核心依赖库安装与版本管理
首先,强烈建议使用虚拟环境(如venv或conda)来管理依赖,避免与系统或其他项目的Python环境冲突。这里以venv为例:
# 创建并激活虚拟环境 python -m venv affectnet_preprocess_env source affectnet_preprocess_env/bin/activate # Linux/macOS # affectnet_preprocess_env\Scripts\activate # Windows # 升级pip pip install --upgrade pip接下来安装核心依赖。除了基础的numpy和opencv-python,本项目的一个关键依赖是dlib库及其预训练的人脸关键点检测模型。dlib的安装有时会因系统环境而报错,以下是稳妥的安装方案:
# 安装基础依赖 pip install numpy opencv-python-headless pandas tqdm scikit-learn # 安装dlib。如果直接pip install dlib失败,可以尝试以下方法: # 方法一(推荐,使用预编译的wheel): # 访问 https://pypi.org/project/dlib/ 查看对应你Python版本和系统的版本,或直接尝试: pip install dlib==19.24.2 # 一个较稳定且兼容性好的版本 # 方法二(macOS/Linux,可能需要先安装CMake和Boost): # macOS: brew install cmake boost # Ubuntu/Debian: sudo apt-get install cmake libboost-all-dev # 然后再 pip install dlib # 安装Pillow用于更丰富的图像操作(可选但推荐) pip install Pillow注意:
opencv-python-headless是不包含GUI功能的OpenCV版本,更适合服务器环境。如果你需要在本地显示图片调试,可以安装opencv-python。dlib的安装是最大的坎,如果遇到编译错误,优先考虑寻找对应你Python版本(如Python 3.9)和系统(Windows 10/11, Ubuntu 20.04等)的预编译.whl文件进行离线安装。
2.2 项目目录结构设计
一个清晰的目录结构是项目可维护性的基础。建议按如下方式组织你的工作区:
affectnet_preprocessor/ ├── src/ # 源代码目录 │ ├── __init__.py │ ├── data_splitter.py # 数据划分模块 │ ├── face_cropper.py # 面部裁剪模块 │ ├── face_aligner.py # 面部对齐模块 │ └── utils.py # 通用工具函数(如日志、路径处理) ├── configs/ # 配置文件目录 │ └── preprocessing_config.yaml # 所有可配置参数 ├── scripts/ # 执行脚本目录 │ └── run_preprocessing.py # 主运行脚本 ├── input_data/ # 原始AffectNet数据(需手动创建并放入数据) │ ├── training/ # 训练集原始图像和标注 │ └── validation/ # 验证集原始图像和标注 ├── processed_data/ # 处理后的输出目录(程序自动生成) │ ├── train/ # 处理后的训练集 │ ├── val/ # 处理后的验证集 │ └── test/ # 处理后的测试集(从验证集划分) ├── dlib_models/ # 存放dlib预训练模型文件 │ └── shape_predictor_68_face_landmarks.dat # 关键点检测模型 ├── logs/ # 日志文件目录 ├── requirements.txt # 项目依赖列表 └── README.md # 项目说明文档你需要手动完成以下几件事:
- 从AffectNet官网下载数据,解压后将其中的
training和validation文件夹(包含图片和对应的标注文件annotation.csv)放入input_data/目录。 - 下载dlib的68点人脸关键点预测器模型(
shape_predictor_68_face_landmarks.dat.bz2),解压后得到.dat文件,放入dlib_models/目录。这是面部对齐步骤的核心。 - 根据你的需求,编辑
configs/preprocessing_config.yaml文件,调整各项参数。
这种结构将配置、源代码、数据、模型和日志分离,符合现代机器学习项目的惯例,也便于后续的版本控制和协作。
3. 核心模块一:科学的数据划分策略
AffectNet官方提供了训练集和验证集(validation set)的划分。但在实际模型开发中,我们通常需要三个集合:训练集(Train)、验证集(Val)和测试集(Test)。官方的验证集常被我们用作测试集(Test),以评估模型的最终泛化能力。那么,用于模型调参的验证集(Val)从哪里来?答案是从官方训练集中再划分出一部分。
3.1 从官方划分到实际需求的映射
我们的目标是:保持测试集的纯粹性(只用官方验证集),从官方训练集中科学地划分出新的训练集和验证集。这里的关键在于“科学”二字。表情识别数据集普遍存在类别不均衡问题(“高兴”的图片远多于“恐惧”),如果随机划分,可能导致某个表情在验证集中样本极少,无法有效评估模型在该类上的性能。
因此,我采用了分层抽样(Stratified Sampling)策略。具体思路是:读取官方训练集的标注文件(通常是一个包含图像路径和表情标签的CSV文件),以表情类别为分层依据,使用scikit-learn的train_test_split函数,按预设比例(如8:2)进行划分。这样可以确保划分后的训练集和验证集中,各个表情类别的比例与原始训练集基本一致。
# 代码示例:data_splitter.py 中的核心划分函数 import pandas as pd from sklearn.model_selection import train_test_split def create_custom_split(annotation_path, train_ratio=0.8, seed=42): """ 从官方训练集标注文件创建新的训练/验证划分。 参数: annotation_path: 官方训练集标注CSV文件路径。 train_ratio: 新训练集占原始训练集的比例。 seed: 随机种子,确保结果可复现。 返回: train_df, val_df: 包含'image_path'和'expression_label'的DataFrame。 """ df = pd.read_csv(annotation_path) # 假设DataFrame包含'image_path'和'expression'列 # 清理数据,移除标签为-1(无效)或8( contempt, contempt类别在7类分类中通常不用)的样本 df = df[df['expression'].between(0, 6)] # 保留0-6共7类基本表情 # 使用分层抽样划分 train_df, val_df = train_test_split( df, test_size=1-train_ratio, stratify=df['expression'], # 关键:按表情标签分层 random_state=seed ) # 重置索引 train_df = train_df.reset_index(drop=True) val_df = val_df.reset_index(drop=True) # 可选:保存划分结果到文件,方便后续步骤直接读取 train_df.to_csv('processed_data/splits/train_split.csv', index=False) val_df.to_csv('processed_data/splits/val_split.csv', index=False) return train_df, val_df3.2 划分策略的细节与陷阱
这里有几个容易踩坑的细节:
- 无效样本过滤:AffectNet的标注中,
-1表示“无法识别”,8表示“ contempt”( contempt)。在7类基本表情分类任务中,通常需要过滤掉这些样本。这一步必须在划分之前进行,否则无效样本会被随机分到训练集或验证集,污染数据。 - 随机种子:务必固定
random_state(如seed=42)。这是实验结果可复现的生命线。不同的随机种子会产生不同的数据划分,从而导致模型性能波动,让你无法判断性能提升是来自模型改进还是运气。 - 路径处理:标注文件中的图像路径可能是相对路径。在划分后,需要根据你的项目目录结构,更新或保存正确的图像绝对路径或相对于新根目录的路径,供后续裁剪和对齐模块读取。
- 内存考虑:AffectNet训练集有近30万张图片,对应的标注文件也很大。直接使用
pandas读取整个CSV可能内存占用较高。如果内存紧张,可以考虑分块读取(chunksize)或使用dask库进行处理。
通过这个模块,我们得到了三个清晰的数据列表:train_split.csv,val_split.csv,以及直接将官方验证集作为test_split.csv。这为后续处理奠定了坚实的基础。
4. 核心模块二:精准的面部检测与裁剪
拿到划分好的图像路径列表后,下一步是从原始图片中把人脸区域“抠”出来。这一步的目标是最大化保留表情信息,同时最小化背景干扰。我们选用OpenCV的CascadeClassifier结合dlib的get_frontal_face_detector进行人脸检测,这是一种兼顾速度和精度的常见方案。
4.1 人脸检测器的选型与级联
为什么选择两种检测器?因为各有优劣。OpenCV的Haar级联分类器速度极快,但对于侧脸、遮挡、极端光照的检测效果会下降。dlib的HOG+SVM检测器精度更高,尤其是对正面和稍侧的人脸非常稳健,但速度稍慢。在实际应用中,我采用了一种级联(Cascade)策略:先用快的OpenCV检测器尝试,如果检测到人脸且置信度(通过检测框大小和位置初步判断)较高,则直接使用;如果OpenCV没检测到或结果可疑,则启用更准的dlib检测器作为后备。
# 代码示例:face_cropper.py 中的检测与裁剪函数 import cv2 import dlib import numpy as np from pathlib import Path class FaceCropper: def __init__(self, dlib_detector, cv_classifier_path='haarcascade_frontalface_default.xml'): """ 初始化人脸检测器。 dlib_detector: dlib.get_frontal_face_detector() 返回的检测器对象。 cv_classifier_path: OpenCV Haar级联分类器XML文件路径。 该文件通常位于OpenCV安装目录的`data/haarcascades/`下,需要复制到项目内。 """ self.dlib_detector = dlib_detector self.cv_classifier = cv2.CascadeClassifier(cv_classifier_path) def detect_face(self, image): """ 级联人脸检测。 返回: 检测到的人脸矩形框 (x, y, w, h),如果未检测到则返回None。 """ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 第一级:OpenCV快速检测 faces_cv = self.cv_classifier.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30) ) if len(faces_cv) == 1: # 理想情况:只检测到一张脸 x, y, w, h = faces_cv[0] # 简单启发式规则:检查人脸框是否过小或位置过于边缘 height, width = gray.shape if w > 50 and h > 50 and x > 10 and y > 10 and (x+w) < width-10 and (y+h) < height-10: return (x, y, w, h) # 第二级:OpenCV检测失败或不可信,使用dlib faces_dlib = self.dlib_detector(gray, 1) # 第二个参数是上采样次数,有助于检测小脸 if len(faces_dlib) > 0: # 选择面积最大的脸(假设这是主要表情主体) face = max(faces_dlib, key=lambda rect: rect.area()) # 将dlib的矩形格式转换为(x, y, w, h) x, y = face.left(), face.top() w, h = face.width(), face.height() return (x, y, w, h) return None # 未检测到人脸 def crop_and_save(self, image_path, output_dir, margin_ratio=0.2): """ 检测人脸,根据边界框裁剪,并保存。 margin_ratio: 在边界框外扩展的比例,以保留更多上下文信息(如头发、下巴)。 """ image = cv2.imread(str(image_path)) if image is None: print(f"警告:无法读取图像 {image_path}") return False bbox = self.detect_face(image) if bbox is None: print(f"警告:在 {image_path} 中未检测到人脸") # 可以选择保存原图或跳过,这里选择跳过 return False x, y, w, h = bbox height, width = image.shape[:2] # 计算扩展后的边界框,并确保不超出图像范围 margin_x = int(w * margin_ratio) margin_y = int(h * margin_ratio) x1 = max(0, x - margin_x) y1 = max(0, y - margin_y) x2 = min(width, x + w + margin_x) y2 = min(height, y + h + margin_y) cropped_face = image[y1:y2, x1:x2] # 构建输出路径并保存 rel_path = Path(image_path).relative_to(self.input_root_dir) output_path = Path(output_dir) / rel_path output_path.parent.mkdir(parents=True, exist_ok=True) cv2.imwrite(str(output_path), cropped_face) return True4.2 边界框扩展与失败处理
上面的代码中有两个关键点:
- 边界框扩展(Margin):直接裁剪检测到的人脸框,可能会切掉额头、下巴或部分脸颊,这些区域对某些表情(如惊讶时的额头皱纹)也有贡献。因此,我引入
margin_ratio参数(通常设为0.2),将框按比例向外扩展,保留更多上下文。同时,必须进行边界检查(max(0, ...)和min(width, ...)),防止扩展后坐标越界。 - 失败处理:不是每张图片都能成功检测到人脸。可能是极端姿态、严重遮挡、图像质量太差。对于这些“漏网之鱼”,我的策略是记录日志并跳过。在后续统计中,你需要关注失败率。如果失败率很高(比如超过5%),可能需要回查原因:是检测器参数太严格?还是这批数据本身质量有问题?对于跳过的样本,在最终的划分CSV文件中也应该被移除,保证数据列表的纯净。
这个模块会遍历所有划分好的图像列表,将成功裁剪的人脸保存到processed_data/train/,processed_data/val/,processed_data/test/的对应子目录中,目录结构与原输入保持一致。
5. 核心模块三:基于关键点的面部对齐
裁剪出的人脸,其姿态(如倾斜、抬头、低头)仍然不一致。面部对齐的目的就是通过几何变换,将所有脸“标准化”到同一个正面的参考坐标系中。这是提升模型性能非常有效的一步,因为它减少了类内差异(同一个表情因姿态不同而产生的变化),让模型更容易学习到表情的本质特征。
5.1 对齐原理与仿射变换
对齐的核心是人脸关键点检测。我们使用dlib的68点预测器(就是之前下载的shape_predictor_68_face_landmarks.dat)来定位人脸上的68个特征点,包括眼睛、眉毛、鼻子、嘴巴和脸部轮廓。
对齐的思路是:选取一张“标准正面脸”作为模板,其眼睛、嘴巴等关键点处于理想位置。然后,对于每一张待处理的脸,计算其检测到的关键点与模板关键点之间的映射关系,最后通过仿射变换(Affine Transformation)将待处理脸“扭曲”到模板的姿势上。
仿射变换是一种线性变换,包含旋转、缩放、平移和剪切,可以用一个2x3的变换矩阵表示。OpenCV的cv2.getAffineTransform(src_points, dst_points)可以根据三对匹配点计算这个矩阵,cv2.warpAffine()则应用这个矩阵进行图像变换。
# 代码示例:face_aligner.py 中的对齐函数 import cv2 import dlib import numpy as np class FaceAligner: def __init__(self, predictor_path, desired_face_width=256, desired_face_height=None): """ 初始化对齐器。 predictor_path: dlib 68点预测器模型文件路径。 desired_face_width: 对齐后输出图像的宽度。 desired_face_height: 对齐后输出图像的高度,如果为None则与宽度相同(生成正方形)。 """ self.predictor = dlib.shape_predictor(predictor_path) self.detector = dlib.get_frontal_face_detector() # 对齐前可能需要再次检测 self.desired_face_width = desired_face_width if desired_face_height is None: self.desired_face_height = desired_face_width else: self.desired_face_height = desired_face_height # 定义模板脸的坐标(基于 desired_face_width 和 desired_face_height 计算) # 这里是一个常见的模板:双眼水平,位于图片上半部分 self.desired_left_eye = (0.35, 0.35) # 左眼在输出图像中的相对位置 self.desired_right_eye = (0.65, 0.35) # 右眼在输出图像中的相对位置 # 计算实际的像素坐标 self.desired_left_eye_x = self.desired_left_eye[0] * self.desired_face_width self.desired_left_eye_y = self.desired_left_eye[1] * self.desired_face_height self.desired_right_eye_x = self.desired_right_eye[0] * self.desired_face_width self.desired_right_eye_y = self.desired_right_eye[1] * self.desired_face_height def align(self, image): """ 对齐单张图像。 返回: 对齐后的人脸图像,如果失败则返回None。 """ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) rects = self.detector(gray, 0) if len(rects) != 1: # 对齐要求检测到且仅检测到一张脸,否则返回None return None shape = self.predictor(gray, rects[0]) shape = self._shape_to_np(shape) # 将dlib的shape对象转为numpy数组 # 获取左右眼的中心坐标(使用68点模型中的第36-41点为左眼,42-47点为右眼) left_eye_center = shape[36:42].mean(axis=0).astype("int") right_eye_center = shape[42:48].mean(axis=0).astype("int") # 计算双眼连线的角度 dY = right_eye_center[1] - left_eye_center[1] dX = right_eye_center[0] - left_eye_center[0] angle = np.degrees(np.arctan2(dY, dX)) # 计算两眼之间的中点 eyes_center = ((left_eye_center[0] + right_eye_center[0]) // 2, (left_eye_center[1] + right_eye_center[1]) // 2) # 计算缩放比例:使变换后双眼间的距离等于模板中的距离 dist = np.sqrt((dX ** 2) + (dY ** 2)) desired_dist = self.desired_right_eye_x - self.desired_left_eye_x scale = desired_dist / dist # 构造旋转矩阵(以两眼中心为旋转中心) M = cv2.getRotationMatrix2D(eyes_center, angle, scale) # 调整平移量,使旋转缩放后,两眼中心对准模板位置 tX = self.desired_face_width * 0.5 tY = self.desired_face_height * self.desired_left_eye[1] # 使用左眼的y相对位置 M[0, 2] += (tX - eyes_center[0]) M[1, 2] += (tY - eyes_center[1]) # 执行仿射变换 (w, h) = (self.desired_face_width, self.desired_face_height) output = cv2.warpAffine(image, M, (w, h), flags=cv2.INTER_CUBIC) return output def _shape_to_np(self, shape, dtype="int"): """将dlib的shape对象转换为(x, y)坐标的numpy数组。""" coords = np.zeros((shape.num_parts, 2), dtype=dtype) for i in range(0, shape.num_parts): coords[i] = (shape.part(i).x, shape.part(i).y) return coords5.2 对齐过程中的质量把控与参数调优
对齐步骤虽然自动化程度高,但仍有几个需要仔细考量的地方:
- 模板点的选择:上面的代码以双眼为基准进行对齐,这是最常用的方法。模板中双眼的相对位置(
desired_left_eye和desired_right_eye)决定了对齐后脸在图片中的位置和大小。(0.35, 0.35)和(0.65, 0.35)意味着双眼位于图片宽度35%和65%的位置,高度在35%的位置,这样通常能保证脸在图片中央偏上,留出额头和下巴空间。你可以根据后续模型的需求调整这些值。 - 仅处理单张人脸:对齐逻辑默认只处理包含恰好一张人脸的图片。如果检测到多张脸或没有人脸,则跳过。这是因为AffectNet数据集中绝大多数是单人脸,且多张脸的情况下难以确定应对齐哪一张。对于检测失败或多人脸的图片,处理方式应与裁剪模块保持一致(记录并跳过)。
- 图像插值方法:
cv2.warpAffine中的flags=cv2.INTER_CUBIC指定了插值方法,用于计算变换后非整数坐标像素点的值。INTER_CUBIC(双三次插值)质量较好但速度稍慢,INTER_LINEAR(双线性插值)是速度和质量的一个平衡点。对于表情识别任务,INTER_CUBIC通常是更好的选择,因为它能更好地保留面部细节。 - 输出尺寸:
desired_face_width和desired_face_height决定了输出图像的大小。这个尺寸需要与后续模型输入的期望尺寸匹配。常见的尺寸有224x224(适配许多CNN骨干网络)、112x112或96x96(用于更轻量的模型)。统一尺寸也便于批量加载和GPU内存利用。
对齐模块会读取裁剪后的人脸图片,进行对齐操作,并将结果保存到新的目录(例如processed_data/aligned_train/),或者直接覆盖原裁剪文件(根据配置决定)。至此,我们得到了经过划分、裁剪、对齐的“干净”数据集。
6. 实战整合:配置化流程与批量处理脚本
将三个核心模块串联起来,并处理数十万张图片,需要一个稳健的、可配置的批量处理流程。我采用配置文件(YAML)来管理所有参数,并通过一个主脚本协调整个流程。
6.1 配置文件设计
configs/preprocessing_config.yaml文件包含了所有可调参数,使得实验可复现,也方便他人使用。
# preprocessing_config.yaml paths: input_root: "./input_data" # 原始AffectNet数据根目录 dlib_model: "./dlib_models/shape_predictor_68_face_landmarks.dat" # dlib模型路径 output_root: "./processed_data" # 处理结果输出根目录 haar_cascade: "./models/haarcascade_frontalface_default.xml" # OpenCV模型路径 data_split: seed: 42 # 随机种子 train_ratio: 0.8 # 新训练集占原始训练集的比例 original_train_annotation: "training/annotation.csv" # 官方训练集标注文件相对路径 original_val_annotation: "validation/annotation.csv" # 官方验证集标注文件相对路径 face_crop: enable: true # 是否启用裁剪 margin_ratio: 0.2 # 边界框扩展比例 min_face_size: 50 # 最小人脸尺寸(像素),小于此值视为检测失败 use_cascade: true # 是否使用OpenCV+dlib级联检测 face_align: enable: true # 是否启用对齐 desired_size: 224 # 对齐后图像尺寸(正方形) desired_left_eye_pos: [0.35, 0.35] # 模板左眼相对位置 desired_right_eye_pos: [0.65, 0.35] # 模板右眼相对位置 interpolation: "cubic" # 插值方法,可选 'linear', 'cubic', 'area' logging: level: "INFO" # 日志级别 file: "./logs/preprocess.log" # 日志文件路径 processing: num_workers: 4 # 并行处理的进程数(用于加速) batch_size: 32 # 批处理大小(用于进度显示) skip_existing: true # 是否跳过已处理过的文件(根据输出文件是否存在判断)6.2 主流程脚本与并行加速
主脚本scripts/run_preprocessing.py负责读取配置,按顺序调用各个模块,并加入日志记录和进度条。
# 代码示例:run_preprocessing.py 主流程骨架 import yaml import logging from pathlib import Path from tqdm import tqdm import multiprocessing as mp from src.data_splitter import create_custom_split from src.face_cropper import FaceCropper from src.face_aligner import FaceAligner def process_single_image(args): """包装函数,用于多进程池中处理单张图片。""" img_path, cropper, aligner, crop_enable, align_enable, output_dir = args # 具体的裁剪、对齐、保存逻辑... # 返回处理结果(成功/失败) pass def main(config): # 1. 设置日志 logging.basicConfig(...) # 2. 数据划分 logging.info("开始数据划分...") train_df, val_df = create_custom_split(...) test_df = load_official_validation(...) logging.info(f"划分完成: 训练集 {len(train_df)} 张, 验证集 {len(val_df)} 张, 测试集 {len(test_df)} 张") # 3. 初始化处理工具 cropper = FaceCropper(...) if config['face_crop']['enable'] else None aligner = FaceAligner(...) if config['face_align']['enable'] else None # 4. 为每个数据集(train/val/test)创建任务列表 all_tasks = [] for split_name, df in [('train', train_df), ('val', val_df), ('test', test_df)]: output_subdir = Path(config['paths']['output_root']) / split_name output_subdir.mkdir(parents=True, exist_ok=True) for img_rel_path in df['image_path']: img_full_path = Path(config['paths']['input_root']) / img_rel_path all_tasks.append((img_full_path, cropper, aligner, ..., output_subdir)) # 5. 使用多进程并行处理 logging.info(f"开始处理 {len(all_tasks)} 张图片,使用 {config['processing']['num_workers']} 个进程...") with mp.Pool(processes=config['processing']['num_workers']) as pool: # 使用imap_unordered可以配合tqdm显示进度条 results = list(tqdm(pool.imap_unordered(process_single_image, all_tasks), total=len(all_tasks), desc="处理进度")) # 6. 统计结果 success_count = sum(results) failure_count = len(all_tasks) - success_count logging.info(f"处理完成!成功: {success_count}, 失败: {failure_count}, 失败率: {failure_count/len(all_tasks):.2%}") # 7. 根据处理结果,更新数据列表(移除失败样本),并保存最终可用的划分文件 # ... (代码略) if __name__ == "__main__": with open("configs/preprocessing_config.yaml", 'r') as f: config = yaml.safe_load(f) main(config)并行处理的关键:处理几十万张图片是IO密集型和计算密集型混合的任务。使用Python的multiprocessing.Pool可以充分利用多核CPU,显著加速。注意,dlib和OpenCV的部分操作可能释放了GIL(全局解释器锁),在多进程中能获得较好的加速比。我将每张图片的处理包装成一个独立的函数process_single_image,然后提交给进程池。tqdm库提供了美观的进度条,让你能实时了解处理进度。
6.3 处理后的数据整理与校验
所有图片处理完毕后,最后一步是整理。因为会有处理失败的图片被跳过,所以最初的数据列表(train_df等)需要被更新,移除那些没有对应输出图片的条目。生成最终的train_processed.csv、val_processed.csv、test_processed.csv,其中包含成功处理图片的路径和对应的标签。这个文件才是你后续构建PyTorch的Dataset或TensorFlow的tf.datapipeline时真正需要读取的清单。
此外,强烈建议进行人工抽样检查。随机从每个集合中挑选几十张处理前后的图片进行对比,观察裁剪是否准确、对齐是否端正、图像质量是否下降。这是保证预处理质量不可或缺的一环。
本文还有配套的精品资源,点击获取