简介:这是一份面向计算机视觉初学者与深度学习实践者的社交距离检测项目资源,基于YOLOv3目标检测模型实现人群间距实时分析,适用于疫情防控、智慧安防等实际场景。资源包共13个文件,包含3个核心Python脚本(含主检测逻辑、配置管理与工具函数)、YOLO-COCO模型所需cfg与names配置文件、预置测试视频pedestrians.mp4、GIF效果演示、HTML说明页及环境依赖清单,整体压缩包仅21.17MB,轻量易部署。已有50人学习下载,适合希望快速上手目标检测落地应用的开发者。资源附带完整项目结构说明与可复现的CPU运行流程,涵盖虚拟环境搭建、依赖安装、权重加载指引及命令行参数详解,并提供YouTube配套讲解视频链接,便于理解算法原理与代码组织逻辑。
1. 项目整体设计与思路拆解
1.1 为什么选择YOLO来做社交距离检测
提到社交距离检测,很多人第一反应是“这不就是两个人之间画条线量距离吗?”。但真正落地过类似项目的人都知道,难点从来不在“量距离”,而在“怎么知道哪里有人”。监控画面是二维图像,人可能站着、坐着、走动、重叠,光线时好时坏,摄像头角度千奇百怪。如果没有一个稳定可靠的人体检测层,距离计算就是空中楼阁。
YOLO系列在这个场景里几乎是首选。原因很直接:第一,它把检测做成了单阶段回归问题,速度足够快,能在实时视频流上跑,而不是先出候选框再做二次分类;第二,YOLO在COCO数据集上预训练好的权重可以直接拿来做迁移学习,自己手里那点标注数据不需要太多,也能微调出不错的精度;第三,社区生态成熟,PyTorch、Ultralytics等框架封装得很友好,几行代码就能加载模型,像我这种不想从零手写卷积网络的人,也能快速把方案跑通。
有人会问,为什么不用Faster R-CNN或者SSD?Faster R-CNN精度确实高,但推理速度在CPU或者普通GPU上很难满足实时分析多路视频的需求;SSD对小目标不友好,而监控画面里人往往只占几十个像素,容易漏检。YOLO在当前硬件条件下,是精度和速度平衡得最好的选项,尤其是YOLOv5、YOLOv8这些版本,工程化程度很高。
1.2 核心流程与技术选型
整个社交距离检测项目,本质上是一条流水线:视频帧 -> 人体检测 -> 获取检测框坐标 -> 计算检测框底边中心点(或脚点)-> 计算两两欧氏距离 -> 根据像素距离阈值判断是否小于安全距离 -> 可视化标红告警。简单画个逻辑链,比想象中要清晰得多。
这里面有几个关键决策点值得展开说:
- 用检测框的哪个点作为“人的位置”。如果直接取框的中心点,站起来的人和蹲下的人中心点高度差异很大,容易导致距离误判。更稳妥的做法是用检测框底边的中点,近似代表脚部位置。因为人在图像中的平面位置主要靠脚点来确定,这样计算出的距离更贴近地面的真实投影距离。
- 距离用什么单位。像素距离不等于真实距离。不同摄像头分辨率、焦距、安装高度都会影响同样的“1米”在画面里占多少像素。所以需要一个换算系数,把像素距离映射成实际距离(如米)。这个系数可以通过标定获得,或者直接假设一个近似值(例如某些场景下1米≈70像素),先跑通流程再优化精度。
- 阈值怎么定。如果安全距离设定为2米,那在像素层面就要把2米对应的像素数算出来,再遍历所有检测目标对,只要是距离小于该像素阈值且人不是同一人的,就标记为“近距离接触”。
在技术选型上,我采用了YOLOv8作为检测器,后处理用NumPy做距离计算,可视化用OpenCV画框和连线。整体代码量不大,核心逻辑大概两百行以内,但工程上要考虑的东西比算法本身多得多,比如视频流断线重连、模型推理时间统计、告警帧去重等,这些在后面实操部分详细讲。
2. 核心细节解析与实操要点
2.1 人体检测模型的选择与处理
YOLO家族迭代到现在,各个版本的差异对于普通工程应用来说并没有想象中那么大。我做这个项目时首先试了YOLOv8n,参数量最小、推理最快,但小目标人体检测率在远距离画面里表现一般;后来换成YOLOv8s和YOLOv8m,精度明显提升,FPS从100多降到40左右,但依然够用。如果你有GPU,推荐用YOLOv8s起步,速度和精度的甜点区;如果部署在树莓派或Jetson NX这类边缘设备上,YOLOv8n可能更现实。
一个特别容易踩的坑:直接用COCO预训练权重时,检测类别索引是0代表人,但如果你用的是自定义训练版本或者某些剪裁过的权重,类别顺序可能变化。代码里必须显式过滤出class_id为0的结果,否则你可能会把“人”和“自行车”都框进去,因为人群中有时候骑自行车、推婴儿车,这些目标会被识别为其他类别。
我在处理模型输出时,会做三步过滤:置信度阈值过滤(一般设0.4-0.5,太低会有一堆误检框)、类别过滤(只保留person)、非极大值抑制(YOLO输出里通常已经做了NMS,但如果你在批量推理时手动拼接结果,就需要再做一次)。只有把框的质量提上来,后续距离计算才有意义。
2.2 距离计算与阈值判定逻辑
这一步是整个项目的核心算法部分,重点在于“到底怎么算距离才是对的”。
先说理论公式。假设一张图像中,第i个人的脚点坐标为(xi, yi),第j个人的脚点为(xj, yj)。像素欧氏距离:
d_pixel = sqrt((xi - xj)² + (yi - yj)²)
然后需要把d_pixel换算成实际距离。最理想的办法是摄像头的单应性变换——通过标定地面平面,得到像素坐标到世界坐标的映射矩阵。但这对大多数快速原型项目来说过于复杂,所以更实用的折衷策略是:
已知摄像头的安装高度H(米)和俯仰角θ(度),根据镜头焦距和传感器参数,可以推导出图像中每个像素对应的地面距离。但一般人不一定拿得到这些参数,所以简单粗暴的方案是人工标定:在画面中找一段已知长度的地面线段(比如地砖是60cm,数出10块砖就是6米),在图像上量出对应的像素长度,算出每米对应的像素数。由于透视关系,近处和远处的比例尺不同,所以最稳妥做法是在画面不同纵深位置分别标定几个比例尺,然后对每个目标对按其中点所在深度区域,选择对应的比例系数。这种做法能达到80%以上的准确率,对于预警类应用已经够用。
阈值设定直接用“最小安全距离”,比如2米。假设当前深度区域中,2米对应100像素,那么当两个脚点像素距离小于100像素时,判定为违规接触。
这里还有一个非常容易忽略的问题:两个检测框属于同一个人的相邻帧,或者同一个人因为遮挡被拆成两个框后,距离计算会把自己和自己算成一对。为了避免这种闹剧,我在代码里增加了目标跟踪ID(简单的IOU追踪即可),只对不同的track ID计算距离,并且当两个坐标完全重合时跳过计算。
2.3 数据与标注说明
如果你准备在自己的数据集上微调模型,而不是直接用预训练权重,那么数据这块的建议是:不要自己去标几千张图,先找开源数据集,比如COCO的person类、CrowdHuman、MOT数据集等。社交距离检测通常不需要非常精细的分割,只需要人体框,所以标注相对简单,但要注意边界情况。
我实际使用中遇到的问题是,公开数据集里大多是站立或行走的人,对于蹲着、坐着、趴着的人,模型漏检率会增高。这种情况下,我会从自己摄像头采集几百张截图,用LabelImg标注人物框,加入训练集进行微调。每次采集几十张就够,不要贪多,因为模型权重已经很强,微调只是让它适应你的场景角度和尺度分布。
值得提醒的是,标注框的紧致程度会影响脚点位置估算。如果标注框包含了太多头顶以上背景,底边中点会偏高,导致脚点位置偏差。所以标注时要让框尽量贴合人身体,尤其是底部要精确贴合脚下地面。
3. 实操过程与核心环节实现
3.1 环境搭建与依赖安装
这个项目我用的环境是Python 3.10 + PyTorch 2.0 + CUDA 11.8,GPU是RTX 3060。如果是纯测试,CPU也能跑,只是速度只有每帧约2-3秒,满足不了实时性要求。安装依赖时,建议直接用Ultralytics的YOLO包,一步到位:
pip install ultralytics opencv-python numpy需要说明的是,Ultralytics会默认拉取torch,如果你的环境里已经有特定版本的torch,建议先手动安装torch再用pip install ultralytics --no-deps避免依赖冲突。我在实际部署到云服务器时,就因为torch和CUDA版本对不上,折腾了半天,后面干脆换成CPU推理先验证逻辑,再把模型搬到GPU推理。
3.2 关键代码实现:检测、映射、距离计算
核心代码分为三个模块:视频读取、推理检测、距离计算与可视化。我先放一个精简版的完整实现,后面再逐段拆解。
import cv2 import numpy as np from ultralytics import YOLO # 加载模型 model = YOLO("yolov8s.pt") # 参数配置 SAFE_DISTANCE_METER = 2.0 # 假设当前场景每米对应像素数,实际需要通过标定获得 PIXELS_PER_METER = 60 safe_distance_pixel = SAFE_DISTANCE_METER * PIXELS_PER_METER CONF_THRESHOLD = 0.5 # 视频读取 cap = cv2.VideoCapture("test.mp4") while cap.isOpened(): success, frame = cap.read() if not success: break results = model(frame, verbose=False)[0] boxes = results.boxes persons = [] for box in boxes: if int(box.cls) != 0: # 只保留person类别 continue conf = float(box.conf) if conf < CONF_THRESHOLD: continue x1, y1, x2, y2 = map(int, box.xyxy[0]) persons.append((x1, y1, x2, y2)) # 计算脚点坐标 foot_points = [] for (x1, y1, x2, y2) in persons: foot_x = (x1 + x2) // 2 foot_y = y2 foot_points.append((foot_x, foot_y, (x1, y1, x2, y2))) # 两两计算距离 violations = set() for i in range(len(foot_points)): for j in range(i + 1, len(foot_points)): fx1, fy1, box1 = foot_points[i] fx2, fy2, box2 = foot_points[j] dist_pixel = np.sqrt((fx1 - fx2) ** 2 + (fy1 - fy2) ** 2) if dist_pixel < safe_distance_pixel: violations.add(i) violations.add(j) # 可视化 for idx, (x1, y1, x2, y2) in enumerate(persons): color = (0, 0, 255) if idx in violations else (0, 255, 0) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) label = f"person {idx}" if idx in violations: label = "violation!" cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) cv2.imshow("Social Distancing", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()这段代码能跑通,但距离映射很粗糙,直接用一个固定的PIXELS_PER_METER。真实场景中,因为透视效应,画面近处的人一米占200像素,远处一米可能只占30像素,固定比例尺会导致近处过度告警、远处漏报。我在实验中被这个问题坑了很久,后来采用了一个简单的分区标定策略:在图像上预定义三个横向“深度带”,每个带对应不同的像素米比。
# 假设画面从上往下分别是远、中、近三个区域 ZONES = [ {"y_start": 0, "y_end": 300, "ppm": 30}, # 远区域 {"y_start": 300, "y_end": 500, "ppm": 60}, # 中区域 {"y_start": 500, "y_end": 720, "ppm": 100} # 近区域 ] def get_ppm_by_y(y): for zone in ZONES: if zone["y_start"] <= y < zone["y_end"]: return zone["ppm"] return ZONES[-1]["ppm"] # 在距离计算中,采用两个脚点的平均y坐标来确定区域,取ppm avg_y = (fy1 + fy2) // 2 ppm = get_ppm_by_y(avg_y) safe_distance_pixel = SAFE_DISTANCE_METER * ppm if dist_pixel < safe_distance_pixel: # 判违规这样做比固定比例要合理很多,因为两个人同处一个区域时,比例尺一致;跨区域时用平均位置,误差也在可接受范围内。
3.3 可视化与告警提示
除了画框,一个可用的项目还需要告警。最简单的是在画面左上角显示一个“当前违规对数”的计数器,同时在违规人数大于0时输出一条日志。如果接到实际业务里,可以用log日志记录时间戳和检测结果,或者通过HTTP POST把违规人数推到后端。
我在这块的经验是把“检测”和“展示”分离。检测线程只负责输出结果列表(bbox坐标、违规标记),展示线程负责渲染。这样当视频流卡顿或断线时,检测逻辑不会直接崩溃。下面是简单的线程化示意:
import threading class SocialDistDetector: def __init__(self, video_source): self.cap = cv2.VideoCapture(video_source) self.frame = None self.result_frame = None self.running = True def detect_loop(self): while self.running and self.cap.isOpened(): ret, self.frame = self.cap.read() if not ret: continue # 在这里执行检测和距离计算 self.result_frame = processed_frame当然,这只是工程优化,初学者可以先跑通单线程代码,再迭代升级。
4. 常见问题与排查技巧实录
4.1 检测精度与误报问题
问题一:人体检测框时有时无,尤其是穿着深色衣服的人在低照度场景下。这个问题我遇到过两次,排查后发现一个原因是视频帧直接缩放到了640x640,原始小目标信息丢失严重。另一原因是置信度阈值设太高。建议把输入图像分辨率调大到1280(Ultralytics支持imgsz参数),同时降低置信度阈值到0.35,再通过检测框尺寸过滤掉过小的噪点。
还有一个容易被忽视的点:YOLO的锚框对“人坐在椅子上,身体被桌子遮挡”这种情况容易产生半身框或三个框。解决方法是给模型训练时加入更多部分遮挡样本,或者在后处理中合并重叠度高的框,保留置信度最高的那个。
4.2 距离映射误差问题
很多人问我,为什么明明两个人实际离得很远,系统却报警。大多数是映射比例尺没标定好,或者使用了固定比例尺导致远处误报。一个典型的例子:画面中左侧靠近摄像头,右侧在远处,如果统一用近处比例尺,那么远处一对真实相距3米的人,因为像素距离小,会判定为安全;相反,用远处比例尺,近处真实距离1.5米的两人会被判定为安全,而事实上可能已经违规。因此,做分区标定是必须的。
如果还想更精细,可以标定多个已知坐标点的地面坐标,然后用cv2.findHomography计算单应矩阵,将脚点像素坐标投射到实际平面坐标,再算距离。这样精度最高,我在另一个工业项目中就是这么做的,误差能控制在5%以内。
4.3 性能优化建议
在Jetson Nano或CPU上跑实时检测时,经常遇到FPS只有个位数。我的优化顺序是:
- 改用更小的模型(yolov8n),推理速度提升2-3倍。
- 将视频帧缩放处理后再送入模型,而不是直接用原始分辨率,比如1080p降到768或640。
- 检测帧率与画面刷新率分离,比如每秒只检测5帧,中间帧沿用上一帧检测结果,但画面上依然流畅。
- 使用ONNX Runtime或TensorRT导出模型,推理时间能再减半。
我用TensorRT导出YOLOv8s后,在RTX 3060上每帧推理时间从15ms降到了6ms,四路视频同时处理时总体FPS依然超过30。如果你的部署环境固定,强烈建议做这一步。
另外还有一个隐蔽的性能问题:如果用OpenCV的waitKey(1)做循环,CPU占用率往往会很高。可以加上cv2.waitKey(30),让每帧间隔30ms,兼顾流畅度和资源占用。
5. 写在最后的个人体会
从确定选题到跑通整个社交距离检测流程,我只花了两天时间,但把精度调到让人满意的程度,前前后后用了好几个版本。最深刻的教训是:算法模型只是整个项目的一道前菜,真正决定项目能否落地的是对实际场景的理解——摄像头装多高、地面有多少遮挡、人流量峰值时怎么保证帧率,这些问题比单纯的loss值更重要。
这个项目后续可以延展的方向非常多,比如把检测框换成跟踪算法(ByteTrack、DeepSORT)做多人轨迹记录,统计一段时间内近距离接触的时常;或者接入人体关键点检测,判断接触时是否发生碰撞;再或者直接把告警信息推送到微信/钉钉机器人,做成一套完整的实时防疫预警系统。核心的YOLO检测和距离计算逻辑不变,扩展起来非常方便。
如果你想拿这个项目练手,建议先用自己的摄像头录一段半分钟的办公室或走廊视频,按我上面的步骤把demo跑起来,然后尝试调整分区标定参数,观察告警效果的变化。这种动手调试的过程,比看十篇技术博客都有用。
本文还有配套的精品资源,点击获取