简介:基于深度学习的工地危险区域入侵检测监控告警系统,面向智慧工地场景下的安全管理人员、计算机相关专业学生及开发者。项目融合YOLOv5目标检测框架,能够识别搅拌车、吊车等机械与作业工人,支持自定义危险区域绘制,一旦检测到人员闯入即刻触发画面告警与语音提醒,适用于毕设、课设、实训及项目预研演示。压缩包共92个文件,主要包括Python源码、pt训练权重、yaml模型与配置、jpg/png测试图像及mp4演示视频,整体约84.3MB,部署简单,可处理本地图片、视频及网络视频流,运行稳定可靠。当前已有134人学习使用,代码经验证可完整运行;除主程序外,还附带了数据集配置、测试图表与推理结果示例,便于二次开发与算法效果评估,是快速落地工地安全监控方案的高性价比参考资料。
1. 工地监控画面里,最缺的不是摄像头,是识别动作的人
你在工地安全管理系统里接入二十路摄像头,值班员盯不了两小时就会走神。这是智慧工地落地时最现实的问题——采集数据的设备早就齐全了,缺的是能把危险事件自动挑出来的那一层软件。这个基于深度学习的工地危险区域入侵检测监控告警系统,把 YOLOv5s 作为感知核心,用训练好的模型识别搅拌车、吊车、作业工人,再用一套自定义的入侵判定逻辑,判断工人是否进入预先绘制的危险区域。随着检测结果实时叠加告警框,系统同时通过语音播报提醒现场人员撤离。源码由完整的 YOLOv5 3.0 项目结构和二次开发的检测脚本构成,带权重、带检测示例视频,解压后首先跑detect.py就能验证模型效果,随后把危险区域标定逻辑接进去就能形成一套可演示的告警闭环。下面按模型检测、区域判定、告警联动、模型替换四层依次展开。
2. YOLOv5s 检测管线:用一条命令把工人和机械从画面里抠出来
2.1 模型选型为什么是 YOLOv5s 而不是更大的 v5l/v5x
压缩包里的weights/yolov5s.pt是 YOLOv5 系列里体积和速度最均衡的一个版本。工地监控场景有一个容易被忽略的要求:并不是每帧都要做高精度检测,而是要在低成本设备上长时间连续运行。yolov5s 的模型体积小,CPU 上也能跑到可用级别,换到 NVIDIA 显卡后帧率可以再翻几倍,这是 yolov5l 和 yolov5x 做不到的硬件适应性。
models目录下的yolov5s.yaml定义了网络结构,它与其他版本的主要差异在 depth_multiple 和 width_multiple 两个缩放系数。官方开源时保留了一整套可切换的配置,但实际部署时我倾向于固定用 s 版本,理由是:工地场景的检测目标大小跨度很大,远处吊车可能占据上百像素,安全帽区域可能只有二十个像素,yolov5s 的 PANet 颈部结构已经能处理这种尺度变化,继续加网络深度对召回率提升有限,却会直接拉高推理延迟。
2.2 detect.py 参数与图片、视频、RTSP 三种输入
项目根目录的detect.py是推理入口,参数设计与原生 YOLOv5 保持一致。先用项目自带的测试图片跑通流程:
python detect.py --source inference/images/00058.jpg --weights weights/yolov5s.pt --conf-thres 0.4 --img-size 640--source支持图片路径、视频文件路径、目录路径,也支持rtsp://开头的网络视频流地址;--conf-thres是置信度阈值,用于过滤置信度低的检测框;--img-size指定推理时统一缩放的边长,默认 640。执行完成后结果输出到runs/detect/目录,检测到的目标会以矩形框和类别标签形式叠加在输出图上。
对于视频检测,直接用压缩包里的sp1.mp4验证:
python detect.py --source sp1.mp4 --weights weights/yolov5s.pt --conf-thres 0.4 --img-size 640 --view-img--view-img会在桌面窗口实时弹窗显示检测结果,适合快速验证模型效果。若需要把结果保存成视频文件,原生逻辑会自动基于输入文件名生成带标注的输出视频。接入现场网络摄像头时,把--source替换成rtsp://用户名:密码@IP地址:端口/Streaming/Channels/101即可,常见做法是先在海康、大华的 SDK 里确认 RTSP 主码流地址格式,主码流分辨率高但带宽占用大,工地场景建议同时准备好子码流地址用于巡检,有了目标后再切换主码流做精细识别。
下面是本项目detect.py常用的参数说明表:
| 参数 | 示例值 | 作用 |
|---|---|---|
--source | inference/images/00058.jpg | 输入源:图片、视频、目录、RTSP |
--weights | weights/yolov5s.pt | 模型权重路径 |
--conf-thres | 0.4 | 置信度阈值,调低可检出更多目标但误报增加 |
--iou-thres | 0.45 | NMS 交并比阈值 |
--img-size | 640 | 推理尺寸,大尺寸提升小目标召回 |
--classes | 0 7 | 只保留指定类别索引 |
--save-txt | 无 | 同时导出检测框坐标到 txt 文件 |
2.3 类别过滤:只保留 person 与工程机械
COCO 预训练权重能检测 80 类目标,但工地场景只需要其中一部分。直接对所有 80 类做告警判定会产生大量无关框,比如画面里出现一辆普通轿车也会被标记。项目在二次开发时按业务需要做类别白名单过滤,--classes 0 7表示只保留 person(索引 0)和 truck(索引 7),吊车、搅拌车这类工程机械在 COCO 预训练模型下大概率会被识别为 truck 或 bus,因此先用这两个类别把流程跑通,再基于自有工地数据做微调。若更换项目中runs/exp240/weights/best.pt这类自训练权重,类别索引与data目录下的 yaml 配置对应,先用--save-txt跑一遍,查看 txt 文件中类别编号,避免硬编码索引导致的漏检。
3. 危险区域自定义:从背景提取到多边形命中判定
3.1 提取背景.py:把移动的人和机械从场景里拿掉
危险区域的标定不能在地面真实拉线或摆放围栏,否则就失去了视频监控的意义。项目里的提取背景.py解决了区域标定时的干扰问题:工地现场人员、机械始终在运动,直接截取某一帧画区域,人和车的轮廓会遮挡地面参考物,而标定区域需要的是干净的道路、脚手架、基坑边缘线。
这个脚本的常见实现逻辑是:固定摄像头不动,采集连续 N 帧图像,在相同像素位置取中值作为该点像素值。运动目标在大部分帧里都不占据同一位置,取中值后运动目标自然被抹掉,剩下的就是纯背景。关键参数是采样帧数,我一般取 50 到 100 帧,帧数太少背景中会残留运动目标的半透明拖影,帧数太多则标定前等待时间长。
背景图生成后,用 OpenCV 的鼠标回调事件在背景图上逐个点击多边形顶点,生成危险区域坐标文件:
import cv2 import json points = [] def on_mouse(event, x, y, flags, param): if event == cv2.EVENT_LBUTTONDOWN: points.append((x, y)) print(f"已添加顶点: ({x}, {y}), 当前顶点数: {len(points)}") bg = cv2.imread("background.png") cv2.namedWindow("mark_zone") cv2.setMouseCallback("mark_zone", on_mouse) while True: img = bg.copy() if len(points) >= 3: cv2.polylines(img, [np.array(points)], True, (0, 0, 255), 2) cv2.imshow("mark_zone", img) key = cv2.waitKey(1) & 0xFF if key == ord("s"): # 按 s 保存 with open("danger_zone.json", "w") as f: json.dump({"polygon": points}, f) print("危险区域已保存到 danger_zone.json") break elif key == ord("r"): # 按 r 撤销上一个点 if points: points.pop()这段代码将多边形顶点以像素坐标形式保存到danger_zone.json。逻辑说明:鼠标点击收集顶点坐标,cv2.polylines实时预览闭合多边形,按s落盘,按r回退误点的顶点。实际使用时要注意一个细节——背景图尺寸必须与后续视频推理的原始帧尺寸一致,如果摄像头分辨率是 1920x1080,就应在该尺寸的背景图上标定,否则区域坐标与检测帧坐标对不上。
3.2 用射线法判定工人是否踩进危险区域
获得多边形区域后,下一层核心是判断行人检测框是否落在区域内。直接拿整个检测框做「框与多边形是否相交」会导致大量误报:工人站在区域边上,检测框很大,只有手臂伸入也会被判定入侵。
正确的做法是取检测框底部的中点作为人的立足点。检测框的左上角和右下角坐标(x1, y1, x2, y2)来自 YOLO 输出,底边中点坐标为((x1 + x2) / 2, y2),它对应人的脚部位置。用射线法判断这个点是否在多边形内部:
def point_in_polygon(check_point, polygon): x, y = check_point n = len(polygon) inside = False p1x, p1y = polygon[0] for i in range(1, n + 1): p2x, p2y = polygon[i % n] if (y > min(p1y, p2y)) and (y <= max(p1y, p2y)): if p1y != p2y: xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x if p1x == p2x or x <= xinters: inside = not inside p1x, p1y = p2x, p2y return inside射线法逻辑说明:从待判定点向右水平作射线,统计射线与多边形边的交点个数,交点数为奇数则该点在多边形内,偶数则在多边形外。这个过程对任意凸多边形和凹多边形都有效。注意代码中有两个边界处理,一是y > min(p1y, p2y)与y <= max(p1y, p2y)保证顶点只被计数一次,二是p1x == p2x处理垂直边段的除零问题。check_point传人的立足点坐标,polygon传从danger_zone.json读出的顶点列表。
3.3 坐标对齐:为什么判定要在原始帧做而不是在 640 推理图上做
原生detect.py会把输入帧缩放到 640x640 再送入模型,检测框坐标在 640 的分辨率坐标系下。如果直接把危险区域多边形也按比例缩到 640,表面看可行,实际会引入一个隐蔽问题:YOLOv5 的letterbox缩放会保持宽高比并在两侧填充灰边,不是简单的等比拉伸,坐标换算稍不注意就会整体偏移。
项目开发时的经验是:危险区域坐标始终固定在原始视频分辨率坐标系下,检测框从模型输出后先换算回原始坐标再判定。换算公式为,假设模型输出框为(nx1, ny1, nx2, ny2),原图宽W高H,则x1 = nx1 * W / 640,这个换算在letterbox填充场景下会有一小段偏移,严谨做法是先读取detect.py中letterbox算子生成的仿射参数再做逆映射。如果只是快速验证,直接按比例换算在大多数画面中也能用,但精度要求高的姿态下应改造 Detect.py 在inference阶段就完成坐标反算。
决策逻辑采用如下规则表:
| 检测目标 | 位置状态 | 告警动作 |
|---|---|---|
| 工人(person) | 立足点在危险区域内 | 立即告警 |
| 工人(person) | 立足点在区域外 | 不告警 |
| 机械(truck/bus) | 进入危险区域 | 不告警 |
| 无目标 | 任意 | 不告警 |
机械不告警是业务规则上的取舍——搅拌车、吊车本身就是作业设备,它们的正常工作范围往往就在红线区域内部,真正的风险是工人误入机械作业区,因此只对 person 类目标做入侵判定。
4. 入侵告警与语音提醒:把检测结果变成现场能听见的声音
4.1 告警状态机:连续 N 帧命中才拉响,避免单帧误报
模型检测天然存在抖动的可能,某一帧把远处戴安全帽的工人误识别成人员且其立足点刚好落在区域边缘,如果单帧命中就告警,值班室一天会响几十次假警。所以在检测判定之上需要加一层状态机,项目实践中的设计是三态:安全态、确认态、告警态。安全态下每帧执行判定,命中一次就进入确认态;确认态持续统计后续帧是否连续命中,若连续命中超过设定值CONFIRM_FRAMES才转入告警态;告警态触发语音和截图,之后进入冷却时间,冷却期间即使区域内有工人也不再重复告警,防止语音循环轰炸。
class InvasionDetector: def __init__(self, confirm_frames=5, cooldown_seconds=10): self.confirm_frames = confirm_frames self.cooldown_seconds = cooldown_seconds self.hit_count = 0 self.state = "SAFE" self.last_alarm_time = 0 def update(self, invaded: bool, current_time: float): if self.state == "ALARM": if current_time - self.last_alarm_time > self.cooldown_seconds: self.state = "SAFE" return self.state if invaded: self.hit_count += 1 if self.hit_count >= self.confirm_frames: self.state = "ALARM" self.last_alarm_time = current_time self.hit_count = 0 else: self.hit_count = 0 self.state = "SAFE" return self.state代码逻辑说明:invaded是上一节中point_in_polygon的判定结果,current_time用time.time()传入。连续命中confirm_frames帧后才拉响告警,告警后进入ALARM状态并开启冷却计时,冷却结束自动回到安全态。参数权衡:confirm_frames设为 3 到 5 较为合适——相机帧率 25fps 时约 0.2 秒确认延迟,人耳基本无感;设为 15 或 20 虽然更稳,但快速跑过区域边缘的行人可能还没触发确认就离开了。
4.2 语音播报的实现与线程隔离
语音提醒是监控告警系统的关键动作之一。Python 环境下离线语音合成最常见的选择是pyttsx3,它调用系统自带的语音引擎,不需要网络请求,工地内网环境下也能工作。
import threading import pyttsx3 def speak_alert(message="有工人进入危险区域,请立即撤离"): engine = pyttsx3.init() engine.say(message) engine.runAndWait() def on_alarm_detected(): thread = threading.Thread(target=speak_alert, daemon=True) thread.start()pyttsx3.init()会初始化系统语音引擎,runAndWait()是阻塞操作,单次播报可能耗时 1 到 3 秒。如果直接在视频检测主循环里调用,帧率会瞬间降为零。用threading将语音播报放到独立线程中执行,检测主循环不受影响。注意每个线程内都要重新init(),避免跨线程复用引擎实例带来的崩溃。如果机器上没有可用的中文语音包,engine.setProperty('voice', 'zh')可以切换语言,但需要系统先安装对应语音库;say()的文本内容也可以与告警状态联动,比如播报「检测到工人进入 1 号危险区域」,这类信息从配置文件中读取。
pyttsx3依赖系统语音引擎,在精简版 Windows Server 或某些 Linux 容器里可能没有预装。常见的替代方案是调用espeak或edge-tts。edge-tts需要联网访问微软语音服务,工地网络环境受限时不推荐;espeak音质一般但稳,适合作为备选。还有一个更简单的思路是用.wav提示音文件,在告警触发时通过playsound播放,音质可控且零依赖。
4.3 告警截图留档与 RTSP 巡检循环
告警信息除了语音提醒,还需要留下一份可追溯的证据。在检测循环中,当状态机输出ALARM时,将当前帧连同检测框、危险区域多边形一起写入本地磁盘:
import cv2 from datetime import datetime def save_alert_frame(frame, detections, polygon, save_dir="alerts"): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") for *box, conf, cls in detections: x1, y1, x2, y2 = map(int, box) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.polylines(frame, [polygon], True, (0, 255, 255), 2) cv2.imwrite(f"{save_dir}/{timestamp}.jpg", frame)这段代码将检测框绘制为红色、危险区域边界绘制为黄色,文件名精确到微秒,避免同一秒多次告警时文件名覆盖。写入磁盘的 I/O 会阻塞主循环,因此文件写入也应该放到子线程,或者使用cv2.imencode先编码成内存缓冲再异步写盘。
RTSP 流接入的场景下,OpenCV 的VideoCapture读流失败是个高频问题。网络抖动会导致read()返回空帧,某些摄像头在断开后VideoCapture.isOpened()仍返回 True,造成虚假连接。常见做法是:每次read()失败计数累加,连续 30 帧失败就释放VideoCapture并重新初始化,初始化时在CAP_PROP_OPEN_TIMEOUT_MSEC属性上传入超时值。巡检多路摄像头时,每一路独立进程或独立线程,避免一路断流拖垮整个告警系统。
5. 换权重跑自己的工地:类别数修改与模型导出加速
初始部署用 COCO 预训练权重可以识别 person 和 truck,但要精准识别搅拌车、挖掘机、吊车,甚至细分不同颜色的安全帽,必须用工地自有数据微调。项目根目录的train.py和data/voc.yaml提供了完整训练入口,以 3.0 版本的 YOLOv5 写法为例,把 VOC 配置改成自己的数据集:
train: dataset/images/train val: dataset/images/val nc: 4 names: ['worker', 'mixer_truck', 'crane', 'excavator']nc是类别总数,names按索引排列,顺序必须与标注工具导出的标签索引一致。然后执行微调训练:
python train.py --data data/工地.yaml --cfg models/yolov5s.yaml --weights weights/yolov5s.pt --epochs 100 --batch-size 16 --img-size 640 --hyp data/hyp.scratch.yaml--weights yolov5s.pt表示在预训练权重基础上续训,收敛速度远快于从零开始;--hyp指向数据增强策略文件,hyp.scratch.yaml是官方调好的默认增强参数集。训练日志和results.png会输出到runs/train目录。观察训练结果时,重点看precision-recall_curve.png——曲线与坐标轴围成的面积大,说明模型在置信度阈值变化时表现稳定;labels_correlogram.png反映标注框的分布是否均匀,如果类别严重不平衡,需要补充少数类样本而不是加长训练时间。
训练完成后将best.pt放入weights目录,替换掉原来的yolov5s.pt,在detect.py里把类别过滤参数改成自己的类别编号即可。此时推理速度如果不够,可以用项目内models/export.py将 PyTorch 权重导出为 ONNX:
python models/export.py --weights weights/best.pt --img-size 640 --batch 1 --onnxONNX 格式可直接用 ONNX Runtime 或 TensorRT 加速推理,在 NVIDIA 显卡上通常能获得比 PyTorch 原生推理高一倍的吞吐。导出后验证输出维度是否与export.py中model.model[-1].nl的推断一致,类别数修改后如果出现维度不匹配的报错,多数是models/yolov5s.yaml的nc没同步更新。部署到边缘设备时,Jetson 系列优先跑 TensorRT,普通 PC 用 ONNX Runtime 足够,CPU 上还可以开启 OpenMP 多线程,这些优化改的是部署框架,不需要动检测核心代码。
值得额外检查的是runs/exp240/中opt.yaml保存的训练超参数组合,这个文件能看到别人的调参按什么策略走的。如果你在自己的数据集上训练结果始终不收敛,优先排查data/工地.yaml里train路径是否写成了绝对路径,以及样本图片是否统一为三通道 JPEG,这两处出错不会报异常,模型会在几十轮后输出一堆低置信度框。利用该文件检查标注类别的频次也很有参考价值:用results.png的val/obj_loss曲线能判断模型是否在学到目标本身的特征,而不是在学习训练集图片的噪声。
本文还有配套的精品资源,点击获取