简介:一份面向智慧城市与安防监控场景的“目标识别+区域入侵检测”C++工程。基于YOLOv5与OpenCV 4.5,在VS2019下实现从目标框识别到越线/区域判定的完整流程,适合有一定C++基础、希望将检测算法落地为可运行项目的开发者。压缩包共337个文件,约54.88MB,主要包含242个hpp与61个h头文件、2个cpp实现文件,以及onnx模型、dll动态库、lib、pdb调试信息和mp4演示视频等。头文件与源码构成完整工程结构,动态库与模型用于直接运行,演示视频可直观查看检测效果。该资源已有2453人学习。下载后除了可编译运行的项目代码,还附带OpenCV与ONNX Runtime依赖、配置脚本和演示视频,参考配套博客还能理清从划定区域到判定入侵目标的算法思路,适合工程入门或方案原型验证。
1. 目标识别与区域入侵检测:别把“检测到人”当成“发生入侵”
目标识别与区域入侵检测在园区周界、工地围挡、变电站这类场景里是一套组合能力。目标检测模型负责输出“画面里有人、有车”,但业务真正要回答的是:这个人有没有进入划定的防区,有没有跨越虚拟警戒线,有没有在里面滞留超过规定时间。区域入侵检测就是在检测算法之上叠加空间规则与时序判断,把“看见目标”变成“理解行为”。
这类系统最常见的形态是一路或几路 RTSP 摄像机加一台边缘盒子。工程上的难点不在模型本身,而在三个容易做糙的环节:夜间与小目标的召回率、区域判定在边界上的抖动、报警事件的去重与冷却。接下来按模型选型、空间规则、推理管线、调试验证四个环节推进,把一套可用系统的搭建路径和常见坑位讲清楚。
2. 模型选型与场景化训练:目标识别决定入侵检测的天花板
2.1 检测模型怎么选:YOLOv8 与 RT-DETR 的实际取舍
区域入侵检测对检测器的要求有两条:远距离小目标要抓得住,单帧推理延迟要可控。
常见做法是在 YOLO 系里做选择。YOLOv8n 或 YOLOv8s 在 Jetson Orin Nano 上能跑到 30-60 FPS,覆盖单路视频流绰绰有余;如果推理端是带 RTX 显卡的服务器,换成 YOLOv8m 甚至 YOLOv8l,小目标上的精度收益非常明显。RT-DETR 这类无锚框检测器在遮挡场景的召回率确实更好,但部署链路过重,TensorRT 适配成本高,周界项目里除非有专职算法工程师长期维护,否则不推荐作为第一选择。
需要纠正一个常见误区:检测帧率不等于监控帧率。一个 5 米宽的警戒区,人在 25 FPS 画面里穿过至少能留下 20 帧,把推理频率降到 5-8 FPS 完全足够判断入侵行为,还能省出一半算力。代价是相邻帧间隔变大,后续跟踪模块必须用时间戳而不是帧序号来计算位移和速度。
2.2 标注策略:夜间难例决定线上的召回率
训练数据这部分,我的建议是先解决难例覆盖,再谈样本量。周界场景里 person 和 vehicle 是主要目标,但真正影响线上效果的是下面几类样本:
| 难例类型 | 对检测的影响 | 建议处理方式 |
|---|---|---|
| 夜间红外与低照度人形 | 漏检、检测框抖动 | 单独建 night 目录,按时段拆分验证集 |
| 遮挡 30% 以上的人 | 框中心偏移 | 标注可见部分,不猜测被遮挡边缘 |
| 雨雾与逆光 | 对比度低导致置信度波动 | 训练集混入同场景增强样本 |
| 狗、鸟等动物 | 被误报为 person | 标注为单独类别或 ignore 类 |
遮挡样本的标注方式尤其影响后续链路。检测框与遮挡边缘贴合的训练结果,框的中心位置偏差更小。入侵判定用的是框底边中心点,框的位置越准,点和区域边界的包含关系就越可靠。训练集按“普通样本:难例 = 7:3”混合,比单纯增加总样本量更容易收敛。
2.3 模型导出与边缘部署的验证方法
训练完成后要把模型导出为推理引擎格式。以 YOLOv8 为例,官方 export 命令按硬件选择目标格式:
# 导出 TensorRT FP16 引擎,适合 N 卡边缘盒子 yolo export model=best.pt format=engine device=0 half=True dynamic=False # 导出 OpenVINO IR,适合 Intel CPU 或无独显设备 yolo export model=best.pt format=openvino half=Falsehalf=True的 FP16 推理通常能带来 30%-50% 提速,但必须先跑一遍验证集,确认 mAP 掉点不超过 0.5%。dynamic=False固定输入尺寸为 640x640,TensorRT 部署最省事,代价是远端小目标可能被压缩到几个像素。需要保留小目标时,常见做法是改用 1280 输入,推理耗时大约翻倍,要跟检测帧率一起权衡。
部署完必须做一次分辨率敏感性验证。同一个模型,1080p 画面缩放到 640 推理,和保持原分辨率推理,远端行人的召回率差距可能超过一倍。稳妥的做法是取一段有代表性的录像,在 0.3 置信度阈值下统计每帧召回率,确认没有悬崖式掉点再上线。
3. 区域判定与坐标映射:什么才算“入侵”
3.1 区域定义:多边形与射线法点包含
入侵区域在图像上就是一个多边形。标定时用鼠标在画面里点出四到八个顶点,保存为归一化坐标(0-1),分辨率更换时不需要重新标定。
判定目标是否在区域内,工业界最常用的是射线法点包含测试。目标点取检测框底边中点,不取框中心:人体弯腰或携带物品时,框的上缘位置变化很大,底边更接近目标与地面的接触点,受姿态影响最小。
def point_in_polygon(px, py, poly): inside = False j = len(poly) - 1 for i in range(len(poly)): xi, yi = poly[i] xj, yj = poly[j] # 条件1:点的 y 值位于边的两个端点之间,保证水平射线与该边相交 # 条件2:交点的 x 大于点的 x,保证交点在点右侧,满足射线方向 if ((yi > py) != (yj > py)) and \ (px < (xj - xi) * (py - yi) / (yj - yi + 1e-6) + xi): inside = not inside j = i return insideinside每经过一条边翻转一次,最终为 True 说明点在多边形内。1e-6是防止边恰好水平时除零。射线法时间复杂度 O(n),多边形顶点在十个以内时,单帧内对所有目标做判定的耗时可以忽略。
边界情况有两个。一是目标站在区域边界上,检测框抖动导致底边中点在边界两侧横跳,解决办法在 3.4 节的状态机里,用连续帧投票来平滑。二是多边形自相交,标定时应避免,否则射线法结果不确定,建议标完做一次自相交检测。
3.2 虚拟警戒线:线段相交判定
周界场景除了“进入区域”,另一种常见需求是跨越虚拟警戒线,比如围墙外 1 米处拉一条绊线。绊线判定用线段相交算法:两个线段分别是“目标前后两帧的底边中点连线”和“警戒线”。
def cross(o, a, b): # 向量 oa 与 ob 的叉积,正负表示 b 在 a 的哪一侧 return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) def seg_intersect(p1, p2, p3, p4): # 线段 p1p2 与线段 p3p4 是否严格相交 d1 = cross(p3, p4, p1) d2 = cross(p3, p4, p2) d3 = cross(p1, p2, p3) d4 = cross(p1, p2, p4) return d1 * d2 < 0 and d3 * d4 < 0用两帧轨迹线段而不是单帧点,是为了对抗检测抖动。目标跨线的那一帧,检测框可能偏左或偏右几像素,单帧点会落在线的另一侧导致漏判;轨迹线段把“从线的一侧到另一侧”这个过程表达出来,只有真正越过才报警。
绊线的方向判断也基于叉积:cross(p1, p2, p3)和cross(p1, p2, p4)异号说明轨迹从一侧穿到另一侧,再结合轨迹走向可以区分“正向闯入”和“反向离开”,这对单向禁入区域很有用。
3.3 透视校正:图像坐标与地面坐标的换算
如果业务只需要“进没进区域”这个布尔值,图像坐标已经够用。但有以下需求时,就必须把图像坐标投影到地面平面:输出目标在地面上的实际位置、计算移动速度、多台相机共享同一套区域坐标。
| 做的事情 | 用哪种坐标 | 原因 |
|---|---|---|
| 判断是否进入多边形区域 | 图像坐标即可 | 判定快,透视不影响包含关系 |
| 算速度、算驻留面积 | 地面坐标 | 消除近大远小的透视畸变 |
透视校正用四点标定加单应矩阵。在画面里选地面上的四个点,对应实际地面的一个矩形:
import cv2 import numpy as np # 图像上选取地面四个点,实际对应一个 10m x 6m 的矩形 image_quad = np.float32([[860, 480], [1120, 480], [620, 720], [1240, 720]]) ground_quad = np.float32([[0, 0], [10, 0], [0, 6], [10, 6]]) H = cv2.getPerspectiveTransform(image_quad, ground_quad) # 检测框底边中点 (cx, bottom_y) 投影到地面坐标 foot = np.array([[[cx, bottom_y]]], dtype=np.float32) ground_x, ground_y = cv2.perspectiveTransform(foot, H)[0][0]标定细节:四个图像点必须选在地面上,选到墙面或树冠会让映射直接扭曲;地面点取矩形,单位用米,后面算速度时不用再做单位换算。getPerspectiveTransform返回 3x3 单应矩阵 H,它只对同一平面成立,所以俯视角度越大的相机标定越准,平视相机的远处地面误差会指数放大。
项目里我一般把区域多边形和警戒线也投影到地面坐标,再跑 3.1 和 3.2 的算法。好处是换一台相机覆盖同一片区域时,规则多边形不用重新画。
3.4 抗抖动:连续帧投票与状态机
单帧判定不可靠,行业标准做法是连续帧投票:连续 N 帧在区域内才确认进入,连续 M 帧在区域外才确认离开。用状态机实现:
class ZoneState: def __init__(self, enter_frames=3, leave_frames=6): self.enter_frames = enter_frames self.leave_frames = leave_frames self.inside_count = 0 self.outside_count = 0 self.inside = False self.enter_time = None def update(self, in_zone: bool, ts: float): if in_zone: self.inside_count += 1 self.outside_count = 0 if not self.inside and self.inside_count >= self.enter_frames: self.inside = True self.enter_time = ts return "enter" else: self.outside_count += 1 self.inside_count = 0 if self.inside and self.outside_count >= self.leave_frames: self.inside = False return "leave" return Noneenter_frames设为 3,表示连续 3 帧判定在区域内才触发进入事件,通常对应 0.5 秒左右,能过滤掉检测框一瞬间的越界抖动。leave_frames要比enter_frames大,这样目标在边界徘徊时不会频繁进入、离开。目标 ID 消失超过一秒就丢弃对应的状态对象,避免内存累积。
4. 区域入侵检测的推理管线与报警抑制:把单帧结果变成可靠事件
4.1 视频流读取与推理线程解耦
实际部署里,RTSP 流的抖动和卡顿是常态,把抓帧、解码、推理放在同一个循环里,一旦网络波动就会整体延迟,漏掉关键帧。
标准做法是抓帧线程负责 read 和丢帧策略,推理线程从队列里取最新帧:
import threading, queue, time, cv2 def capture_worker(rtsp_url, frame_q: queue.Queue, stop_event): cap = cv2.VideoCapture(rtsp_url) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) while not stop_event.is_set(): ret, frame = cap.read() if not ret: time.sleep(0.5) # 断流后间歇重连 cap.open(rtsp_url) continue if frame_q.full(): try: frame_q.get_nowait() # 队列满时丢弃最旧帧 except queue.Empty: pass frame_q.put(frame) # 保证消费端拿到的始终是最新帧 cap.release()CAP_PROP_BUFFERSIZE设为 1,避免 OpenCV 内部缓冲堆积导致画面延迟越来越大。队列上限设 2-3 帧,满时丢最旧帧,原则是入侵判断宁可跳过中间帧,也不能用 3 秒前的画面。断流重连的时间间隔按 0.5-2 秒之间调,太短会让相机设备频繁重连重启,太长会漏掉事件窗口。
4.2 目标跟踪与 ID 关联
区域入侵必须知道“这一帧的框还是不是上一个人”,否则无法计算驻留时长。最小可用方案是 IoU 匹配:
def iou(box1, box2): # 两个检测框的交并比,范围 0-1 ix1 = max(box1[0], box2[0]) iy1 = max(box1[1], box2[1]) ix2 = min(box1[2], box2[2]) iy2 = min(box1[3], box2[3]) inter = max(0, ix2 - ix1) * max(0, iy2 - iy1) area1 = (box1[2] - box1[0]) * (box1[3] - box1[1]) area2 = (box2[2] - box2[0]) * (box2[3] - box2[1]) return inter / (area1 + area2 - inter + 1e-6)匹配时用卡尔曼预测出的框位置计算 IoU,比直接用上一帧位置更可靠。iou_thresh取 0.3-0.4 之间,低于 0.3 会让同一目标在快速移动时分裂出多个 ID,高于 0.4 会让遮挡恢复后的目标匹配不上。
单相机周界场景不需要上 DeepSORT。DeepSORT 的 ReID 特征在跨相机接力跟踪时才物有所值,单相机下引入外貌特征反而会在光照突变时产生误匹配。卡尔曼加 IoU 的组合,足够应对单相机的目标关联需求。
4.3 报警去重:按目标 ID 做冷却
入侵报警应该定义为状态转移事件,而不是每一帧都输出。目标进入区域时产生一条事件,驻留期间只更新时间戳,离开时补上驻留时长。
去重必须按 track_id 维度,不能用全局冷却。两个人先后进入同一区域,全局冷却会把第二个人的报警吞掉:
class AlarmManager: def __init__(self, min_interval=10): self.min_interval = min_interval self.last_alarm = {} def should_alarm(self, track_id, now_ts): last = self.last_alarm.get(track_id, 0) if now_ts - last < self.min_interval: return False self.last_alarm[track_id] = now_ts return Truemin_interval表示同一目标相邻两次报警的最小时间间隔,10 秒在周界场景里比较合理,配合状态机的进入事件,能覆盖“进入报警一次、持续驻留不重复打扰”的需求。last_alarm字典要定期清理超过 5 分钟的 track_id,否则长稳运行会内存泄漏。
4.4 性能预算:多路视频时的资源分配
多路视频接入时,瓶颈几乎都在推理端。按实测经验,1080p 视频从解码到事件输出的耗时占比大致如下:
| 模块 | 耗时占比 | 优化手段 |
|---|---|---|
| 解码与缩放 | 15%-25% | NVIDIA 硬解或 CUDA 缩放 |
| 模型推理 | 50%-70% | TensorRT FP16,多路合批 |
| 检测后处理 NMS | 10%-15% | 用 torchvision.ops.nms |
| 跟踪与区域判定 | 小于 5% | Python 直接写即可 |
多路推理合批能让 GPU 利用率显著提升。把四路的预处理图像拼成一个 batch 推理一次,再按 batch 维度拆回各路结果。合批后帧率不是线性下降,四路 1080p 降到 10 FPS,GPU 占用通常只有单路 25 FPS 的 1.5 倍左右。
5. 调参与验证:用历史录像回放把区域入侵误报当 bug 修
5.1 先盯住三个必调参数
区域入侵系统的调试不能靠肉眼看实时画面。项目里最该先调的是下面三个参数:
| 参数 | 推荐范围 | 影响 | 调试方向 |
|---|---|---|---|
| conf_thres | 0.25-0.45 | 低于 0.2 容易框出椅子、树影 | 夜间调低,白天调高 |
| NMS IoU | 0.45-0.7 | 人群密集时误抑制相邻目标 | 目标重叠多时调低 |
| enter_frames | 3-5 | 过滤单帧抖动 | 推理卡顿、帧率低时调高 |
conf_thres和enter_frames是联动关系。白天可以把置信度阈值调高到 0.4 来压误报,夜间调低到 0.3 保召回;如果模型在夜间依然抖动,优先把enter_frames从 3 改成 5,而不是继续降阈值。
5.2 离线回放与误报定位
回放验证的做法:选夜间、雨天、早晚高峰各 30 分钟录像,离线跑完整管线,输出带时间戳的事件列表;再人工标注真实入侵事件,两份结果对比,统计漏报和误报的分布。
当误报集中在某个固定区域时,常见原因不是模型,而是标定。把检测框底边中点的轨迹画在输出视频上,如果轨迹在区域边界上呈锯齿状来回穿,是检测框抖动;如果轨迹平稳但触发报警,多半是区域多边形画小了,或者目标点取到了框中心而不是底边中点。
当漏报集中在某个时段时,先看该时段模型输出的平均置信度。大量目标在 0.3 附近波动,说明是模型对光照的适应问题,优先补训练样本而不是全局降阈值,否则所有时段的误报会一起抬上来。
最后一个具体技巧:事件对比时不要用帧号对齐,RTSP 回放和在线推理的帧号是从零独立计数的,不可比。要以时间戳对齐,并在每次报警时把触发帧的 JPG 落盘保留 24 小时。排查时按事件 ID 调出当时的画面,一眼就能判断是检测没框住、跟踪断了 ID,还是区域判定逻辑写错了,一次定位到具体模块。
本文还有配套的精品资源,点击获取