简介:这份资源围绕深度学习图像篡改检测展开,面向对机器视觉与图像真实性认证感兴趣的研究人员、算法工程师及具备一定基础的中高级开发者。内容先梳理两条实现路线:一是微调Faster R-CNN、RGB-N、BiseNet等小模型,二是采用SAM、Grounded-SAM等大模型或多模型协同,随后以PyTorch为框架给出完整代码实例,涵盖自定义数据集类加载图像并标注篡改标签、数据增强与预处理、加载预训练Faster R-CNN并替换分类头改为二分类、配置优化器与学习率调度器、多轮迭代训练直至模型保存部署,最后还涉及篡改测试集精度验证及单张图片独立预测的脚本片段。资源包为1个docx文档,约21KB,以文字讲解配合可运行代码片段呈现,结构紧凑。目前已有163人学习,适合希望将检测网络落到实际项目、系统掌握微调流程与评估思路的读者参考。
1. 图像篡改检测为什么落到 Faster R-CNN 这种区域级模型上
做图像篡改检测的深度学习工程师,多半都踩过同一个坑:拿 U-Net 跑像素分割,掩码可视化很惊艳,一换真实样本就崩——JPEG 重压缩、缩放、二次截图把高频伪影抹掉,模型干脆把整片天空、整片皮肤都判成篡改。问题不在网络深度,而在于“篡改”从来不是一个像素属性,它是一块具有位置、尺寸和边界的内容被替换掉了。
Faster R-CNN 的切入点正在这里。RPN 在共享特征图上生成候选区域,ROI 头对每个候选做分类和边框回归,输出带类别与坐标的矩形框。拼接、复制-移动、局部抹除这三类最常被检验的篡改,业务形态恰好都是某块区域被换掉,用框描述比用概率图更稳,也更容易接进后续处置流程。
代价同样明确:框标注比点标注贵,训练前要写掩码转 bbox 的脚本;模型对细长抹除区域的召回不如分割网络。这些后面用具体参数和代码展开。
2. 用 Python 跑起 Faster R-CNN 篡改检测的最小工程
这一章的目标很具体:在一台有独显的机器上,把 torchvision 自带的 Faster R-CNN 权重换成能在篡改数据上继续训练的状态,并且能读到我们自己的标注。不追求精调,先让整条链路跑通,再在后面的章节里补数据和参数。
2.1 环境依赖与版本选型的实际取舍
torchvision 的检测模块迭代很快,接口在 0.13 前后有过一次比较大的调整,pretrained=True被替换成了weights=...。如果按老教程写,最典型的报错是TypeError: fasterrcnn_resnet50_fpn() got an unexpected keyword argument 'pretrained'。所以先把版本对齐再动手,比事后逐个修报错省事得多。
| 组件 | 建议版本 | 选型理由 |
|---|---|---|
| Python | 3.9 / 3.10 | 3.11 早期部分 CUDA 轮子缺位,3.9-3.10 兼容面最广 |
| PyTorch | 2.1.x | 检测模块接口稳定,半精度推理问题少 |
| torchvision | 0.16.x 及以上 | 使用weights=新接口,与 2.1 对齐 |
| CUDA | 11.8 / 12.1 | 与 PyTorch 官方轮子对应,避免自编译 |
| OpenCV | 4.8+ | 掩码解析、图像 IO 与增强 |
| pycocotools | 2.0.7 | 评估 mAP 时直接复用官方实现 |
装环境的命令按 CUDA 版本二选一即可,不要同时装两个源:
# CUDA 11.8 环境 pip install torch==2.1.2 torchvision==0.16.2 --index-url https://download.pytorch.org/whl/cu118 # 只装 CPU 调试用(跑通代码逻辑够用,训练慢) pip install torch==2.1.2 torchvision==0.16.2 --index-url https://download.pytorch.org/whl/cpu pip install opencv-python pycocotools matplotlib tqdm代码里的关键点是索引地址:它决定了装的是 CUDA 版还是 CPU 版,装错不会报错,只会在torch.cuda.is_available()返回False时才发现。验证命令如下:
import torch, torchvision print(torch.__version__, torchvision.__version__) print("cuda:", torch.cuda.is_available(), torch.version.cuda)如果cuda为False而机器有显卡,先看驱动版本是否高于nvidia-smi里的 CUDA 12.x,再确认是不是混装了 CPU 轮子。
2.2 从 COCO 预训练权重迁移到篡改检测头
直接从头训练检测网络在篡改数据上几乎不可行,样本量通常只有几千张,COCO 的 80 类预训练权重提供了通用的边缘、纹理和物体先验,这对识别“区域边界是否自然”是有帮助的。改造的核心只有一步:换掉 ROI 头的分类器,让输出类别数等于“背景 + 篡改类型数”。
import torchvision from torchvision.models.detection.faster_rcnn import FastRCNNPredictor def build_detector(num_classes=3, min_size=800, max_size=1333): # num_classes 必须包含背景类:1 拼接 + 2 复制-移动 + 3 抹除 -> 传 3 或 4 model = torchvision.models.detection.fasterrcnn_resnet50_fpn( weights=torchvision.models.detection.FasterRCNN_ResNet50_FPN_Weights.COCO_V1, min_size=min_size, # 短边缩放目标,直接影响小目标召回 max_size=max_size, # 长边上限,控制显存占用 box_detections_per_img=200, # 单图最多保留框数,密集篡改时上调 ) in_features = model.roi_heads.box_predictor.cls_score.in_features # 替换分类头,回归头同步重建 model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes) return modelFastRCNNPredictor的第二个参数是含背景的总类别数,篡改类别 3 类时传 4;如果传 3 却在标注里写了类别 3,训练时会在损失计算处抛越界错误。min_size调大到 1000 能提升小篡改块召回,但显存涨得很快,通常配合 batch size 降到 2。box_detections_per_img默认 100,做密集复制-移动检测时偏小,漏检会集中体现在验证集上。
2.3 自定义 Dataset 与篡改标注格式转换
Faster R-CNN 的输入约定是每张图返回一个 dict:image为[C,H,W]的 float tensor,boxes为[N,4]的(x1,y1,x2,y2)绝对坐标,labels为[N]的长整型,area和iscrowd用于评估。缺失字段会在训练时报 key 错,所以模板要写全。
import cv2, torch from torch.utils.data import Dataset class TamperDataset(Dataset): def __init__(self, records, transforms=None): # records: [(img_path, [(x1,y1,x2,y2,label), ...]), ...] self.records = records self.transforms = transforms def __len__(self): return len(self.records) def __getitem__(self, idx): img_path, anns = self.records[idx] img = cv2.imread(img_path) # BGR img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) boxes, labels = [], [] for x1, y1, x2, y2, lb in anns: boxes.append([x1, y1, x2, y2]) labels.append(lb) boxes = torch.as_tensor(boxes, dtype=torch.float32).reshape(-1, 4) labels = torch.as_tensor(labels, dtype=torch.int64) target = { "boxes": boxes, "labels": labels, "area": (boxes[:, 3] - boxes[:, 1]) * (boxes[:, 2] - boxes[:, 0]), "iscrowd": torch.zeros((len(labels),), dtype=torch.int64), "image_id": torch.tensor([idx]), } img = torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 if self.transforms is not None: img, target = self.transforms(img, target) return img, target两个容易忽略的点:一是area必须用变换后的框重新算,如果增强里做了随机裁剪却把原始 area 带进去,COCO 评估会给出错误数值;二是空标注图片要保留,boxes设成[[0,0,0,0]]且labels为[0]并不能当背景,正确做法是让框数量为 0,靠负样本图自身参与 RPN 的负样本分配,否则模型会学到“每张图必有篡改”的偏置。
3. 篡改区域标注与数据增强:把边界和纹理一起学进去
检测精度上不去,八成不是网络不够深,而是采样和增强没对准篡改的物理特性。拼接改变的是局部噪声与压缩历史,复制-移动改变的是区域间自相似性,局部抹除拉开的是纹理与结构的落差,三者需要的增强方式并不一样。
3.1 掩码转 bbox 与重叠框的合并策略
标注工具常给的是像素掩码,需要先做连通域分析再转框。一张图里同一篡改块可能被切成多个连通域,必须按类别做一次合并,否则一个篡改区域会变成十几个碎片框,RPN 学到的尺度分布会被带偏。
import cv2, numpy as np def mask_to_boxes(mask, label, min_area=64, merge_iou=0.1): # mask: 单通道 0/1 掩码;返回 [(x1,y1,x2,y2,label), ...] num, labels_map = cv2.connectedComponents(mask.astype(np.uint8)) raw = [] for i in range(1, num): ys, xs = np.where(labels_map == i) if len(xs) < min_area: # 去噪点,防小碎片干扰 continue raw.append([xs.min(), ys.min(), xs.max(), ys.max()]) # 简单合并:中心距离近或高度重叠的框归并 merged = [] for b in raw: for m in merged: if _iou(b, m) > merge_iou: m[0], m[1] = min(m[0], b[0]), min(m[1], b[1]) m[2], m[3] = max(m[2], b[2]), max(m[3], b[3]) break else: merged.append(list(b)) return [(*b, label) for b in merged]min_area是最值得调的参数:调小会引入标注边缘抖动产生的细碎框,调大则会把细长的抹除痕迹整条丢掉。经验值是在原图分辨率下取 64 到 256 之间,图片长边超过 2000 像素时按比例放大。merge_iou设 0.1 是偏松的合并策略,因为篡改标注的主观边界差异大,严格按 IoU 合并反而不如按距离聚类稳。
3.2 拼接与复制-移动各自的增强方式
通用增强里,随机裁剪和颜色抖动对检测模型有益,但旋转和翻转会破坏拼接痕迹的方向一致性,需要区别对待。下面这张表是常用策略与适用场景的对应关系。
| 增强 | 作用 | 推荐强度 | 适用篡改类型 |
|---|---|---|---|
| RandomHorizontalFlip | 扩充样本,代价低 | p=0.5 | 全部 |
| 小角度旋转 | 提升姿态鲁棒 | ±10° | 拼接、复制-移动 |
| JPEG 重压缩 | 逼近真实取证场景 | quality 70-95 | 全部,必做 |
| 缩放重采样 | 破坏重采样痕迹一致性 | 0.8-1.2 倍 | 拼接 |
| 高斯模糊 | 模拟二次编辑 | σ=0.5-1.5 | 抹除 |
| 颜色抖动 | 弱化颜色先验 | 轻微 | 拼接 |
JPEG 重压缩是必须加的,标注数据大多来自 PNG 无损裁剪,直接用会让模型依赖不存在的无损高频特征,换到真实图就废。实现上把压缩和解压写进 transform,随训练轮次随机切换 quality。
3.3 正负样本比例与难例挖掘
RPN 阶段正负样本默认按 1:1 采样,但篡改区域通常只占整图 3% 到 10%,负样本天然占压倒多数。如果整批数据里负样本图(无篡改)占比过高,分类头会长期偏向背景,表现为训练 loss 很快降到很低但召回极差。常见做法是把无篡改图控制在总样本的 20% 到 30%,并在训练中期做一次难例回放:把验证集里假阳性最高的若干张图加入下一轮训练。这个动作不需要改网络结构,只改 sampler 的数据来源,是提升精度最省力的手段之一。
4. Faster R-CNN 关键参数调优与训练排错
到了这一步,网络跑得通、数据也到位,剩下的差距基本都在参数上。篡改检测与通用目标检测最大的不同是:目标尺寸跨度大、形状偏细长、正样本稀疏,默认锚框配置并不是为这个场景设计的。
4.1 RPN 锚框尺寸与篡改区域尺度的匹配
默认锚框是(32,64,128,256,512)五组、三比例,对自然图像里的物体很合适,但抹除痕迹可能只有 20 像素宽、横跨大半张图。这种形状用默认锚框很难匹配上正样本,RPN 召回会持续偏低。
from torchvision.models.detection.anchor_utils import AnchorGenerator # 针对细长和中等尺寸篡改区域重新定义锚框 anchor_generator = AnchorGenerator( sizes=((16,), (32,), (64,), (128,), (256,), (512,)), # 每层一个尺度 aspect_ratios=((0.25, 0.5, 1.0, 2.0, 4.0),) # 增加横竖细长比例 ) model.rpn.anchor_generator = anchor_generatorsizes的层数必须和 FPN 的特征图数量一致,否则前向时报尺寸不匹配;aspect_ratios加宽到 0.25 和 4.0 是专门照顾细长区域的,代价是每个位置生成的锚框数量翻倍,显存和推理时间都会涨约 20%。如果数据集里篡改区域普遍小于 40 像素,最有效的手段其实是提高输入分辨率,而不是继续加密锚框。
4.2 ROI Align 与 NMS 阈值对密集篡改的影响
Faster R-CNN 用 ROI Align 替代了早期的 ROI Pooling,避免了量化误差,这对边界敏感的篡改检测尤其重要。这一点在 torchvision 实现里默认就是 Align,不用改。
真正需要动的是 NMS。复制-移动检测中,同一块纹理可能被复制到多个位置,出现多个真实框紧挨的情况,nms_thresh设小会互相抑制掉。默认 0.5,密集场景建议放到 0.6 到 0.7,同时把detections_per_img调高:
from torchvision.models.detection.roi_heads import RoIHeads from torchvision.ops import MultiScaleRoIAlign model.roi_heads.nms_thresh = 0.65 # 放宽抑制,减少相邻篡改块互相吞掉 model.roi_heads.detections_per_img = 300 # 密集小目标场景 model.roi_heads.score_thresh = 0.05 # 保留低分框供评估,推理时可再筛score_thresh在训练阶段只影响box_detections_per_img的截断,不影响 loss;评估时把它调低到 0.05 能拿到更完整的 PR 曲线,最终业务部署时再按精确率要求放宽或收紧。
4.3 损失曲线异常的四种典型排查
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| rpn_objectness 长期不降 | 锚框尺度不匹配、正样本太少 | 重设锚框、检查掩码转框是否丢框 |
| 分类 loss 降但 box_reg 不降 | 标注框边界抖动大 | 收紧合并策略、复查标注质量 |
| 验证 mAP 卡在 0.2 附近 | 数据里无篡改图过少 | 调整正负样本比、加难例回放 |
| loss 出现 NaN | 学习率过高、框宽高为 0 | 学习率降到 1e-4、过滤退化框 |
框宽高为 0 是最隐蔽的一种缺陷:标注时把点标成了框,area算出 0,除零后 loss 直接 NaN。训练前跑一遍数据校验,把所有宽或高小于 1 像素的框打印出来,比事后调学习率有效得多。
5. 推理加速与效果验证的落地技巧
训练收敛只是完成一半,真正上线还要面对吞吐和验证口径。检测模型的推理瓶颈通常不在主干网络,而在 RPN 生成的候选框数量和 NMS 的排序开销。
5.1 批量推理与半精度
推理阶段可以不保留梯度,配合torch.inference_mode()和 FP16,吞吐通常能翻倍:
import torch model.eval() with torch.inference_mode(): # 预处理后的 tensors 是列表形式,Faster R-CNN 天然支持多图输入 with torch.autocast("cuda", dtype=torch.float16): preds = model(images)autocast只对卷积和矩阵乘生效,NMS 与坐标计算仍在 FP32 执行,因此不会因为半精度导致框偏移。注意images是 list 而不是堆叠 tensor,Faster R-CNN 的前向签名接受不等尺寸图,强行 pad 成同一尺寸反而浪费算力。
5.2 用 mAP 与框级可视化定位漏检
只看 mAP 会掩盖很多问题,建议把验证集按篡改面积分档统计:小于 1% 面积、1% 到 10%、大于 10% 三档分别算 AP。小面积档的 AP 明显偏低,说明输入分辨率或锚框要调整;各档都低,就得回头看标注。
可视化验证时,把预测框和真值框画在同一张图上,用不同颜色区分,并标注分数。实际操作中比数值更直观的做法是:筛出分数在 0.3 到 0.5 之间的框,这些是模型“犹豫”的区域,通常对应边界模糊或增强后失真的样本,把它们单独挑出来复查标注,往往能发现一批系统性错误。
推理阶段的min_size可以和训练时不同,部署时设为训练值的 1.2 到 1.5 倍,能在不重训的前提下换回一部分小目标召回,代价是单张耗时上升。这个旋钮适合在验收前做最后一次平衡。
本文还有配套的精品资源,点击获取