ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Faster RCNN口罩识别实战:从源码跑通到模型训练完整指南

Faster RCNN口罩识别实战:从源码跑通到模型训练完整指南 简介这份资源是面向计算机、人工智能及相关专业学生与开发者的课程设计/毕设级项目主题为基于Faster R-CNN的人脸口罩识别系统可用于课程作业、毕业设计或项目初期立项演示。压缩包共5个文件约881KB包含2个Python源码文件、2张运行效果截图和1份说明文档分别承担训练与识别部署、结果展示及使用指引等用途。项目提供数据集目录规范、训练脚本与Streamlit部署代码并附有模型文件说明读者可据此完成从数据准备、模型训练到口罩人脸识别界面部署的完整流程理解Faster R-CNN在目标检测任务中的落地方式。代码经测试可运行答辩评审平均分达96分适合基础尚可者在此基础上修改扩展功能。目前已有203人学习可作为课程设计参考与实战练手素材。1. 从一张没戴口罩的脸说起Faster RCNN 口罩识别到底能做成什么样地铁闸机口、医院分诊台、学校门禁这些场景里摄像头每天都在拍人脸但真正需要系统回答的问题只有一个这张脸上有没有口罩。课程设计里选这个题目的人不少原因很直接——它同时踩中了目标检测和公共场景两个点数据集好找、模型有现成框架、跑出来的结果肉眼可见。但真动手做的时候大部分人卡在三个地方Faster RCNN 的代码跑不通、自己的数据集标注格式对不上、训练完模型不知道该怎么验证。这篇笔记就按「先跑通官方 demo → 换成口罩数据集 → 调参训练 → 排查翻车点」的顺序把基于 Faster RCNN 的人脸口罩识别系统从源码到模型完整走一遍。适合正在做课程设计、需要一份能复现的 Python 目标检测方案的人也适合已经跑过 YOLO 想换 Faster RCNN 试试手感的熟手。2. Faster RCNN 为什么适合口罩识别两阶段检测的取舍2.1 两阶段检测和单阶段检测的本质区别目标检测的模型大致分两派。一派是单阶段代表是 YOLO 系列和 SSD一张图进去直接回归出框和类别速度快适合实时视频流。另一派是两阶段代表就是 Faster RCNN它先把可能的目标区域找出来再对这些区域逐个分类和微调框的位置。Faster RCNN 的流程拆开看是四步。第一步用一组卷积网络常见是 VGG16 或 ResNet50把输入图变成特征图。第二步RPNRegion Proposal Network在特征图上滑动生成一堆候选框每个框带一个「有没有目标」的二分类分数。第三步把候选框映射回特征图用 RoI Pooling 或 RoI Align 统一成固定尺寸。第四步分类头判断每个框属于哪一类回归头修正框的坐标。口罩识别这个任务有个特点人脸本身尺度变化大近处的人脸可能占半张图远处的人脸只有几十个像素。两阶段检测因为先做候选框再做分类对小目标的召回率通常比单阶段稳一些。代价是推理慢一张 1080P 的图在普通显卡上可能要 200 毫秒以上。课程设计场景下如果只是对图片或离线视频做检测这个速度完全够用如果要做实时摄像头流就得考虑换 backbone 或者降输入分辨率。2.2 口罩识别数据集长什么样公开的口罩检测数据集一般有两种标注方式。一种是 VOC 格式每张图对应一个 XML 文件里面记录每个框的坐标和类别名。另一种是 COCO 格式用一个 JSON 文件统一管理所有图的标注。Faster RCNN 的 PyTorch 实现通常吃 VOC 格式所以拿到 COCO 格式的数据集需要先转。类别设置上常见的是两类with_mask和without_mask。也有三类版本多一个mask_weared_incorrect表示口罩戴得不规范。课程设计里建议先用两类跑通因为三类样本不均衡会更严重训练时容易偏向样本多的类。数据集规模方面公开口罩数据集通常在 7000 到 10000 张图之间其中戴口罩和不戴口罩的比例大概是 8:2 或 7:3。这个不均衡会直接影响训练后面调参章节会具体讲怎么处理。2.3 源码结构一份能跑的 Faster RCNN 工程包含什么一份完整的口罩识别工程目录结构通常是这样mask_detection/ ├── data/ │ ├── annotations/ # XML 标注文件 │ └── images/ # 原始图片 ├── dataset/ # 转换后的训练数据 ├── model/ │ ├── faster_rcnn.py # 模型定义 │ └── backbone.py # 特征提取网络 ├── utils/ │ ├── dataset.py # 数据加载 │ ├── transforms.py # 数据增强 │ └── engine.py # 训练和验证循环 ├── train.py # 训练入口 ├── predict.py # 推理脚本 └── config.py # 超参数配置这个结构不是固定的但核心模块就这几个数据加载、模型定义、训练循环、推理脚本。拿到一份源码后先看config.py里有哪些参数再看train.py的入口逻辑最后看dataset.py怎么读标注。这三处看懂了整个工程就能改。3. 把源码跑起来环境配置和最小验证3.1 环境依赖和版本选择Faster RCNN 的 PyTorch 实现对环境版本比较敏感尤其是 torchvision 的版本。常见组合是 PyTorch 1.10 到 1.13 配 torchvision 0.11 到 0.14。如果版本对不上torchvision.models.detection里的接口可能会报参数错误。安装命令按 CUDA 版本走以 CUDA 11.3 为例# 创建虚拟环境 conda create -n mask_rcnn python3.8 conda activate mask_rcnn # 安装 PyTorch 和 torchvision pip install torch1.12.1cu113 torchvision0.13.1cu113 -f https://download.pytorch.org/whl/torch_stable.html # 安装其他依赖 pip install opencv-python pillow matplotlib numpy tqdm lxml这里lxml是用来解析 VOC 格式 XML 文件的opencv-python用来做图像预处理和画框。如果机器上没有 GPU把cu113去掉装 CPU 版本也能跑只是训练会慢很多。提示装完 torch 后先用python -c import torch; print(torch.cuda.is_available())确认 GPU 是否可用返回 False 的话后面训练会默认走 CPU。3.2 用官方预训练模型做一次最小推理在换自己的数据集之前先用 torchvision 自带的预训练 Faster RCNN 跑一张图确认环境没问题。这段代码不依赖任何项目文件单独存成test_env.py就能跑import torch import torchvision from PIL import Image import torchvision.transforms as T # 加载预训练的 Faster RCNNCOCO 数据集 91 类 model torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrainedTrue) model.eval() # 读一张测试图转成 tensor img Image.open(test.jpg).convert(RGB) transform T.Compose([T.ToTensor()]) img_tensor transform(img) # 推理 with torch.no_grad(): prediction model([img_tensor]) # 打印检测结果 boxes prediction[0][boxes] labels prediction[0][labels] scores prediction[0][scores] # 只保留置信度大于 0.5 的框 keep scores 0.5 print(检测到, keep.sum().item(), 个目标) print(框坐标:, boxes[keep]) print(类别:, labels[keep])这段代码的逻辑是加载模型 → 图片转 tensor → 前向推理 → 过滤低置信度框。pretrainedTrue会下载 COCO 预训练权重第一次跑需要联网。scores 0.5是置信度阈值调低会检出更多框但误检也会增加调高则相反。如果这一步能正常输出框和类别说明环境没问题可以进入下一步。3.3 把 VOC 格式数据集转成训练需要的格式大部分口罩数据集给的是 VOC 格式每张图一个 XML。Faster RCNN 训练时需要的是图片路径列表和对应的框坐标、类别标签。写一个转换脚本把 XML 解析成统一的 CSV 或 JSONimport os import xml.etree.ElementTree as ET import pandas as pd def parse_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() # 图片文件名 filename root.find(filename).text # 遍历所有目标框 boxes [] for obj in root.findall(object): name obj.find(name).text bbox obj.find(bndbox) xmin int(bbox.find(xmin).text) ymin int(bbox.find(ymin).text) xmax int(bbox.find(xmax).text) ymax int(bbox.find(ymax).text) boxes.append({filename: filename, class: name, xmin: xmin, ymin: ymin, xmax: xmax, ymax: ymax}) return boxes # 遍历 annotations 目录下所有 XML all_records [] for xml_file in os.listdir(data/annotations): if xml_file.endswith(.xml): all_records.extend(parse_xml(os.path.join(data/annotations, xml_file))) df pd.DataFrame(all_records) df.to_csv(dataset/labels.csv, indexFalse) print(共解析, len(df), 个标注框)解析逻辑是读 XML → 找 filename → 遍历 object 节点 → 提取类别名和四个坐标 → 存成 CSV。xmin/ymin/xmax/ymax是左上角和右下角坐标VOC 格式固定用这四个字段。转换完检查一下 CSV 行数是否和标注框总数一致不一致通常是 XML 里有空 object 或者坐标越界。3.4 自定义 Dataset 类的三个关键方法PyTorch 的 Dataset 类必须实现__init__、__len__、__getitem__三个方法。口罩识别的 Dataset 大概长这样import torch from torch.utils.data import Dataset from PIL import Image import pandas as pd class MaskDataset(Dataset): def __init__(self, csv_file, img_dir, transformsNone): self.df pd.read_csv(csv_file) self.img_dir img_dir self.transforms transforms # 类别映射0 是背景必须保留 self.class_map {with_mask: 1, without_mask: 2} def __len__(self): return self.df[filename].nunique() def __getitem__(self, idx): # 取第 idx 张图的所有标注 filename self.df[filename].unique()[idx] records self.df[self.df[filename] filename] img_path os.path.join(self.img_dir, filename) img Image.open(img_path).convert(RGB) # 组装框和标签 boxes [] labels [] for _, row in records.iterrows(): boxes.append([row[xmin], row[ymin], row[xmax], row[ymax]]) labels.append(self.class_map[row[class]]) boxes torch.tensor(boxes, dtypetorch.float32) labels torch.tensor(labels, dtypetorch.int64) target {boxes: boxes, labels: labels} if self.transforms: img self.transforms(img) return img, target__len__返回的是图片数量而不是标注框数量所以用nunique()去重。__getitem__里先把同一张图的所有框取出来再转成 tensor。类别映射里 0 必须留给背景这是 Faster RCNN 的约定如果从 0 开始标类别训练时会报 label 越界。4. 训练自己的口罩模型参数怎么设、loss 怎么看4.1 模型定义换掉分类头用 torchvision 的 Faster RCNN 做迁移学习核心操作是替换分类头把原来的 91 类改成 3 类背景 戴口罩 不戴口罩import torchvision from torchvision.models.detection.faster_rcnn import FastRCNNPredictor def build_model(num_classes3): # 加载预训练模型 model torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrainedTrue) # 获取分类头的输入特征数 in_features model.roi_heads.box_predictor.cls_score.in_features # 替换成新的分类头 model.roi_heads.box_predictor FastRCNNPredictor(in_features, num_classes) return modelnum_classes3对应背景、戴口罩、不戴口罩。in_features是 backbone 输出的特征维度ResNet50 FPN 通常是 1024。替换分类头后backbone 的预训练权重保留分类头随机初始化这样训练时收敛更快。4.2 训练循环和 loss 组成Faster RCNN 的 loss 是四项之和RPN 的分类 loss、RPN 的框回归 loss、最终分类 loss、最终框回归 loss。训练时模型返回一个 dict直接求和反向传播import torch from torch.utils.data import DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(num_classes3).to(device) # 数据集和加载器 dataset MaskDataset(dataset/labels.csv, data/images, transformsNone) dataloader DataLoader(dataset, batch_size4, shuffleTrue, collate_fnlambda x: tuple(zip(*x))) # 优化器 params [p for p in model.parameters() if p.requires_grad] optimizer torch.optim.SGD(params, lr0.005, momentum0.9, weight_decay0.0005) # 学习率调度每 3 个 epoch 降一次 lr_scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size3, gamma0.1) num_epochs 10 for epoch in range(num_epochs): model.train() epoch_loss 0 for images, targets in dataloader: images [img.to(device) for img in images] targets [{k: v.to(device) for k, v in t.items()} for t in targets] loss_dict model(images, targets) losses sum(loss for loss in loss_dict.values()) optimizer.zero_grad() losses.backward() optimizer.step() epoch_loss losses.item() lr_scheduler.step() print(fEpoch {epoch1}, Loss: {epoch_loss/len(dataloader):.4f})collate_fn这里必须自定义因为每张图的框数量不一样默认的 collate 会报错。batch_size4是显存够的情况8G 显存建议降到 2。学习率0.005是 SGD 的常用起点如果 loss 震荡厉害可以降到0.001。StepLR每 3 个 epoch 把学习率乘 0.1防止后期震荡。4.3 训练过程怎么判断模型在变好光看总 loss 不够要把四项 loss 分开看。正常情况下loss_objectness和loss_rpn_box_reg在前 2 个 epoch 下降最快说明 RPN 在学会找候选框。loss_classifier下降稍慢因为分类头是随机初始化的。loss_box_reg在后期会趋于平稳如果一直不降说明框回归没学好。如果loss_objectness一直很高检查一下标注框有没有越界或者宽高为 0 的情况。如果loss_classifier震荡多半是学习率太大或者 batch 里类别极度不均衡。注意训练时模型处于 train 模式会返回 loss推理时要先调model.eval()否则不会返回预测结果。4.4 用验证集算 mAP训练完不能只看 loss要用验证集算 mAP。torchvision 没有内置 mAP 计算可以自己写一个简化版def compute_iou(box1, box2): # 计算两个框的交并比 x1 max(box1[0], box2[0]) y1 max(box1[1], box2[1]) x2 min(box1[2], box2[2]) y2 min(box1[3], box2[3]) inter max(0, x2 - x1) * max(0, y2 - y1) area1 (box1[2]-box1[0]) * (box1[3]-box1[1]) area2 (box2[2]-box2[0]) * (box2[3]-box2[1]) union area1 area2 - inter return inter / union if union 0 else 0 def evaluate(model, dataloader, device, iou_thresh0.5): model.eval() tp, fp, fn 0, 0, 0 with torch.no_grad(): for images, targets in dataloader: images [img.to(device) for img in images] outputs model(images) for output, target in zip(outputs, targets): pred_boxes output[boxes].cpu() pred_scores output[scores].cpu() gt_boxes target[boxes] # 只保留置信度大于 0.5 的预测 keep pred_scores 0.5 pred_boxes pred_boxes[keep] # 简单匹配每个 gt 找最大 IoU 的预测 matched set() for gt in gt_boxes: best_iou, best_idx 0, -1 for i, pb in enumerate(pred_boxes): if i in matched: continue iou compute_iou(gt, pb) if iou best_iou: best_iou, best_idx iou, i if best_iou iou_thresh: tp 1 matched.add(best_idx) else: fn 1 fp len(pred_boxes) - len(matched) precision tp / (tp fp) if (tp fp) 0 else 0 recall tp / (tp fn) if (tp fn) 0 else 0 print(fPrecision: {precision:.4f}, Recall: {recall:.4f}) return precision, recall这个评估逻辑是对每个 gt 框找 IoU 最大的预测框超过阈值算 TP否则算 FN没匹配上的预测框算 FP。iou_thresh0.5是常用标准调高会更严格。算出来的 precision 和 recall 能直观反映模型有没有漏检或误检。5. 训练口罩识别模型最容易翻车的五个地方5.1 现象训练 loss 一直不降卡在 2.0 左右原因最常见的是类别标签从 0 开始标。Faster RCNN 要求 0 是背景如果with_mask标成 0训练时分类头会把背景和口罩混在一起loss 降不下去。解决检查class_map确保背景是 0实际类别从 1 开始。改完重新生成 labels.csv。5.2 现象推理时框全图乱飞置信度都很低原因训练时用了数据增强但推理时没做同样的归一化。比如训练时图片被 resize 到 800x800推理时输入原图模型看到的尺度对不上。解决推理脚本里加和训练一致的预处理或者训练时不做 resize让模型自己处理多尺度。5.3 现象不戴口罩的样本几乎检不出来原因数据集里戴口罩和不戴口罩比例可能是 8:2模型偏向多数类。加上不戴口罩的人脸特征变化大模型学到的特征不够。解决两种做法。一是过采样把不戴口罩的图复制多份二是在 loss 里给少数类加权。torchvision 的 Faster RCNN 不直接支持类权重可以在 Dataset 里做采样。5.4 现象训练到第 5 个 epoch 后 loss 突然变成 NaN原因学习率太大或者梯度爆炸。SGD 的 lr 设到 0.01 以上时容易出现。解决把 lr 降到 0.001 或 0.005加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm10.0)加在losses.backward()之后、optimizer.step()之前。5.5 现象显存不够batch_size 降到 1 还是 OOM原因Faster RCNN 的 RPN 会生成大量候选框显存占用和输入图尺寸强相关。默认输入是 800x1333显存小的卡扛不住。解决在模型定义时限制输入尺寸model torchvision.models.detection.fasterrcnn_resnet50_fpn( pretrainedTrue, min_size600, max_size800 )把min_size和max_size调小能显著降显存代价是小目标检测精度会降一些。6. 把模型用起来推理脚本和效果验证的几个技巧训练完的模型要能实际用起来推理脚本得处理好几件事加载权重、预处理、后处理画框、保存结果。下面是一个完整的推理函数import torch import cv2 import numpy as np from PIL import Image import torchvision.transforms as T def predict_image(model, image_path, device, score_thresh0.5): model.eval() img Image.open(image_path).convert(RGB) img_tensor T.ToTensor()(img).to(device) with torch.no_grad(): output model([img_tensor])[0] # 转成 numpy 画框 img_cv cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) boxes output[boxes].cpu().numpy() labels output[labels].cpu().numpy() scores output[scores].cpu().numpy() # 类别名映射 class_names {1: with_mask, 2: without_mask} colors {1: (0, 255, 0), 2: (0, 0, 255)} for box, label, score in zip(boxes, labels, scores): if score score_thresh: continue x1, y1, x2, y2 map(int, box) color colors.get(label, (255, 255, 0)) cv2.rectangle(img_cv, (x1, y1), (x2, y2), color, 2) text f{class_names.get(label, unknown)}: {score:.2f} cv2.putText(img_cv, text, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(result.jpg, img_cv) return img_cv这段代码的关键在后处理score_thresh控制显示哪些框class_names和colors把类别映射成可读文字和颜色。实际用的时候score_thresh建议从 0.5 开始调如果漏检多就降到 0.3误检多就升到 0.7。验证模型效果不能只看几张图要统计一批测试图的结果。我一般会做三件事第一按场景分组测试。室内正脸、室外侧脸、戴口罩遮挡、不戴口罩遮挡各挑 20 张分别算 precision 和 recall。如果某一组明显差说明模型对该场景过拟合不够。第二看混淆矩阵。把with_mask被预测成without_mask的情况单独拎出来通常是口罩颜色和肤色接近导致。第三测极端情况。比如全黑图、纯色图、多人重叠图看模型会不会输出一堆假框。如果假框多说明 RPN 的loss_objectness没训好可以回去检查正负样本比例。最后说一个我踩过的坑模型在验证集上 mAP 0.85但实际用的时候发现戴口罩的人脸框总是偏大。原因是训练时标注框画得松模型学到了这个偏差。后来把标注框收紧到贴合口罩边缘推理框就准了。标注质量对最终效果的影响比调参大得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表