智能厂房人数统计与超员报警系统
基于单目视觉(YOLO + ByteTrack)的多路 RTSP 人数统计与超员报警系统,覆盖跨线差分计数、区域计数、房间多相机融合与 MQTT 上报
智能厂房人数统计与超员报警系统 — 技术文档
文档性质:面向实现方法的功能逻辑说明 覆盖范围:
algorithms/算法模块、people_counter_W.py计数引擎、main_z_chuangkou_image.py主程序
一、总体技术方案
系统基于 单目视觉人数统计算法体系,通过多路 RTSP 网络摄像头采集画面,使用 YOLO 完成行人检测、ByteTrack 完成多目标跟踪,再结合”跨线计数(出入差分)“与”区域计数(逐帧存在人数)“两种统计手段,计算每个房间的实时人数。全程不依赖双摄像头/深度相机,人数与位置均靠单目相机像素坐标 → 世界坐标转换得到。
关键技术点:
| 环节 | 采用方法 | 作用 |
|---|---|---|
| 行人检测 | YOLO 模型(model.track,仅 person 类) | 输出检测框与跟踪 ID |
| 目标跟踪 | ByteTrack(bytetrack.yaml,persist 跨帧保持 ID) | 同一人跨帧 ID 稳定,是过线计数的前提 |
| 像素→世界坐标 | 单目测距:宽度估计法(相似三角形)或射线投影法(光心射线与地平面求交) | 得到人脚部世界坐标(米),供去重、补点、位置上报 |
| 人数统计 | 跨线差分计数 / 区域逐帧计数 / 20 帧众数平滑 | 见第三章算法详解 |
| 房间汇总 | 增量式跟随(较大值更新 + 增量叠加)+ 检测点选取与随机补点 | 多相机融合成房间总人数 |
| 上报 | HTTP 图片服务 + MQTT JSON 上报 | 前端看板展示人数、照片、报警 |
二、系统分层与数据流
┌─────────────────────────────────────────────────────────────┐
│ main_z_chuangkou_image.py(UI / 数据组装 / 上报) │
│ PyQt5 MainWindow ── PeopleCounterThread(主循环) │
│ ├─ DataProcessor(0.1s 组装数据 + 超员判定 + 拍照) │
│ ├─ MQTTPublisher(3s 发布 room / 订阅 model_room) │
│ └─ HTTPServerManager(8080 图片静态服务) │
└───────────────────────────┬─────────────────────────────────┘
│ 调用
┌───────────────────────────▼─────────────────────────────────┐
│ people_counter_W.py(计数引擎 RoomPeopleCounter) │
│ 每相机:CameraCaptureThread(取流) ─ CameraInferenceManager │
│ (YOLO.track → 线程池异步 → 算法.process/draw) │
│ 每房间:按算法分发到 room_statistics 的统计器汇总人数 │
└───────────────────────────┬─────────────────────────────────┘
│ process
┌───────────────────────────▼─────────────────────────────────┐
│ algorithms/(算法工厂按配置 type 动态加载) │
│ yolo_detection │ coordinate_detection │ cross_line_detection│
│ cross_line_region_detection │ region_counter │
│ (底层算法) ──► room_statistics(房间级汇总统计器) │
└─────────────────────────────────────────────────────────────┘
数据流单向:RTSP 帧 → 跳帧存储 → YOLO 检测跟踪 → 坐标转换 → 过线/区域判定 → 房间统计器融合 → 全厂汇总/内外比对 → 超员判定 → MQTT 发布。
三、算法模块详解(algorithms/)
3.1 世界坐标转换(coordinate_detection.py)
解决的问题:单目相机无法直接测距,需要利用相机标定参数把像素坐标映射为真实房间坐标(米)。
用到的参数与方法:
- 相机内参矩阵 K、畸变系数 D、去畸变后新内参
new_K、相机到世界变换矩阵camera2world(R、t)。 - 像素点先经
cv2.undistortPoints做畸变校正,消除广角镜头桶形畸变造成的定位偏移。
两种测距模型(按配置 method 选择):
- 宽度估计法(width):假设行人肩宽(
real_width_m,默认 0.5 m)在世界中恒定,检测框的水平像素宽度与真实宽度成正比,利用小孔成像相似三角形depth = 真实宽度 × 焦距 / 像素宽度求出深度,再把相机系下的脚部点经camera2world变换得到世界坐标。优点:实现简单、无地面假设。 - 射线投影法(ray):以检测框底部中心(脚部)或框中心为起点,经反投影得到相机坐标系下的射线方向,旋转到世界系后与世界平面(地面
z=0,或按target_height指定高度)求交点,交点即人脚在世界系的位置。优点:符合透视几何,适合俯视/斜视安装,需知道相机离地高度。
输出检测点结构:{class, conf, xyz_m(世界xyz米), xy(平面x,y), pixel_center, method, on_ground},供后续统计与补点使用。
Coordinate_detection 包装类:逐帧对每个检测框做上述转换,并对世界坐标做边界修正——超出房间长宽(room_length/room_width)的坐标直接截断到边界(此文件为截断式;跨线类算法文件中为”随机拉回”式,见 3.2)。不产生过线计数,返回检测点列表。
3.2 跨线检测算法(cross_line_detection.py,类 Cross_line_detection)
思想:在画面中定义一条(或多条)虚拟”检测线”,把线两侧定义为 side1/side2。行人跟踪 ID 连续的两帧连线若与检测线相交,且跨越前后所在侧发生变化,则判定为一次有效”过线”,按方向给计数 +1/-1。这种方式记录的是进入与离开的净流量(室内人数增量)。
核心状态(按相机实例持有):
line_counters:每条线的累计净出入数,全房间人数 = 各线计数之和(通常单线)。track_history:每个 track_id 记录”上一帧脚部位置 (x,y)、每一条线的历史所在侧、最后更新时间”,并限制最多保留 10 个 ID 防内存增长。crossed_tracks:记录(track_id, line_idx),防止同一目标在同一帧内跨越多条线被重复计数。
单帧处理流程(process):
- 取坐标转换器,清空上帧世界坐标结果,清空
crossed_tracks,区域计数归零。 - 对每个检测框:算底边中心(脚部像素点)→ 转世界坐标 → 读跟踪 ID。
- 若该 ID 首次出现则写入 track_history(首帧不计数,只建基线,避免误判)。
- 过线判定:用上一帧脚点与本帧脚点连成线段,与每条检测线做线段相交判断(向量叉积符号 + 共线端点特判,即跨立实验);相交后再用叉积符号确定目标当前在线的哪一侧,与历史侧比对:side2→side1 记 +1(进入),side1→side2 记 -1(离开)。只有两侧发生翻转且该 (track_id, line) 本帧未计数过才生效。
- 区域过滤:使用 shapely 多边形
contains(Point(脚点))判断人是否在配置的检测区域内,仅区域内的检测才计入输出,区域外的不计(用于把检测限制在门口/指定通道)。 - 边界随机修正:世界坐标越出房间边界时,向房间内随机偏移 0.2~1.0 m 拉回,避免点贴墙造成位置上报异常。
- 随机检测点生成:由于”区域计数”只统计多边形内的人,而跨线差分可能给出比当帧可见人数更大的值,算法以房间配置的
random_x/random_y为中心生成 3×3 网格(间距 0.5 m) 的 9 个随机补充点(track_id 从 101 起),供房间统计器在检测点不足时”凑足人数”,保证 UI/上报的可见点数量与人数一致。 - 输出:
{detections, random_detections, room_total_count(过线净人数), reset_triggered, room_id, camera_id}。
重置机制(reset):重置相机计数时,把所有过线计数器清零、清空 track 与 crossed 记录,并置 reset_triggered=True、reset_signal_counter=10,让后续连续 10 帧返回重置信号;房间统计器收到该信号会把”上一帧过线人数基线”同步为当前值、增量置 0,避免清零瞬间产生虚假的大幅差分。
3.3 跨线 + 区域检测算法(cross_line_region_detection.py,类 Cross_line_region_detection)
在 3.2 全部逻辑的基础上,增加三点改进:
- 重叠框 NMS:处理前先对检测结果做自定义 NMS(按置信度排序,IoU>0 且”并集/较大框面积比 < 1.25”判为重叠并抑制较小框),减少同一个人被重复检测导致的多余轨迹/错误计数。
- 区域人数输出:每帧统计落在区域内目标的去重集合大小
region_counters(按 track_id 去重),作为”当前区域可见人数”一并输出,供房间统计器与过线人数交叉验证。 - 零人超时自复位:当
region_counters == 0持续 30 秒(记录region_zero_start_time)时,判定房间无人,自动清零所有过线计数、清空跟踪记录,并置reset_triggered=True、reset_signal_counter=20(连续 20 帧返回重置信号)。这解决”有人一直坐着不动 → 跟踪丢失 → 过线计数长期不变甚至为负”的漂移问题,确保清场后计数归零。 - 其余过线判定、随机点、边界修正同 3.2。
3.4 纯区域计数算法(region_counter.py,类 Region_counter)
适用:摄像头正对无需区分进出方向的区域(如车间内部),只统计”画面里此刻有几个人”。
- 仅保留”区域”配置(不解析检测线)。
- 逐帧对每个检测框做多边形包含判断(shapely contains 脚点),区域内目标 ID 写入集合,区域人数 = 集合大小(每帧独立统计,无跨帧累计、无连续帧确认逻辑,也未启用随机补点)。
- 输出
{detections, region_counters(去重区域人数), random_detections:{}},不含过线值与重置信号。 - 缺点:被遮挡/短暂离场的人会被漏计,因此房间统计层通常搭配众数平滑使用(见 3.6 的 RegionCounter)。
3.5 纯 YOLO 检测算法(yolo_detection.py,类 Yolo_detection)
定位:早期/演示用算法,不做区域与过线过滤,只做”检测 + 轨迹平滑可视化”:
- 每目标保留最近 10 个中心点做移动平均平滑,抑制抖动;同时保留最近 30 点用于绘制渐变轨迹线 + 方向箭头。
- 人数 = 当前帧全部检测框数(不区分是否在区域内)。
- 边界修正为截断式。输出不含 room_total_count / random_detections(置 0 或空)。
3.6 房间统计汇总(room_statistics.py)——多相机融合核心
这是算法层到”房间人数”的关键一层。单个相机的算法输出只是一路画面结果,一个房间通常有多台相机(各守一个门口/区域),需要一套房间级统计器把多路结果融合成该房间当前总人数,并返回一组用于展示/上报的”检测点”。
系统内三个统计器与三种房间算法一一对应:
(a) Cross_Line_DetectionCounter —— 过线差分式房间统计
- 维护两级状态:房间状态
{total_people};每相机状态{last_cross_line_people, delta_cross}。 - 逐相机计算增量:
delta = 本帧过线人数 − 上帧过线人数(进入为 +,离开为 −)。若该相机算法返回reset_triggered信号(底层刚被清零),则把基线last直接同步为当前值、delta 置 0,屏蔽清零假增量。 - 总人数更新策略(房间级):
- 先取”各相机过线人数之和”作为当前测量值;
- 若测量值 > 当前总人数 → 直接用较大值覆盖(向上跟随,防止漏检导致的低估);
- 否则总人数 += 各相机 delta 之和(差分维持,人员离场时靠负增量下降);
- 结果钳制不小于 0。
- 检测点选取与随机补点:将需要的人数与真实检测点(含各相机)匹配——按世界 xy 距离 > 0.3 m 去重后逐个选取;点数不足时用各相机生成的随机网格点补足,直到与总人数一致,使上报的可见点数量等于房间人数。
(b) Cross_Line_Region_DetectionCounter —— 过线+区域融合式房间统计
在 (a) 的相机增量计算基础上增加区域交叉验证与超时归零:
- 额外累计各相机
region_counters(区域可见人数)之和; - 只要区域人数或过线人数 > 0,就认为”房内有人”,清零零人计时;全部为 0 时开始计时,连续为 0 满 60 秒则将房间总人数强制归 0(防累计误差残留);
- 总人数更新:区域人数和作为”较大值跟随”的依据,否则叠加过线增量(同 (a))。
(c) RegionCounter —— 众数平滑式房间统计(用于纯区域算法)
- 每个房间维护一个最近 20 帧的滑动窗口(deque,存每帧的区域人数与当时的检测点)。
- 对窗口内 20 个区域人数值取众数(出现最多的值;多个众数并列时取最小值)作为输出人数。
- 检测点则从窗口内由新到旧找到第一个”人数 = 众数”的那一帧,取该帧检测点作为输出,保证人数与点集合自洽。
- 目的:区域计数容易因遮挡/抖动单帧突变,取众数可平滑毛刺、稳定输出。
兜底方法 _default_count_people:当数据中取不到 room_id 时,直接用真实检测点数作为人数,不足 10 人时用随机点补到 10。
小结:三种统计思路对比
| 统计器 | 数据来源 | 上升方式 | 下降方式 | 抗漂移手段 |
|---|---|---|---|---|
| Cross_Line_DetectionCounter | 相机过线净人数 | 较大值覆盖 | 过线负增量 | 重置信号屏蔽假增量 |
| Cross_Line_Region_DetectionCounter | 过线 + 区域人数 | 区域较大值覆盖 | 过线负增量 | 全零 60s 归零 |
| RegionCounter | 区域可见人数 | 20 帧众数 | 众数自然下降 | 众数平滑 |
3.7 历史脚本(tongjibeifen.py)
早期备份版本:房间总人数 = 各相机 room_total_count(过线净人数)之和,负数钳 0,直接作为最终人数;无增量/重置/超时等容错,已不再使用。
四、计数引擎 people_counter_W.py
4.1 配置加载(ConfigLoader)
读取 JSON 配置文件,校验必须包含 rooms(房间→相机树)与 model_config(模型权重等)。test.json 为实际加载文件。
4.2 帧缓冲(FrameBuffer)
线程安全环形双缓冲(容量 2):采集线程 add_frame 覆盖写,消费方 get_latest_frame 只取最新一帧并标记已消费。避免推理积压导致画面越来越”旧”。
4.3 算法工厂(AlgorithmFactory)
按房间配置的算法 type 字符串,用 importlib 动态加载 algorithms.<type> 模块中同名大写类并实例化(如 cross_line_detection → Cross_line_detection)。新增算法只需在 algorithms 下加文件并在配置里写 type,无需改引擎代码。
4.4 相机采集线程(CameraCaptureThread)
- 每相机一个线程,
cv2.VideoCapture(FFMPEG 后端)拉取 RTSP 流,设置小缓冲、读/连接超时与硬件加速。 - 断线自愈:读帧失败连续多次即释放句柄,按 5 秒间隔自动重连。
- 跳帧降载:每 2 帧只存 1 帧(约降到 12.5 fps 入库),有效降低后端推理与带宽压力;统计实际存储帧率供日志监控。
4.5 相机推理管理器(CameraInferenceManager)——每相机一管推理流水线
- 每相机独立加载一个 YOLO 模型实例,对最新帧执行
model.track(classes=[0], conf=0.6, iou=0.6, persist=True, tracker="bytetrack.yaml"),一次调用同时完成检测与跟踪。 - 两级线程池异步流水线(共享引擎级线程池,避免每帧同步阻塞主线程):
tracking_executor提交跟踪;- 跟踪完成回调
_tracking_done_callback再把结果提交到processing_executor执行算法process()与draw(); - 结果写回
algorithm_detections / random_detections / algorithm_data(含 room_total_count、region_counters、reset_triggered 等)与显示缓冲。
- 单帧串行锁:每相机用
processing标志 + 锁保证同一时刻只处理一帧,天然削峰防积压。 - 提供查询接口给上层(get_detections / get_random_detections / get_algorithm_data / get_display_frame),并支持
reset_algorithm_state()(调用算法的 reset,用于系统级清零)。 - 停止时释放模型、清缓冲。
4.6 显示管理(DisplayManager)
以约 30 fps 频率轮询各相机的显示缓冲,用 cv2 窗口展示(每相机独立窗口,标题 房间/相机);无帧时显示”WAITING FOR FRAME”占位并复用缓存帧。
4.7 房间人数总控(RoomPeopleCounter)
- 模型预热:启动时用 3 次纯黑 dummy 帧跑一遍 track,完成 CUDA 图/算子预热并清显存,避免首帧卡顿。
- 线程池规模:跟踪池 = 2×CPU 核数,处理池与房间计算池 = CPU 核数。
- 相机初始化:遍历配置
rooms,为每个房间的每台相机创建采集线程 + 推理管理器(算法类型来自房间配置)。 - 房间统计入口
calculate_room_occupancy:将各房间提交到房间线程池并行计算(_process_room_occupancy),各房间内部先收集所有相机的算法数据,再按房间算法类型分发到 3.6 中对应的统计器,得到 (房间人数, 检测点集合, 相机数据);主线程对每个房间计算与上一轮的人数差分change_count/change_status(进 +1 / 出 0),汇总成整体 JSON。 - 内外人数比对校正
people_compare:rooms.total(全厂汇总房间,代码称”外部”)与各普通房间之和(“内部”)分别统计;若内部 > 外部,说明某房间多算了人,则按三重排序(track_id 降序 → camera 编号降序 → room 编号降序)优先删除”新出现的高 ID 目标”,直到内部人数 = 外部人数,并把总人数修正为外部值。防止跟踪碎片导致虚增。 - 全系统零人重置
_check_and_reset_if_needed:当内部总人数持续为 0 达阈值(默认 3600 秒)时调用reset_system_counters(),对所有相机的算法、上一帧人数基线与三个统计器统一清零,保证次日/长时间空场后计数不残留。
五、主程序 main_z_chuangkou_image1.py
5.1 运行线程(PeopleCounterThread,QThread)
- 启动顺序固定:HTTP 图片服务 → 计数引擎 RoomPeopleCounter(含取流与推理线程)→ DataProcessor → MQTTPublisher,保证图片可访问后再开始拍照与上报。
- 主循环每 10 ms 遍历所有相机的推理管理器执行
update()(触发跟踪-处理流水线);画面以 0.1 s 为间隔通过 Qt 信号frame_updated推给 UI,避免信号队列积压。 safe_cleanup反向安全停止:先停 MQTT → DataProcessor → RoomPeopleCounter → HTTP 服务,最后清 GPU 显存;配合cleanup_complete事件保证线程完全退出。
5.2 数据处理线程(DataProcessor._process_data_loop,每 0.1 s)
- 调
counter.calculate_room_occupancy()+people_compare()取房间数据;随后做每日清理检查(按 90 天滚动删除日期目录与入口照片)。 - 超员判定:总人数 > 容量上限(来自配置文件或 MQTT 动态修改,线程安全读写)后开始计时,连续超限 ≥ 1 秒才置
factory_error_flag=true(报警带 1 秒去抖);人数回落即解除并记录恢复时刻。 - 位置数据组包:把每个检测点映射为
user_id = G0519_{room}_{camera}_usr{track_id},携带当前世界坐标 (x,y)(保留 2 位小数)与上一次坐标(remain,用于前端画移动轨迹/区分静止人),实现”逐人逐点上报”。 - 拍照标志置位:超限期间按
capture_interval(0.1 s)周期置need_capture(由发布线程消费);同时缓存各路相机最新帧到camera_frame_cache(拍照时优先取缓存,避免阻塞推理线程)。 - 组装
full_data(timestamp / workshop / capacity / user_count / error_flag / rooms 明细 / capture_ready / entrance_image_urls),写入加锁共享的最新数据区。
5.3 拍照取证系统
- 普通房间拍照
capture_images:取最新数据找出user_count>0的房间,逐个房间调用capture_room_all_cameras——对房间内每台相机,帧来源优先级:帧缓存 → 推理管理器显示缓冲 → 重试接口(最多 3 次);成功后经generate_image写入images/YYYYMMDD/room_cam_时间戳.jpg并生成可访问 URL,存入该房间room_last_image_urls供 MQTT 挂载。capture_ready标志标记本次拍照动作完成。 - 入口摄像头拍照
capture_entrance_cameras:对象为配置rooms.total.cameras(门口机),帧同样优先缓存、其次重试;图片写入独立目录images/entrance_cameras/total_cam_时间戳.jpg,URL 挂入entrance_last_image_urls。 - 触发策略(数量变化触发,非周期连拍):
_process_data_loop检测到总人数与上轮不同,且处于”超员相关状态”(当前超员,或刚从超员恢复且在 10 秒恢复窗口内)时置entrance_need_capture;由发布线程消费执行拍照。正常进出(0→1、1→2)不拍,避免磁盘膨胀。
5.4 MQTT 发布器(MQTTPublisher)
- 每 3 秒向话题
room发布 JSON(发布前先消费两个拍照标志:超员周期拍照与入口人数变化拍照,拍完重取一次最新数据,保证照片 URL 随当次上报)。 - 订阅
model_room:收到{"value": N}即在线更新容量上限,无需重启。 - 发布后记录每个 user_id 的当前坐标作为下次的 remain。
5.5 HTTP 图片服务(HTTPServerManager)
基于 ThreadingHTTPServer + SimpleHTTPRequestHandler 提供图片静态访问,根目录指向 images/;端口 8080 起自动探测空闲端口(绑定成功即用),URL 形如 http://IP:PORT/YYYYMMDD/xxx.jpg 与 /entrance_cameras/xxx.jpg。
5.6 PyQt5 界面(MainWindow / RoomWidget / CameraDisplayWindow)
- 主窗口含启动/停止按钮、状态栏、各房间人数卡片(RoomWidget 展示房间名/人数/容量/超限红色告警 UI)、相机画面缩略与弹窗(CameraDisplayWindow 每 0.1 s 收 frame_updated 信号刷新)。
- 支持”自动模式”:启动后不经点击直接进入运行;通过定时器每秒监控线程状态,异常时提示并在主线程中安全重启。
六、关键运行参数
| 参数 | 位置 | 值 | 含义 |
|---|---|---|---|
capacity | 配置文件顶层 | 1 | 全厂容量上限(超员报警阈值) |
over_capacity_duration | DataProcessor | 1 s | 连续超限去抖时长 |
capture_interval | DataProcessor | 0.1 s | 超员期间普通房间拍照最小间隔 |
entrance_recover_send_window | DataProcessor | 10 s | 恢复后入口照片仍下发窗口 |
publish_interval | MQTTPublisher | 3 s | 发布周期(含拍照消费周期) |
conf / iou | CameraInferenceManager | 0.6 / 0.6 | YOLO 检测置信度与 IoU |
zero_count_threshold | RoomPeopleCounter | 3600 s | 全零触发系统重置阈值 |
| 区域零人复位时长 | cross_line_region | 30 s | 底层过线计数自复位 |
| 房间统计零人归零 | room_statistics | 60 s | 过线+区域统计器归零等待 |
| 众数窗口 | RegionCounter | 20 帧 | 区域人数平滑窗口 |
| 随机补点网格 | 跨线类算法 | 3×3 / 间距0.5m | 补足检测点与人数一致 |
| HTTP 端口 | HTTPServerManager | 8080+ | 图片服务(自动找空闲) |
| MQTT | MQTTPublisher | 1883 / 话题 room、model_room | 数据上报与容量下发 |
七、设计要点与容错机制汇总
- 双向计数:跨线差分天然支持”进加出减”,配合较大值覆盖避免检测漏帧导致的低估。
- 两级抗漂移:底层相机级”区域 0 人超时自复位”,顶层房间级”全零 3600 s 系统重置”,双保险防止长跑后计数残留。
- 重置信令同步:清零瞬间通过连续多帧
reset_triggered信号通知统计器同步基线、屏蔽假增量,这是本系统人数不跳变的关键设计。 - 点补足机制:检测点不足时用固定网格随机点补齐,保证”人数 = 可见点数”的自洽性,便于前端展示。
- 异步流水线 + 单帧锁:跟踪与后处理分属两个线程池,每相机单帧串行,兼顾吞吐与有序性。
- 内外比对校正:以全厂汇总(外部)为准修剪内部多余点,抑制跟踪碎片导致的虚增。
- 拍照降频:入口拍照由”周期连拍”改为”人数变化 + 超员相关状态”触发,避免磁盘膨胀;90 天自动清理兜底。