简介:资源是一套基于YOLOv5的人脸检测与人脸计数完整复现项目,适合深度学习初学者、目标检测方向研究者以及希望快速上手YOLO实战流程的开发者阅读使用。内容涵盖训练数据集、模型配置、测试脚本与详细过程记录,可帮助读者从数据预处理、模型训练、验证测试到结果可视化,完整理解并跑通人脸检测与计数任务。包体共1264个文件,以json标注、png/bmp/jpg图像、yaml模型配置、py训练脚本、log日志、pt权重等为主,压缩包大小约340.72MB。目前已有1455人学习。资源内附人脸训练数据集、yolov5-master工程、训练日志与权重文件,并配有《基于YOLO的人脸检测和人脸计数复现代码过程.docx》说明文档,方便对照复盘;同时提供测试图像与mp4视频,便于验证模型在不同场景下的检测效果。对于锚框调整、损失函数修改、检测结果后处理和人脸计数思路,也有实际代码与日志可供参考。
1. 复现前先问自己:这份YOLOv5人脸计数代码改了什么
很多人下载到“基于YOLO的人脸检测和人脸计数复现”资源包后,第一反应是解压、装环境、直接跑train.py,然后卡在“训练几分钟loss不降”上。实际上,这个包里并没有现成的权重文件,而是给了一堆000 (7).bmp这类测试图、一个events.out.tfevents.*训练日志,以及yolov5-master的完整框架。复现的关键不是跑通官方demo,而是把默认的COCO 80类检测改成单类人脸检测,同时把针对通用目标设计的锚框替换成人脸这种小目标适用的尺度,否则效果根本没法看。下面按数据准备、结构修改、训练调参、推理计数、工程落地五个阶段展开。
2. 数据准备与标注:把BMP图像变成YOLOv5能吃的txt标签
2.1 训练集目录结构:images与labels一一对应
YOLOv5读取数据时并不关心图片是jpg还是bmp,它要求每个标注文件与图片同属一个文件名、后缀不同:图片是000 (7).bmp,标注就是000 (7).txt,并且放在对应的images/和labels/目录下。典型的目录组织方式如下:
data/ ├── images/ │ ├── train/ │ │ ├── 000 (7).bmp │ │ └── ... │ └── val/ │ └── ... ├── labels/ │ ├── train/ │ │ ├── 000 (7).txt │ │ └── ... │ └── val/ │ └── ...train.py只认识 images 路径,会按后缀自动去 labels 目录找同名txt。最容易犯的错是把标注文件命名为000 (7).txt.txt,或者train和val的图片混放。检查方式很简单:用下面命令统计没有标注的图片数。
for f in data/images/train/*.bmp; do name=$(basename "$f" .bmp) [ ! -f "data/labels/train/${name}.txt" ] && echo "$name" done这个循环的作用是遍历train目录下所有bmp,取出不带扩展名的文件名,再检查labels目录里是否存在同名txt,没有就打印出来。如果有输出,说明有遗漏标注,YOLOv5的训练会把这些图当作背景图,导致模型被喂入大量负样本,最终表现为“什么都检不出来”或“对所有区域都输出低置信度框”。
2.2 无标注怎么办:用dlib或MTCNN生成初始框
资源包里只给了原始bmp图片,并没有配套的标签。常见做法是先跑一遍现成的人脸检测器生成粗框,再人工修正。以dlib为例,dlib.get_frontal_face_detector()虽然只有HOG+线性分类器,但对正脸、光线正常的照片效果足够,适合偷懒做初稿。
import dlib, cv2, os, glob detector = dlib.get_frontal_face_detector() def bmp_to_yolo(img_path, out_path): img = cv2.imread(img_path) h, w = img.shape[:2] faces = detector(img, 1) if len(faces) == 0: return with open(out_path, 'w') as f: for face in faces: x, y, r, b = face.left(), face.top(), face.right(), face.bottom() # 越界保护,防止坐标超出图像范围 x, y = max(x, 0), max(y, 0) r, b = min(r, w), min(b, h) cx = (x + r) / 2 / w cy = (y + b) / 2 / h fw = (r - x) / w fh = (b - y) / h f.write(f"0 {cx:.6f} {cy:.6f} {fw:.6f} {fh:.6f}\n") for bmp in glob.glob("data/images/train/*.bmp"): base = os.path.splitext(os.path.basename(bmp))[0] bmp_to_yolo(bmp, f"data/labels/train/{base}.txt")这段代码把dlib返回的人脸框坐标从像素值转成YOLO要求的归一化中心点坐标和宽高:cx = (x+r)/2/w、fw = (r-x)/w,所有值都在0到1之间。原始人脸框如果越界会被max/min裁掉,避免训练时边界框为负或超过1。detector(img, 1)中第二个参数表示上采样次数,值越大越容易检出小人脸,但速度也成倍增加。
提示:dlib生成的是矩形框,有时会把脖子、肩膀框进去,而YOLOv5训练时根据正样本坐标回归,并不会把矩形中的背景区域当成有效特征。所以只要矩形中心点落在人脸主体内,最终模型也能学到近似矩形框。
2.3 数据划分:按人员分组比随机划分更合理
训练集和验证集如果随机打散,同一个人不同角度的图片会同时进入左右两边,验证结果虚高。对人脸计数项目,我一般先按文件名前缀或文件夹把人分组,再按80/20比例划分。
import os, random, shutil from collections import defaultdict imgs = glob.glob("cut_faces_raw/*.bmp") # 替换成你的原图目录 group = defaultdict(list) for p in imgs: key = os.path.basename(p).split('_')[0] # 按前缀分人 group[key].append(p) for g, paths in group.items(): random.shuffle(paths) sp = int(len(paths) * 0.8) for p in paths[:sp]: shutil.copy(p, "data/images/train/") for p in paths[sp:]: shutil.copy(p, "data/images/val/")这里的key = basename(p).split('_')[0]假设你的图片命名是“人名_序号.bmp”,如果不满足就需要手动改分组逻辑。划分的目的是防止同一个人重复出现在训练和验证集,否则模型其实只是记住了这张脸,泛化性测试就没了意义。数据准备好后,下一步是改YOLOv5的网络配置。
3. 改网络结构与人脸训练参数:从yolov5s.yaml到face.yaml
3.1 nc=1与锚框重算:单类检测的形态变化
YOLOv5的检测头输出通道数是3 * (5 + nc),3表示每个特征层预设的3个锚框,5是坐标四值加一个objectness,nc是类别数。COCO的nc=80,所以yolov5s.yaml里把nc改成1后,模型参数量会明显下降,但更重要的是锚框要变。
默认锚框是COCO统计出来的:
anchors: - [10,13, 16,30, 33,23] - [30,61, 62,45, 59,119] - [116,90, 156,198, 373,326]第一行最小的10x13对于人脸来说还是太大。人脸在640x640输入下,常见宽度只有20~60像素,长宽比接近1:1。所以直接把anchors换成更密集的小框:
anchors: - [5,5, 8,9, 15,15] - [21,21, 30,30, 46,46] - [65,65, 95,95, 140,140]这里第一行对应80x80的浅层特征图,负责捕捉20像素以下的人脸;第二行对应40x40中层;第三行对应20x20深层,负责大脸。锚框的作用是给回归提供先验,所以不必和真实框完全一致,只要数量级对即可。如果你懒得手算,YOLOv5在训练时会启用autoanchor重新聚类,命令加--noautoanchor可以关闭,但建议先让它自己算一轮,然后看runs/train/exp/anchors.png里的聚类结果再调整。
3.2 数据配置face.yaml:路径、类别和超参
在data/下新建face.yaml:
train: data/images/train val: data/images/val nc: 1 names: ['face']这是最精简版。如果服务器内存充足,可以在训练命令中加--cache让图片全部缓存进内存,减少磁盘IO等待。更多超参数不写在这个文件里,而是放在data/hyps/hyp.scratch-low.yaml,训练时用--hyp指定。对人脸这类单类、小目标任务,我通常调大hsv_h、hsv_s来增强肤色变化:
hsv_h: 0.015 # 色调增强幅度 hsv_s: 0.7 # 饱和度增强,越大越不容易过拟合 fliplr: 0.5 # 水平翻转概率这三个值控制的是颜色扰动和翻转。人脸检测对光照敏感,适度增大hsv_s能让模型学会在不同光线下找人脸;但hsv_h调太大会出现彩色人脸,反而破坏特征。所以0.015左右是经验值。
3.3 训练命令与关键参数表
启动训练的命令如下:
python train.py --img 640 --batch 16 --epochs 100 --data face.yaml \ --weights yolov5s.pt --hyp data/hyps/hyp.scratch-low.yaml --cache--weights yolov5s.pt的意义是从COCO预训练权重开始迁移学习。虽然类别不同,但浅层卷积提取的纹理、边缘特征仍然有用,从零训练100轮很难收敛到同样精度。如果你是在AMD显卡上跑,PyTorch需要换成ROCm版本,命令本身不用变,只是依赖安装方式不同。
| 参数 | 作用 | 人脸任务推荐值 |
|---|---|---|
--img | 训练输入分辨率 | 640,小脸多可上768 |
--batch | 批次大小 | 16(8G显存用8) |
--epochs | 训练轮数 | 100起步,看loss决定 |
--weights | 初始权重或预训练权重 | yolov5s.pt |
--hyp | 数据增强与损失超参 | hyp.scratch-low.yaml |
--cache | 预加载图像到内存 | 加快训练 |
注意batch和学习率的关系:YOLOv5默认学习率是按batch=64标定的,如果你改成16,建议把--lr0从默认的0.01调低到0.005左右;如果发现loss震荡,再继续减半。训练过程中观察命令行输出的box、obj、cls三个损失。对人脸检测,cls损失会快速降到接近0,因为只有一个类;这时你就知道模型已经分清了“脸/非脸”,剩下就看box和obj是否还在波动。
3.4 用tensorboard监控训练:tfevents不是摆设
资源包里的events.out.tfevents.1624361803.kele.11844.0就是训练过程保存的日志文件。YOLOv5用PyTorch的torch.utils.tensorboard写tensorboard事件,查看方式:
tensorboard --logdir runs/train浏览器打开http://localhost:6006,能看到train/box_loss、train/obj_loss、val/mAP_0.5等曲线。注意事件文件名里的1624361803是Unix时间戳,对应2021年6月;kele是机器名。这个信息在判断训练日志归属、对齐代码版本时很有用——很多复现失败是因为拿另一个时间点训练的日志去对比当前权重。
4. 推理、评估与人脸计数:把检测结果变成人数
4.1 使用detect.py输出检测框和置信度
训练完成后,对单张测试图推理最简单的是用官方detect.py:
python detect.py --source "../test" --weights runs/train/exp/weights/best.pt \ --conf-thres 0.5 --iou-thres 0.45 --save-txt --save-conf--save-txt会把每个检测框写入runs/detect/exp/labels/000 (7).txt,内容格式是class conf x_center y_center w h;--save-conf让置信度一并保存。--conf-thres表示置信度阈值。人脸检测建议设在0.4~0.6之间,不要用默认0.25;因为人脸误检率在低置信度区间很高,头发、手掌、水杯都容易触发。--iou-thres是NMS的IoU阈值,0.45是常见值,越小人脸重合越高时越容易只保留一个框,适合拥挤场景。
4.2 统计单张图片人脸数:读取txt逐行计数
一个快速计数脚本,直接复用detect.py生成的txt:
for f in runs/detect/exp/labels/*.txt; do echo "$(basename "$f") $(wc -l < "$f")" donewc -l < "$f"统计行数,每行一个人脸框,行数就是该图的人脸数。如果用Python实时输出,可以这样:
import torch import cv2 model = torch.hub.load('ultralytics/yolov5', 'custom', path='best.pt') img = cv2.imread('test/000 (7).bmp')[..., ::-1] results = model(img, size=640) boxes = results.pandas().xyxy[0] count = len(boxes[boxes['confidence'] > 0.5]) print('face count:', count)results.pandas().xyxy[0]是DataFrame,包含xmin,ymin,xmax,ymax,confidence,class,name。这里我们用>0.5过滤低置信度框,与上面的--conf-thres语义一致。如果想要可视化,可以把坐标取整后传给cv2.rectangle画框。
4.3 视频或连续帧计数:先做帧间去重
如果统计视频,逐帧把框数加起来没有意义,同一个人会被重复计数。常见做法是用中心点最近邻做帧间匹配,给每个检测框一个临时ID。下面是一个简化版本:
import numpy as np last_centers = [] track_id = 0 max_id = 0 for frame in video_frames: centers = get_face_centers(frame) # 内部调模型,返回所有框中心点 if not last_centers: last_centers = centers continue for c in centers: if last_centers: dists = np.linalg.norm(np.array(last_centers) - c, axis=1) j = np.argmin(dists) if dists[j] < 30: # 像素阈值,按实际分辨率调整 track_id = j else: track_id += 1 max_id = max(max_id, track_id) last_centers = centers这段逻辑很粗糙,只做了一帧的最近邻匹配,没有处理消失和重现,所以更适合“走廊人流量统计”这类短时场景。严谨的做法是接入DeepSORT或ByteTrack,但复现这个项目时能解释清楚“为什么不能直接累加框数”,已经比照抄代码的人强。真正的工程实现还需要卡尔曼滤波预测轨迹,这里不展开。
4.4 验证模型精度:val.py与结果指标
训练过程中跑验证集看mAP是常规操作,但很多人在人脸任务上纠结于mAP@0.5:0.95的数字偏低。实际上人脸检测常用mAP@0.5作为主要指标,因为密集人脸区域存在大量相近框,IoU稍微一高就被判负例,mAP@0.5:0.95数值天然难看。
python val.py --data face.yaml --weights runs/train/exp/weights/best.pt --task val结束后在runs/val/exp/里能看到results.csv,竖向是类别,中间几列是precision、recall、mAP@0.5、mAP@0.5:0.95。评判一个模型是否可用,先看recall:人脸计数最怕漏检,recall低于0.85,后面提升confthres会损失大量真脸;再看precision:高误检会导致把背景人数算多。两个指标要结合你的计数场景——人流密度统计更看重recall,安防抓拍更看重precision。
5. 工程落地:实时摄像头计数与小脸加速技巧
5.1 摄像头实时人脸计数
把detect.py的--source改成0就能跑摄像头,但实际延迟很高。更快的方案是强制半精度推理,并降低输入尺寸:
python detect.py --source 0 --weights best.pt --img 416 --half --conf 0.5输入降到416后速度能提升约40%,但人脸框的最小可检尺寸也会下降。如果摄像头安装位置较高、人脸普遍小于30像素,还是建议保留640,不要为了帧率牺牲小脸检出。
5.2 用切片推理处理超小小脸
摄像头如果是1080p,人脸区域可能只有15x15像素。YOLOv5在640输入下会把原图等比缩放,小人脸在特征图上只剩1~2个像素,难以产生有效响应。常见做法是对原图做切片推理:把图像分割成若干重叠块,每块独立推理,再拼接结果。Sahi库就是专门做这个的,命令类似:
sahi predict --source test/ --model_path best.pt --model_type yolov5 \ --slice_height 320 --slice_width 320 --overlap_height_ratio 0.2切片尺寸设成320可以保留更多细节,重叠率20%是为了让边缘人脸至少有一半完整出现在某一块中。合并检测结果时注意去重——同一张脸可能被两个切片都检到,NMS会在最终合并时处理掉,但不同切片的置信度尺度略有差异,所以--conf可以压到0.3再合并。
5.3 监控loss稳定后提前停训
人脸单类任务收敛很快,不一定需要硬跑100轮。当val/obj_loss连续10个epoch不再下降,可以直接停掉,然后复制best.pt去推理。用脚本自动判断比较省事:
import csv with open('runs/train/exp/results.csv') as f: rows = list(csv.reader(f)) val_losses = [float(r[4]) for r in rows[1:]] # 假设第5列是val/obj_loss if len(val_losses) > 10 and val_losses[-1] >= val_losses[-11]: print("early stop: val/obj_loss no longer decreasing")这是一个很基础的解析。实际应该取当前和10个epoch前的差值,而不是简单比较大小。重点是:人脸检测的loss曲线里,cls先降、obj随后降、box最后降,如果你看到box还在缓慢下降但val mAP已经不动,说明模型容量不够,加数据比加epoch管用。复现这套项目时,建议先拿yolov5s.pt做一次完整流程,确认每一个环节的输出都正常后,再换上你自己的场景数据,把--img调成你的摄像头分辨率,重新训练一轮——这才是这套代码真正开始干活的时候。
本文还有配套的精品资源,点击获取