简介:针对深度伪造假脸视频中面部细节特征提取的本科毕业设计课题,本份资源以Java Spring Boot为后端框架,搭配MySQL数据库构建Web系统,围绕视频数据管理、特征提取算法落地和结果展示等环节提供完整可运行代码,是计算机相关专业学生进行毕设开发、课程设计或视频取证技术验证的实用参考。项目难度定位适中,源码均已在本地编译通过,下载后按照随包文档配置好环境即可顺利运行,整体内容经过助教老师审定,可靠性较高,可放心用于学习研究或二次扩展。压缩包为ZIP格式,大小约37.55MB,内附系统源码与部署环境配置说明,目录结构较为规整,便于按模块查阅核心业务逻辑与前后端接口;由于上游未提供文件明细,具体文件清单可在解压后查看。目前已有89人学习浏览,作者zy_dreamer在下载页支持私信答疑,使用中若遇到环境或运行问题均可联系博主,适合希望获得可运行毕设源码并节省排错时间的人群。
1. 一张假脸的破绽藏在像素级细节里
做 Deepfake 检测的人都有一个共识:判别器和生成器打到最后,拼的已经不是“像不像”,而是“哪里不像”。一张千分之一秒的视频帧里,人的皮肤油脂反光、睫毛投影、瞳孔高光边缘的菲涅尔效应,这些细节几乎不会被生成器精确还原。所谓面部细节特征提取算法,就是从这些像素级差异里把“人造痕迹”量化成可计算的向量,再交给分类器去判定。这套流程既是本科毕设里比较出效果的选题,也是理解整个深度伪造攻防体系的切入点。本资源给出了一套本地可编译运行的完整代码,配好环境后能直接跑通人脸检测、特征提取和判别逻辑,适合用 Deepfake 检测作为毕设方向、同时不想把时间耗在重复造轮子上的同学。
2. Deepfake 面部细节特征:从纹理不一致到时序不连续
2.1 为什么人眼直觉够用但算法不够:细节特征的粒度
人眼判断一张脸是真是假,靠的是整体违和感,比如面部比例失衡、表情僵硬。但这类直觉很难转换成连续可微的数学信号,更没法放进 CNN 里做端到端训练。算法需要的是“粒度更细”的观测维度,直接作用在像素邻域和关键点轨迹上。细节特征提取的价值,就是把这些肉眼可见或不可见的造假线索离散成特征向量,让后续的分类器有一个稳定的输入分布。
真实视频中的人脸存在完整的皮肤微纹理,即由汗毛孔、皮沟和细纹构成的高频信号;而 GAN 生成人脸时,解码器倾向于输出过度平滑的纹理区域,高频成分显著缺失。与此同时,视频帧与帧之间的光流场会呈现周期性规律,Deepfake 换脸后的区域在嘴唇运动、眨眼频率、头部转动时会出现亚像素级的错位。这两类线索正是面部细节特征提取算法要捕捉的核心对象。
2.2 三类最值得提取的面部细节特征
2.2.1 高频纹理特征与噪声谱
把一帧人脸图输入高通滤波器(如拉普拉斯算子)后,真实人脸会保留较多的边缘与噪声响应,生成人脸则因为解码器的平滑约束,响应值分布相对集中。更精细的做法是对人脸区域做分块 FFT,统计高频带能量占比和各向异性程度。代码上通常用 OpenCV 的Laplacian算子计算方差,但维度太低,更适合作为辅助特征,真正的深度特征还需要卷积层来提取。
2.2.2 面部关键点时空轨迹
Deepfake 换脸通常是替换整个脸部区域,但源视频和目标视频的面部骨骼结构不一致,导致关键点轨迹存在抖动。用 MediaPipe 或 Dlib 提取 68 个面部关键点,再对时间维度做一阶差分,可以得到每个关键点的瞬时位移序列。真实视频的轨迹平滑,伪造视频在眼睑和嘴唇边界会出现周期性抖动峰值。这个特征维度低、可解释性强,适合作为模型输入的一部分,也可以单独用来做规则判定。
2.2.3 光照与面部反射一致性
换脸后的区域和原始头部在光照方向上存在细微偏差,尤其是在侧脸、额头高光和下巴阴影处。对整脸区域做球谐光照估计,提取光照系数向量,再计算区域间的一致性得分,可以暴露出伪造区域的光照不匹配问题。这个特征对极端光照变化敏感,但对训练集中出现过的光照条件有依赖,所以毕设里一般把光照一致性得分作为后验过滤项,或者把它拼进特征向量。
2.3 特征提取算法的选型对比
| 特征类型 | 提取方式 | 特征维度 | 对压缩视频鲁棒性 | 实现成本 |
|---|---|---|---|---|
| 传统纹理特征 | LBP / HOG / 拉普拉斯方差 | 几十维 | 较弱,压缩后衰减明显 | 低 |
| 深度卷积特征 | ResNet / EfficientNet 中间层 | 512/1024 维 | 较强 | 中 |
| 关键点轨迹特征 | MediaPipe + 时序差分 | 68×T 维 | 强 | 中 |
| 混合特征 | CNN 特征拼接关键点特征 | 自定义 | 最强 | 较高 |
毕设场景下不建议直接走传统纹理特征路线,因为现在主流 Deepfake 数据集的视频都经过 H.264 重压缩,LBP 在压缩域里的区分度会快速下降。更稳妥的方案是用深度卷积网络做主体,关键点轨迹特征做辅助分支,最后在分类层之前融合。
3. 提取算法的工程实现:从人脸检测到特征序列
3.1 整体管线设计
一个能完成“输入视频、输出真假概率或局部热图”的完整管线,包含 5 个环节。先用 OpenCV 按固定帧率抽帧,再把每一帧送入人脸检测器,检测到的人脸经过仿射变换对齐到112×112或224×224的统一尺寸。对齐后的图像送入特征提取主干网,得到空间特征图;多帧的空间特征再聚合出时序特征;最后过全连接层做二分类。
这一步最常见的误区是直接对整帧视频做分类。整帧画面里有大量背景干扰,模型会学到“背景里有某个人”这种过拟合特征,而不是学到面部细节差异。所以人脸检测和对齐不是前置工程,而是特征提取算法的一部分,直接影响最终准确率。
3.2 人脸检测与对齐的具体实现
3.2.1 基于 MTCNN 的对齐流程
MTCNN 在 CPU 上的单帧推理速度大约在 8 到 20 毫秒,对毕设的帧率要求来说已经足够。检测得到的人脸框与人脸关键点需要做一次仿射变换,使两只眼睛保持水平、人脸中心对齐到图像中心。关键点坐标如果直接用原始检测结果,不做对齐,后续卷积网络提取到的人脸空间位置分布就会不一致,导致特征分布偏移。
import cv2 import numpy as np from mtcnn import MTCNN detector = MTCNN() def align_face(image, target_size=(224, 224)): # 检测人脸并获取关键点 faces = detector.detect_faces(image) if not faces: return None, None # 取面积最大的人脸,排除误检的背景小框 faces = sorted(faces, key=lambda f: f["box"][2] * f["box"][3], reverse=True) keypoints = faces[0]["keypoints"] left_eye = np.array(keypoints["left_eye"], dtype=np.float32) right_eye = np.array(keypoints["right_eye"], dtype=np.float32) nose = np.array(keypoints["nose"], dtype=np.float32) # 计算仿射变换矩阵:左右眼连线水平、中心对准 dx = right_eye[0] - left_eye[0] dy = right_eye[1] - left_eye[1] angle = np.degrees(np.arctan2(dy, dx)) center = (left_eye + right_eye) / 2 rot_mat = cv2.getRotationMatrix2D((float(center[0]), float(center[1])), angle, 1.0) rotated = cv2.warpAffine(image, rot_mat, (image.shape[1], image.shape[0])) return rotated, faces[0]align_face返回旋转校正后的图像和人脸框信息,后续送入特征提取网络前,还需要再裁剪出以两眼中心为基准、扩展 1.2 倍人脸宽度的区域。target_size参数控制最终送入网络的分辨率,值越大细节越丰富,但显存占用会指数增长。毕设机器是 8GB 显存时,建议用112×112或224×224,不要直接上512×512。
3.2.2 通用参数配置
特征提取阶段还需要约定输入尺度、归一化方式和帧间隔。常见做法是:
- 输入尺寸:
224×224 - 归一化:ImageNet 均值
[0.485, 0.456, 0.406]和标准差[0.229, 0.224, 0.225] - 抽帧间隔:每 8 帧取 1 帧,保证相邻帧差异足够但不丢失时序连续性
- 单样本帧数:16 帧
- 人脸置信度阈值:0.9
3.3 细粒度特征提取网络结构
3.3.1 卷积模块设计
Deepfake 面部细节特征集中在眼睛、嘴角、发际线边缘这些局部区域,直接用 ResNet18 的最后一层特征会丢失空间位置信息。常见做法是用 ResNet18 的前 4 个 stage 输出特征图,再接一层通道注意力机制,让网络自适应地放大对细微痕迹敏感的特征通道。
import torch import torch.nn as nn import torchvision.models as models class FineGrainedFeatureExtractor(nn.Module): def __init__(self, feature_dim=512): super().__init__() # 使用预训练ResNet18,去掉最后的全连接层和池化层 backbone = models.resnet18(pretrained=True) self.stage1 = nn.Sequential( backbone.conv1, backbone.bn1, backbone.relu, backbone.maxpool ) self.stage2 = backbone.layer1 self.stage3 = backbone.layer2 self.stage4 = backbone.layer3 # 通道注意力:学习每个通道的重要程度 self.attention = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(256, 256 // 16, kernel_size=1), nn.ReLU(inplace=True), nn.Conv2d(256 // 16, 256, kernel_size=1), nn.Sigmoid() ) self.project = nn.Conv2d(256, feature_dim, kernel_size=1) def forward(self, x): x = self.stage1(x) x = self.stage2(x) x = self.stage3(x) x = self.stage4(x) # [B, 256, 14, 14] 输入224x224时 attn = self.attention(x) # 通道权重 x = x * attn x = self.project(x) # 投影到feature_dim维 return x # [B, feature_dim, 14, 14]attention模块的作用是把特征图压缩成通道权重,再乘回原特征图。Deepfake 生成痕迹通常只集中在部分通道里,比如某个卷积核恰好学到了“牙缝边缘噪声”,注意力机制会放大它在最终特征中的占比。project层把 256 通道投影到feature_dim,方便后续拼接其他维度特征。
3.3.2 空间注意力补充
通道注意力之外,还需要一个空间注意力模块来强化“眼睛和嘴唇”区域。实现思路是:对特征图在通道维度上计算均值,得到单通道空间权重,再与原始特征图相乘。这样模型不会把背景区域的响应误判为造假信号。
class SpatialAttention(nn.Module): def __init__(self, kernel_size=3): super().__init__() self.conv = nn.Conv2d(2, 1, kernel_size, padding=kernel_size // 2) self.sigmoid = nn.Sigmoid() def forward(self, x): avg_out = torch.mean(x, dim=1, keepdim=True) max_out, _ = torch.max(x, dim=1, keepdim=True) # 拼接平均池化和最大池化结果,卷积生成空间权重 scale = torch.cat([avg_out, max_out], dim=1) attn = self.sigmoid(self.conv(scale)) return x * attn空间注意力的使用要节制,叠加太多层会让特征图过于稀疏,训练时容易梯度消失。一般只在最后一个 stage 之后加一层即可,不需要每个 stage 都加。
3.4 时序特征聚合
单帧特征只能反映空间纹理差异,但 Deepfake 的很多伪影要跨帧才能暴露,比如闪烁的眼皮边缘、闪烁的牙齿高光。时序聚合层接收连续帧的特征序列,输出一个包含时序变化的向量。常见做法有 LSTM、GRU 和 Temporal Convolutional Network。GRU 参数量更小,在几百个视频的小数据集上不容易过拟合。
class TemporalAggregator(nn.Module): def __init__(self, input_dim=512, hidden_dim=256, num_layers=2): super().__init__() self.gru = nn.GRU( input_size=input_dim, hidden_size=hidden_dim, num_layers=num_layers, batch_first=True, bidirectional=True # 双向GRU捕捉前后帧上下文 ) def forward(self, frame_features): # frame_features: [B, T, D] T为帧数,D为单帧特征维度 out, _ = self.gru(frame_features) # 取最后一帧的隐状态作为时序特征表示 return out[:, -1, :] # [B, hidden_dim * 2]这段代码把每帧提取出的 512 维特征按时间顺序排列,batch_first=True表示输入维度是[batch, time, feature]。双向 GRU 会同时看当前帧之前和之后的帧,能捕捉到“前半段正常、后半段突然出现造假痕迹”这类突变模式。实际训练时帧数 T 一般取 16,不足 16 帧的视频尾部需要循环填充。
4. 训练与调参:让提取出来的特征真的可分
4.1 数据集组织与标注格式
毕设场景下,首选 FaceForensics++ 的 c23 版本作为训练数据,它包含 1000 个真实视频和 1000 个 Deepfake 视频。每个视频以帧目录的形式组织,根目录下需要维护一个label.csv,列名为video_id,label,label 取 0 或 1。
这里最容易踩的坑是数据泄漏。很多初学者把同一个视频的不同帧同时分进训练集和验证集,模型会通过记忆背景、服装等与真假无关的信息取得虚高准确率。正确的做法是按视频划分,也就是训练集和验证集里的视频完全不重合。读取视频的代码里,要把视频 ID 作为分组键,用GroupShuffleSplit而不是普通随机切分。
# 数据集目录结构示例 dataset/ ├── train/ │ ├── real/ │ │ └── video_0001/ │ │ ├── frame_0000.jpg │ │ └── frame_0001.jpg │ └── fake/ │ └── video_1001/ │ └── frame_0000.jpg ├── val/ ├── test/ └── label.csv这个结构同时方便了 web 端的展示。很多本科毕设会要求配套一个管理后台,用来查看每个视频的提取特征和判定结果,常见技术栈是 Java Spring Boot 加 MySQL。做法是用 OpenCV 把提取出的特征向量转成 JSON 字符串存进 MySQL 表里,管理端再通过接口查询展示,核心的提取算法用 Python 封装成独立模块,Spring Boot 只负责读取结果。
4.2 关键超参数设置一览表
| 超参数 | 推荐值 | 说明 |
|---|---|---|
| 输入图像尺寸 | 224×224 | 再大效果提升有限,显存翻倍 |
| 单样本帧数 T | 16 | 覆盖约 0.5 秒视频,足以捕捉眼皮闪烁 |
| Batch Size | 8 | 受限于显存,T 越大 batch 越小 |
| 初始学习率 | 1e-4 | 预训练模型不宜太大 |
| 学习率衰减 | 每 5 个 epoch ×0.5 | 避免后期震荡 |
| 优化器 | AdamW | weight_decay 设为 1e-4 |
| 损失函数 | Focal Loss | 解决真假样本难易不均 |
| 总训练轮数 | 15 ~ 20 | 小数据量下更久会过拟合 |
Focal Loss 比标准交叉熵更适合 Deepfake 检测,因为真实视频中大部分帧造假特征很弱,模型很容易全部预测为真。Focal Loss 会降低易分类样本的权重,让模型把注意力集中在那些“模糊难辨”的帧上。
4.3 难例挖掘处理
训练过程中可以观察每个 batch 的损失分布,找出损失最高的样本。常见做法是维护一个难例队列,每轮训练后把损失 Top-10% 的样本加入队列,下一轮用队列中的样本做一次额外更新。不用额外写复杂代码,PyTorch 的torch.topk就能解决:
losses = criterion(logits, labels) # logits: [B, 2], labels: [B] topk_loss, topk_idx = torch.topk(losses, k=max(1, losses.size(0)//10)) topk_loss.mean().backward()正常loss.backward()之后再额外对难例做一次反向传播,等效于加重难例的梯度贡献。注意topk_idx是高损失样本在 batch 内的索引,不要把 batch 内的索引和数据集全局索引混淆,否则难例队列会错乱。
4.4 常见的过拟合与特征坍缩排错
训练中如果发现验证集准确率在 5 个 epoch 内冲到 90% 以上,先不要高兴,大概率是模型学到了视频压缩伪影而非面部细节。排查方法是:把验证集中的真实视频做一次轻度高斯模糊再送进去,如果模型预测翻转,说明它依赖的是锐利边缘而非语义特征。
另一个高频问题是特征坍缩,也就是所有视频提取出的特征向量几乎一样,不管真假都映射到同一区域。原因往往是project层输出维度太小时容量不够,或者时序聚合时 GRU 的隐层维度设置过小。至少保证特征维度不低于 256,GRU 隐层不低于 256,并给分类层前的特征加一个nn.BatchNorm1d,能有效避免特征分布偏移。
5. 用可视化验证特征提取效果
5.1 用 Grad-CAM 定位判定依据
模型在黑盒状态下无法证明自己“真的在关注面部细节”,所以需要把最后分类层的梯度回传到特征图,生成热力图来观察模型最敏感的区域。PyTorch 里用 hook 实现,不需要修改网络结构。
def grad_cam(model, aligned_face_tensor): feature_maps = {} gradients = {} def save_feature(module, input, output): feature_maps["value"] = output.detach() def save_gradient(module, grad_input, grad_output): gradients["value"] = grad_output[0].detach() # 注册hook到最后一个卷积层 handle1 = model.backbone.stage4.register_forward_hook(save_feature) handle2 = model.backbone.stage4.register_backward_hook(save_gradient) output = model(aligned_face_tensor.unsqueeze(0)) score = output[0, 1] # 假脸类别的得分 model.zero_grad() score.backward() # 按通道计算梯度均值作为权重 weights = gradients["value"].mean(dim=(2, 3), keepdim=True) cam = (weights * feature_maps["value"]).sum(dim=1, keepdim=True) cam = torch.relu(cam) cam = cam.squeeze().cpu().numpy() handle1.remove() handle2.remove() return cam画热力图的时候,把cam归一化到 0 到 1 之间,用cv2.applyColorMap映射成红色,再按 0.5 的权重叠加到原始人脸图上。真实 Deepfake 案例里,热力区域会集中在眼白边缘、牙齿缝、发际线和耳朵根部,这些位置正是生成器的薄弱环节。如果热力图高亮在额头中央或背景区域,说明模型过拟合了数据集的统计噪声,需要回头检查训练集的划分是否有泄漏。
5.2 用特征分布可视化判断“区分度”
除了热力图,还可以把所有测试视频的特征向量投影到二维平面,观察真假样本的重合程度。用 TSNE 或是 PCA 都行,TSNE 更直观。具体做法是把时序聚合后的 512 维特征拿出来,对每个视频取帧级别特征的平均值,然后调用sklearn.manifold.TSNE。
from sklearn.manifold import TSNE import matplotlib.pyplot as plt tsne = TSNE(n_components=2, perplexity=20, random_state=42) embeddings_2d = tsne.fit_transform(feature_matrix) # feature_matrix: [N, 512] plt.scatter(embeddings_2d[:, 0], embeddings_2d[:, 1], c=video_labels, cmap="bwr") plt.savefig("feature_distribution.png", dpi=120)如果真实样本和伪造样本在两个方向各自聚团,只有少量边界重叠,说明提取的特征已经具备清晰的可分性。如果两类样本完全混合,问题不在后面的全连接层,而在于特征提取本身,需要返回去调整注意力模块或者增加训练数据量。
5.3 几个值得补充的验证角度
特征提取算法做到最后,还可以做一组消融实验来证明每个模块的价值。做法很简单:把通道注意力去掉跑一遍,把时序聚合换成单帧平均跑一遍,对比验证集准确率和热力图差异。这组对比数据写进毕业设计的实验章节,比任何文字描述都有说服力。消融实验的具体数值没有统一标准,但真实场景下的对照结果通常能解释清楚去噪模块和时序模块各自承担的功能。
本文还有配套的精品资源,点击获取