ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

工地危险区域入侵检测:从YOLO检测到几何判定的完整落地指南

工地危险区域入侵检测:从YOLO检测到几何判定的完整落地指南 简介基于深度学习的工地危险区域入侵检测监控告警系统源码聚焦智慧工地场景可准确识别搅拌车、吊车等机械与作业工人并能自定义绘制危险区域一旦人员闯入系统会立即提示告警信息并发出语音提醒。项目自带预训练模型部署方便支持本地图片、视频和网络视频流检测同时包含训练、验证和推理相关代码适用于计算机、信息安全、数据科学、人工智能等专业的毕业设计、课程设计、大作业或项目演示。压缩包共92个文件、84.3MB其中以18个Python源码、23个pyc编译文件、17个YAML配置、3个pt模型权重为主另含mp4示例视频、Shell脚本和常用数据集配置目录结构清晰便于直接运行和二次开发。目前已有134人学习/下载配套的真实工地场景测试视频和背景提取脚本可帮助快速复现入侵告警效果。对部署或调试有疑问时还可与作者沟通获得支持。1. 工地危险区入侵检测最难的从来不是“检测”而是“判定”拿到“工地危险区域入侵检测”这类需求的第一反应大多是“把 YOLO 跑起来框住人就行”。但真实交付时翻车最密集的反而不是深度学习模型人是检测出来了吊臂下方的红线区域却迟迟不报警或是一个人只是路过围挡外侧就被当成入侵。原因在于这套基于深度学习的监控告警系统本质上要做两级判断——先靠模型回答“画面里有没有人”再用几何算法回答“人在不在划定区域内”。标题里的“入侵检测监控告警系统”就是把这两级串成一条可追踪的事件链路检测到入侵目标、截帧留存、触发告警。我会从区域建模、检测与判定代码、参数调优到接入真实工地的部署边界完整讲一遍我处理这类项目时的落地路径适合要做安防算法、系统集成或需要现场调视觉方案的工程师。2. 先定方案为什么这类入侵检测要拆成“检测判定”两级2.1 从需求拆分看选型目标级识别与事件级识别不是一回事工地视频监控要报警的语义是“进入危险区域”它和“画面里有人”是两个粒度的事。深度学习目标检测模型被训练来输出“类别边界框”它根本不理解你画的黄色警戒线在哪里也不理解工人蹲在吊车旁边算不算危险。所以先拆需求目标级识别用什么模型在网络里把人、车辆这类实体抓出来事件级识别目标的空间位置与预定义区域多边形做几何关系判定产生布尔结果事件工程化把这个布尔结果加上时间维度持续多久、是否离开变成告警而不是每帧都报警。模型选型上常见做法是直接用 YOLOv5s 或 YOLOv8s。工地场景算力通常不宽裕常见的运行环境是一台 i5 工控机加 GTX 1650 显卡或者 Jetson 边缘盒子轻量版 s 模型在 640 分辨率下单帧 10~30ms能留出算力给后续判定和推流。相比动用更大的检测模型把省下来的算力花在“判定逻辑正确”上对这类项目收益更直接。为什么不用传统背景建模加轮廓分析工地环境有太多动态干扰吊车臂移动、树木摇晃、运送混凝土的罐车、焊接电弧闪烁、光照和阴影变化背景差分方案在这个场景里误报率极高。深度学习方案的价值是把“什么是人”这个语义问题交给监督学习解决区域判定只做纯几何运算这样边界清晰出了误报也好排查是检测错了还是区域标错了。2.2 危险区域怎么建模多边形坐标、脚点与射线法2.2.1 用多边形而不是矩形定义区域现实里的危险区域范围不是规则的吊装作业半径近似圆形基坑边线是折线配电房门口需要留一条安全通道。所以区域不要用cv2.rectangle写死四个点而应该用多边形坐标列表存起来。区域边界越贴近真实物理边界误报越少。我一般把区域坐标写进config.yaml标定一次后不用每次改代码。region: points: - [120, 480] - [680, 460] - [720, 720] - [80, 720] label: crane_radiusREGION np.array(region[points], dtypenp.int32)坐标系的坑在分辨率。标定时如果用 1920x1080 的原始画面但推理时把帧resize到 640 再画结果区域的四个点就必须也按同样比例缩放。这一条我踩过不止一次很多人最后“测不出来”就是栽在这。把区域坐标定义与推理输入尺寸解耦要么始终用原图坐标要么统一用缩放比例还原坐标。2.2.2 判定算法脚点入域与射线法判断一个人是否侵入不建议用整个检测框和区域多边形做矩形重叠或 IoU。画面里远处的人只有十几个像素高框和区域边缘擦一下就算重叠误报会成倍增加。物理上更合理的语义是“人脚踩进了警戒线内”。取检测框底边中点作为脚点foot ((x1x2)//2, y2)用射线法判断该点是否位于多边形内部。def is_inside(poly: np.ndarray, p: tuple) - bool: x, y p n len(poly) inside False j n - 1 for i in range(n): xi, yi poly[i] xj, yj poly[j] if ((yi y) ! (yj y)) and (x (xj - xi) * (y - yi) / (yj - yi) xi): inside not inside j i return inside这段代码是标准射线法从目标点向左射一条水平线统计与多边形边的交点数量奇数为内、偶数为外。注意poly是多边形的 Nx2 数组坐标类型保持一致p是脚点。实际项目中我会把入域判断再收紧一点例如同时要求脚点入域且检测框下半部分与区域产生交集多一层缓冲来过滤“站在黄线边沿”的临界情况。这个度要按摄像头俯仰角调俯视角度大时只用脚点就很稳几乎是平视的机位则要加一个垂直方向的 margin否则人还没真正踩进区域脚点已经因为透视关系落到线内了。3. 工程实现跑通“标注区域—检测—判定—告警”最小闭环3.1 目录与依赖一个能直接改的骨架site_intrusion/ ├── config.yaml ├── main.py ├── region_annotate.py ├── alert.py ├── weights/ │ └── yolov8s.pt # 官方预训练权重可替换成自己的微调模型 ├── videos/ │ └── site_demo.mp4 └── alert_output/pip install opencv-python numpy ultralytics pyyaml requests依赖说明ultralytics一个包同时带上了推理引擎和模型管理官方权重首次推理时自动下载opencv-python负责读视频流、画框、写图片requests用于把告警推送到 webhook。装opencv时如果出现DLL load failed大多是 ffmpeg 组件问题直接用conda install opencv替代 pip 版本更稳。3.2 区域标注用鼠标在视频帧上画出多边形3.2.1 标注脚本的鼠标事件处理现场标定区域时最忌讳“对着照片脑补坐标”。我一般写一个简单的region_annotate.py读取视频第一帧鼠标左键加点、右键闭合把坐标打印出来再填进config.yaml。# region_annotate.py import cv2 points [] frame None def on_mouse(event, x, y, flags, param): if event cv2.EVENT_LBUTTONDOWN: points.append((x, y)) print(point:, (x, y)) elif event cv2.EVENT_RBUTTONDOWN: print(final points:, points) cv2.destroyAllWindows() def main(video_path): global frame cap cv2.VideoCapture(video_path) ok, frame cap.read() if not ok: raise RuntimeError(read frame failed, check video path) cv2.namedWindow(annotate) cv2.setMouseCallback(annotate, on_mouse) while True: cv2.imshow(annotate, frame) if cv2.waitKey(1) 0xFF ord(q): break cv2.destroyAllWindows() if __name__ __main__: main(videos/site_demo.mp4)按键说明左键单击添加一个边界顶点右键结束并打印坐标列表。注意弹出的窗口默认显示原图尺寸如果显示器窗口过大可以在imshow前用cv2.resize(frame, (960, 540))缩放显示。坐标记录的是缩放画面上的坐标写进配置时推理端要按相同比例还原。3.3 检测与入侵判断把模型输出变成事件这一步是核心。每读一帧先让模型检测人再对每个检测框取脚点入域判断入域目标数量大于 0 就说明当前帧存在入侵事件。import cv2 import numpy as np import yaml from ultralytics import YOLO def load_config(path): with open(path, encodingutf-8) as f: return yaml.safe_load(f) cfg load_config(config.yaml) model YOLO(weights/yolov8s.pt) REGION np.array(cfg[region][points], dtypenp.int32) CONF_THRES cfg.get(conf_thres, 0.5) CLS_PERSON 0 # COCO 数据集里 person 的类别 id def detect_intruders(frame, region): results model.predict(frame, confCONF_THRES, verboseFalse)[0] intruders [] for box in results.boxes: cls int(box.cls[0]) conf float(box.conf[0]) if cls ! CLS_PERSON: continue x1, y1, x2, y2 [int(v) for v in box.xyxy[0]] foot ((x1 x2) // 2, int(y2)) if is_inside(region, foot): intruders.append((x1, y1, x2, y2, round(conf, 2))) return intruders参数说明与注意点confCONF_THRES直接在predict里传入低于该置信度的框会被模型层丢弃box.xyxy给出的是原图像素坐标不需要额外还原但推理时手动resize过输入帧就另说foot取框底边中点这在俯视和斜视画面中都比框中心点更贴近“人脚的物理位置”。如果现场需要过滤车辆单独保留cls判断即可不需要改模型。3.4 告警与落盘截图、冷却和 webhook 推送3.4.1 告警函数的设计告警不能每帧触发。视频 25 帧每秒人站在区域里 5 秒就要产生 125 条告警没人受得了。所以要做两级保护同一目标持续在区域内才告警且触发后进入冷却窗口。# alert.py import time import cv2 import requests class IntrusionAlert: def __init__(self, webhook_urlNone, cooldown10, min_frames15): self.webhook_url webhook_url self.cooldown cooldown self.min_frames min_frames self._last_alert_time 0 self._persist_frames 0 def update(self, frame, intruders): if intruders: self._persist_frames 1 else: self._persist_frames 0 return if self._persist_frames self.min_frames: return now time.time() if now - self._last_alert_time self.cooldown: return self._last_alert_time now path time.strftime(alert_output/%Y%m%d_%H%M%S.jpg) cv2.imwrite(path, frame) if self.webhook_url: requests.post(self.webhook_url, json{image: path, intruders: intruders}, timeout3)min_frames的含义是“连续多少帧检测到入侵才认为事件成立”用帧数而不是秒数因为现场帧率不确定默认 15 帧在 25fps 下约等于 0.6 秒足够滤掉一个路人沿警戒线外侧快速走过的瞬时误触。cooldown是冷却秒数10 秒内只会推送一次。正式接入声光报警器时也可以在这个类里增加一个 GPIO 开关把requests.post一行替换成继电器控制即可。3.5 主循环把模块串成一个可运行系统def main(): cap cv2.VideoCapture(videos/site_demo.mp4) alert IntrusionAlert(webhook_urlNone, cooldown10, min_frames15) while True: ok, frame cap.read() if not ok: break intruders detect_intruders(frame, REGION) alert.update(frame, intruders) # 可视化画出区域和入域目标 cv2.polylines(frame, [REGION], True, (0, 255, 0), 2) for (x1, y1, x2, y2, conf) in intruders: cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(frame, fintrusion {conf:.2f}, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imshow(site monitor, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()主循环只负责一路视频源和后台的多路调度逻辑没有关系这里展示的是监控视频源连续推理的基本形态。imshow在服务器不带显示环境时不能执行检测告警部分不受影响alert_output目录要提前建好否则cv2.imwrite静默失败告警图缺失是现场调试时最隐蔽的问题之一。4. 参数量级与取舍为什么调参要围绕实际机位来定4.1 核心参数速查表参数建议区间取值偏小取值偏大影响对象conf0.40 ~ 0.60召回高误检多漏检多误报少小目标、烟雾遮挡iou0.5 ~ 0.7同目标多框检测框合并更激进密集人群漏检imgsz640 / 960 / 1280帧率高、小目标丢帧率降、小目标召回上升远处几像素高的人frame_step1 / 2 / 3实时性好CPU 压力小告警实时性min_frames10 ~ 25响应快抗干扰强穿越边线速度cooldown5 ~ 30消息密集消息少告警体验把min_frames用帧数还是秒数选择讲细一点项目里不同摄像头帧率可能不同用帧数做告警持续时间的跨设备一致性更差所以我有时会加一个fps cap.get(cv2.CAP_PROP_FPS)把min_frames换算为时间阈值再比较。远处小目标在低分辨率下的置信度普遍在 0.3~0.5如果巡检反馈漏报多先把imgsz提到 960 而不是降 conf。4.2 跳帧检测与检测结果复用实时视频流全帧推理对 GPU 压力大一个工地好一点的现场就是 8 路摄像头汇聚到同一台机器。跳帧思路是检测只在部分帧执行中间帧沿用上一帧的检测结果做绘制与告警判定。25fps 视频、检测帧率降到 12.5fps 时人员行走速度下运动距离也就 0.2~0.3 米每帧入侵判断仍然可靠。frame_idx 0 last_intruders [] while True: ok, frame cap.read() if not ok: break if frame_idx % frame_step 0: last_intruders detect_intruders(frame, REGION) alert.update(frame, last_intruders) frame_idx 1注意告警冷却和min_frames也是在检测帧上累加的。跳帧后实际时间不变只是参与统计的帧数变少因此min_frames要按检测频率换算例如原来 15 帧等于 0.6 秒跳帧后大约 8 个检测帧即可等同 0.6 秒。4.3 部署到边缘设备时的参数差异在 Jetson 这类边缘设备上建议把模型导出为 TensorRT engine 再推理。ultralytics 的导出接口支持一条命令yolo export modelweights/yolov8s.pt formatengine device0导出的 engine 加载路径在detect_intruders里替换为YOLO(weights/yolov8s.engine)即可。int8 量化后精度对远处小目标会有可感知的下降这时可以适当把conf下调 0.05。CPU 上跑不住的情况下优先压缩imgsz到 480 或者用yolov8n权重这两种措施的成本都比牺牲告警正确性低。提示TensorRT 导出时要求本机已安装匹配的 CUDA/cuDNN/TensorRT 版本build 失败报 engine 缺失时先检查这几个环境变量的版本而不是马上怀疑代码。5. 现场排查检测没报、误报爆炸这类问题的定位顺序5.1 模型装好了但程序起不来启动就崩通常集中在两个点权重加载和视频流打开。python -c import torch; print(torch.cuda.is_available()) python -c import cv2; print(cv2.getBuildInformation()) | grep -i ffmpeg第一句看 CUDA 是否可用返回 False 时权重也能跑但会落到 CPU推理耗时可能是 GPU 的 20 倍以上第二句确认 opencv 是否编译进 ffmpeg视频文件打不开大概率是缺少解码库。服务器上跑代码出现cv2.imshow报错时说明当前环境无显示服务把可视化代码放进参数开关检测告警部分照常运行这是最稳妥的处理方式。5.2 检测到了人但区域始终不触发这类问题十有八九是坐标不一致。检查点按优先级来看REGION坐标是否来自未缩放的显示画面推理帧是否被resize检测框坐标是 resize 后的坐标而区域仍是原图坐标视频流里检测框坐标与区域坐标绘制到同一帧上是否有明显错位。我会先把区域画在帧上输出一张图做“坐标校准图”直接肉眼确认多边形是否压在实际警戒线上。另一个常被忽略的点numpy 读 yaml 默认给 float 数组cv2.polylines传入 float 数组在部分版本会报错先astype(np.int32)再传。5.3 误报多导致告警轰炸先分辨误报来源是模型把远处工人漏掉导致该报没报还是检测框抖动造成脚点在边线上来回摆动。这里用“脚点与边线距离”代替“二值判定”更好用把“临界区域”的概念引入距离边线 10 像素以内算临界连续多帧踩线才算入域。def dist_point_to_segment(p, a, b): x0, y0 p x1, y1 a x2, y2 b dx, dy x2 - x1, y2 - y1 if dx 0 and dy 0: return ((x0 - x1)**2 (y0 - y1)**2) ** 0.5 t max(0, min(1, ((x0 - x1)*dx (y0 - y1)*dy) / (dx*dx dy*dy))) proj (x1 t*dx, y1 t*dy) return ((x0 - proj[0])**2 (y0 - proj[1])**2) ** 0.5这个函数和射线法搭配后可以把入域条件从“严格在多边形内”放宽成“进入边线内侧 N 像素”滤掉检测框像素级抖动的临界误报。参数 N 建议取 5~10像素绝对值在 1080P 画面下约等于 10~20 厘米基本不会带来肉眼可感的判定迟钝。6. 落到现场把告警推送到项目管理群并完成验证6.1 一次性接入 webhook 机器人很多现场并不依赖独立告警服务器项目经理要的是群里能收到带图的消息。思路是在报警触发时把截图上传或直接发 base64 给机器人接口webhook 地址写在config.yamldef push_webhook(image_path, detail): with open(image_path, rb) as f: b64 base64.b64encode(f.read()).decode() requests.post(WEBHOOK_URL, json{ msgtype: text, text: {content: f危险区域入侵 {detail}见图片 base64 数据} }, timeout5)这种接入方式要注意服务端对图片大小的限制1080P 截图 base64 后有 1~3MB超出上限会被静默丢弃。稳妥做法是先用cv2.imencode压缩到 800 像素宽再编码或者只推送文字描述。不同的摄像头机位最好在消息里带上机位编号施工现场的处置速度会明显快很多。6.2 用一段普通视频完成回归验证交付前总要验证“会不会漏”“会不会多报”。拿手机绕警戒线走几圈拍一段 3 分钟视频画好区域跑一遍程序用统计值对比人工标注结果告警触发次数、首次触发延迟、是否有重复推送。真正有效的验证指标不是 mAP而是误报率非入侵时段内触发次数除以总时长和漏报率入侵发生后未在 N 秒内触发的次数除以人工标注入侵次数。跑到这两项都收敛在 3% 以内这个系统才有资格谈上线。本文还有配套的精品资源点击获取
返回列表