ARM平台室内人群检测与人流进出驱动开发实战

发布时间:2026/10/15 9:54:45
ARM平台室内人群检测与人流进出驱动开发实战 1. 从一个真实需求说起为什么要在ARM上自己写人流检测驱动去年帮一个做智能楼宇的朋友处理过一件事他们在一栋写字楼的几个主要出入口装了摄像头想统计每天进出的人数用来优化空调和新风系统的运行策略。一开始他们买的是市面上现成的客流统计盒子用了一段时间发现两个问题——一是数据只能在他们自己的云平台上查没法直接对接楼宇自控系统二是每个门口都要单独布一根网线加一个盒子成本高不说后期维护也麻烦。后来他们的想法就变成了能不能把摄像头直接接到一块ARM开发板上在板子上跑检测算法然后通过RS485或者Modbus把人数数据直接给到楼宇控制器。这样一来每个门口只需要一块板子加一个摄像头数据本地处理完直接输出不依赖外部网络也不用担心数据隐私问题。这个需求其实很有代表性。现在做边缘计算的人越来越多ARM板子的算力也上来了把视觉检测放到端侧跑完全可行。但问题在于市面上讲算法训练和模型部署的内容很多讲“驱动层怎么把摄像头数据接进来、怎么把检测结果送出去”的内容却很少。很多人卡在驱动这一层不知道从哪下手。这篇文章就围绕“在ARM平台上自己写室内人群检测驱动和人流进出检测驱动”这件事把从硬件选型、驱动架构设计、数据流打通到应用层对接的完整链路讲清楚。不管你是做嵌入式开发的还是做智能楼宇、安防、零售分析的只要涉及到端侧人流统计这篇内容都能给你一个可参考的框架。2. 先搞清楚要做什么室内人群检测与人流进出检测的区别很多人把这两个概念混在一起觉得都是数人头没什么区别。实际上它们在驱动层和应用层的设计思路上差别很大如果一开始没分清楚后面代码写到一半发现架构不对返工成本很高。2.1 室内人群检测到底在检测什么室内人群检测的核心目标是回答“现在这个空间里有多少人”。它关注的是当前时刻的静态人数不关心这些人是从哪来的、往哪去的。典型的应用场景包括会议室人数统计、商场某个区域的热力分析、图书馆座位占用率监测等。从驱动层的角度看室内人群检测对数据的要求是“全帧检测”。也就是说每一帧图像进来你都需要对整幅画面做一次推理输出所有人的位置框然后统计数量。这种模式下驱动需要保证的是帧率的稳定性和推理的实时性。如果帧率波动太大人数统计就会出现跳变比如上一秒显示15人下一秒变成8人再下一秒又变成14人这种数据给到上层应用是没法用的。另外室内人群检测通常不需要很高的时间分辨率。每秒处理1到2帧就足够了因为室内人数变化不会那么快。这就给驱动设计留出了优化空间——你可以用较低的帧率换取更高的推理精度或者用更大的模型来提升检测效果。2.2 人流进出检测的核心是“方向”和“计数”人流进出检测要回答的问题完全不同“有多少人进来了有多少人出去了”。它关注的是动态的穿越事件而不是静态的人数。这就意味着驱动层不仅要检测到人还要跟踪人的运动轨迹判断运动方向并且在人跨越某条虚拟线的时候触发计数事件。这个“虚拟线”在行业里通常叫“绊线”或者“计数线”。驱动需要维护每条绊线的状态记录每个人在连续帧中的位置变化当检测到某个人从线的一侧移动到另一侧时就产生一个进出事件。这里有个很容易踩的坑如果只是简单地判断“人出现在线的上方还是下方”那当一个人在线附近来回走动的时候就会产生大量的误计数。正确的做法是需要做轨迹跟踪确认这个人确实是“穿过”了线而不是“徘徊”在线附近。这就涉及到跟踪算法的选择和多帧状态机的设计后面会详细讲。2.3 两种模式对驱动架构的不同要求把这两种需求放在一起对比驱动层的设计差异就很明显了对比维度室内人群检测人流进出检测核心输出当前人数进出事件计数数据处理方式单帧独立推理多帧跟踪状态判断帧率要求低1-2fps即可较高至少5fps保证跟踪连续性驱动状态维护无状态或简单缓存需要维护跟踪器和绊线状态对延迟的敏感度低高延迟会导致轨迹断裂典型输出接口定时上报人数事件触发上报实际项目中这两种模式经常需要同时存在。比如一个门口的设备既要统计当前门口区域有多少人等待又要统计进出人数。所以驱动架构最好从一开始就设计成支持两种模式的而不是后面再打补丁。3. ARM平台上的驱动架构怎么搭在ARM上写驱动跟在x86上写应用有本质区别。ARM平台的资源受限外设接口也跟PC不一样很多在PC上理所当然的事情在ARM上需要重新考虑。3.1 硬件选型摄像头接口和算力怎么平衡先说摄像头接口。ARM开发板上常见的摄像头接口有几种MIPI CSI、USB、DVP并口。MIPI CSI是首选因为它是专门为图像传输设计的带宽高、延迟低、CPU占用少。USB摄像头虽然方便但USB协议栈的开销比较大而且带宽共享多路摄像头的时候容易出问题。DVP并口现在用得少了主要是引脚太多布线麻烦。选摄像头的时候要注意一个参数输出格式。有些摄像头只输出MJPEG或者H.264压缩流这种适合做视频录制但做视觉检测需要先解码会增加CPU负担。最好选支持RAW Bayer或者YUV输出的摄像头驱动拿到数据后可以直接送给NPU或者GPU做推理省去解码步骤。算力方面现在主流的ARM板子都会带一个NPU或者GPU。NPU适合跑量化后的检测模型功耗低、效率高。GPU通用性更强但功耗也更高。如果只是做人流检测这种相对简单的任务一块带1TOPS左右算力的NPU就够用了。模型方面YOLO系列的小模型或者MobileNet-SSD都是不错的选择量化到INT8之后在NPU上跑单帧推理时间可以控制在20毫秒以内。内存也很关键。1080P的RGB图像一帧就是6MB左右如果驱动里要做多帧缓存和跟踪至少需要预留几十MB的内存空间。有些板子内存只有512MB跑Linux系统加上应用之后剩不了多少这种就要谨慎设计缓存策略。3.2 驱动分层从V4L2到应用接口的完整链路在Linux系统上摄像头驱动通常走V4L2框架。V4L2是Linux内核里的一套视频采集API它把摄像头的操作抽象成了标准的ioctl调用。应用层通过打开/dev/videoX设备节点设置格式、申请缓冲区、启动流然后就可以从缓冲区里读帧数据了。但V4L2只是最底层的数据获取。对于人流检测这个场景我们需要在V4L2之上再封装一层把原始帧数据变成检测结果。这一层我通常叫它“检测驱动层”它负责的事情包括从V4L2缓冲区取帧做必要的预处理缩放、格式转换、去噪把预处理后的帧送给推理引擎获取检测框对检测框做后处理NMS、置信度过滤如果是人流进出检测还要把检测框喂给跟踪器维护轨迹把结果通过一个统一的接口暴露给应用层这个分层的好处是应用层不需要关心底层用的是哪个摄像头、哪个推理引擎只需要调用“获取当前人数”或者“注册进出事件回调”这样的接口就行了。后面如果要换摄像头或者换模型只需要改检测驱动层应用层代码不用动。3.3 数据流设计帧缓冲、推理队列和结果回调数据流的设计直接决定了系统的稳定性和实时性。我见过不少项目算法本身没问题但数据流没设计好跑一段时间就卡死或者丢帧。一个比较稳妥的数据流是这样的摄像头通过DMA把帧数据写入内核缓冲区V4L2驱动维护一个缓冲区队列。检测驱动层从队列里取帧取到之后立即把缓冲区重新入队这样摄像头可以继续写下一帧不会被阻塞。取到的帧数据拷贝到用户空间的帧池里帧池是一个环形缓冲区大小根据内存情况设定一般8到16帧就够了。推理线程从帧池里取帧送给NPU做推理。推理是异步的提交任务之后不等待结果而是继续取下一帧。NPU完成推理后会通过中断或者轮询的方式通知结果就绪这时候再从结果队列里取检测框。这里有个细节如果推理速度跟不上摄像头帧率帧池会慢慢被填满。这时候有两种策略——一种是丢帧直接丢弃最旧的帧保证处理的永远是最新数据另一种是降帧率通过V4L2的帧率控制让摄像头少出帧。对于人流检测来说丢帧是可以接受的因为人的移动速度不会那么快丢几帧不影响跟踪。但如果是做高速运动分析就不能随便丢帧了。结果回调这块我建议用观察者模式。应用层注册回调函数检测驱动层在产生结果后调用回调。回调里不要做耗时操作否则会阻塞检测线程。如果需要做复杂处理可以在回调里把数据扔到另一个队列由应用层自己的线程去消费。4. 人流进出检测的核心跟踪与绊线逻辑人流进出检测的难点不在检测本身而在跟踪和绊线判断。检测模型只能告诉你每一帧里人在哪但不知道谁是谁也不知道运动方向。这部分逻辑需要驱动层自己实现。4.1 为什么不能只用检测框做计数最简单的想法是在画面中间画一条水平线检测框的中心点从线上方变到线下方就计一次“进入”。这个逻辑听起来没问题但实际跑起来会发现误计数非常多。原因有几个第一人在线附近走动的时候检测框的中心点可能会反复穿越线导致一次真实穿越被计成多次。第二如果两个人并排走检测框可能会合并或者交换导致计数混乱。第三检测本身有抖动同一帧里同一个人的框可能忽大忽小中心点位置也会跳变。所以必须引入跟踪。跟踪的作用是给每个人分配一个唯一的ID并且在连续帧中维护这个ID的位置历史。有了轨迹之后再判断轨迹是否穿越了绊线就可靠多了。4.2 轻量级跟踪算法的选择与调参在ARM上跑跟踪不能选太重的算法。像DeepSORT这种带ReID特征的跟踪器虽然效果好但计算量太大在端侧跑不动。比较合适的是基于运动预测的轻量跟踪比如SORT或者ByteTrack的简化版。SORT的核心是卡尔曼滤波加匈牙利匹配。卡尔曼滤波根据上一帧的位置和速度预测当前帧的位置匈牙利算法把预测位置和当前帧的检测框做匹配。匹配上的就更新轨迹没匹配上的检测框新建轨迹连续多帧没匹配上的轨迹就删除。在ARM上实现的时候有几个参数需要仔细调最大丢失帧数一个轨迹连续多少帧没有匹配到检测框就删除。设太小了人一被遮挡轨迹就断了设太大了人已经离开了画面轨迹还留着会产生幽灵计数。一般设5到10帧比较合适。IOU匹配阈值预测框和检测框的重叠度超过多少才算匹配。设太高了容易匹配失败设太低了容易匹配错。0.3左右是个比较平衡的值。卡尔曼滤波的过程噪声和观测噪声这两个参数决定了滤波器对预测和观测的信任程度。人的运动不是匀速的过程噪声要设大一点让滤波器更信任观测值。如果算力允许可以在SORT的基础上加一个简单的外观特征匹配比如用颜色直方图。这样在两个人交叉的时候不容易把ID搞混。颜色直方图的计算量很小在ARM上完全可以承受。4.3 绊线状态机的设计细节有了轨迹之后绊线判断就变成了一个状态机的问题。每条绊线对每个轨迹维护一个状态状态包括在线的哪一侧、是否正在穿越、穿越方向等。我通常会用这样的状态设计状态A轨迹在线的正侧。如果轨迹的上一帧位置在正侧当前帧位置在负侧并且位移超过一个最小阈值就触发“正向穿越”事件。状态B轨迹在线的负侧。同理反向穿越触发“负向穿越”事件。状态C轨迹在线附近。如果轨迹位置在线的缓冲区内比如线上下各20像素不触发事件但记录轨迹的移动方向。这个最小阈值很重要。如果没有它人在线附近轻微晃动就会触发计数。阈值的大小要根据画面分辨率和人的移动速度来定。1080P画面下20到30像素是个比较合理的值。还有一个细节绊线不一定是水平或垂直的可能是斜线。这时候判断“在线的哪一侧”就需要用向量叉积来计算。假设绊线由点P1和P2定义轨迹点为Q那么叉积(P2-P1)×(Q-P1)的符号就表示Q在线的哪一侧。这个方法对任意方向的线都适用。4.4 多人交叉和遮挡的处理经验多人交叉是人流检测里最头疼的问题。两个人迎面走来在画面里重叠在一起检测框合并成一个跟踪器就懵了——到底是同一个人还是两个人我的经验是在检测后处理阶段就要做判断。如果检测框的宽高比突然变大比如从0.4变成0.8很可能是两个人合并了。这时候可以根据框的宽度和之前轨迹的宽度做对比如果宽度明显增加就尝试把框拆成两个。拆分的位置可以根据轨迹的预测位置来定。遮挡的处理更复杂。如果一个人被另一个人完全挡住检测框会消失跟踪器只能靠预测来维持轨迹。这时候卡尔曼滤波的预测就很重要了。如果遮挡时间短几帧之内预测位置还能跟得上轨迹不会断。如果遮挡时间长预测位置会漂移等目标重新出现的时候可能匹配不上。一个实用的技巧是在遮挡期间不要更新卡尔曼滤波器的观测值只做预测。同时把轨迹的“丢失计数”加一但不要立即删除。等目标重新出现后用较大的匹配阈值去关联提高重新匹配的成功率。5. 从驱动到应用接口设计与数据上报驱动写好了最终是要给应用用的。接口设计得好不好直接决定了应用开发的效率和系统的可维护性。5.1 驱动对应用暴露哪些接口我一般会暴露三类接口第一类是配置接口。应用可以通过这些接口设置检测区域、绊线位置、灵敏度等参数。比如set_detection_roi(x, y, w, h)用来限定只检测画面中的某个区域add_tripwire(id, x1, y1, x2, y2)用来添加一条绊线。第二类是查询接口。应用可以随时查询当前人数、历史计数等。比如get_current_count()返回当前画面中的人数get_flow_count(direction)返回指定方向的累计计数。第三类是事件接口。应用注册回调函数当有进出事件发生时驱动调用回调通知应用。回调的参数包括事件类型进入/离开、时间戳、轨迹ID等。这些接口最好用C语言定义因为C接口最通用上层用C、Python或者Go都能调用。如果驱动是跑在Linux上可以把接口封装成字符设备或者sysfs节点应用通过文件操作来调用。也可以做成共享库应用直接链接调用。两种方式各有优劣字符设备更符合Linux驱动规范但调试麻烦共享库调用方便但耦合度高。我一般会做成共享库加一个可选的字符设备接口灵活一点。5.2 数据上报协议的选择MQTT、Modbus还是自定义数据上报协议的选择取决于上层系统是什么。如果是对接楼宇自控系统Modbus RTU或者Modbus TCP是最常见的。Modbus的优点是简单、稳定、几乎所有楼宇控制器都支持。缺点是他只能传寄存器数据表达能力有限。可以把人数放在保持寄存器里应用层轮询读取。如果是对接云平台或者本地服务器MQTT更合适。MQTT是发布订阅模式驱动把检测结果发布到某个主题服务器订阅这个主题就能收到数据。MQTT还支持QoS等级可以保证消息不丢。在ARM上跑MQTT客户端用mosquitto或者paho-mqtt库都很方便。如果上层系统是自定义的那也可以直接用TCP socket传JSON。这种方式最灵活但需要自己处理粘包、断线重连等问题。我一般会定义一个简单的协议4字节长度头加JSON体这样解析起来不容易出错。不管选哪种协议都建议在驱动层做一个抽象。定义一个report_interface结构体里面包含init、send、deinit三个函数指针。具体用Modbus还是MQTT在初始化的时候指定就行了。这样换协议的时候不用改驱动核心代码。5.3 本地缓存与断网续传的实现在实际部署中网络不是永远可靠的。如果网络断了检测数据不能丢需要先缓存在本地等网络恢复后再补传。本地缓存可以用SQLite或者简单的文件队列。SQLite的好处是支持事务写入可靠查询方便。缺点是写入速度相对慢一点但对于人流检测这种低频写入每秒最多几条记录来说完全够用。缓存的策略是检测到事件后先写入本地数据库然后尝试上报。上报成功就标记为已上报上报失败就留在数据库里。后台起一个线程定期扫描未上报的记录尝试重新上报。数据库不能无限增长需要设置一个上限比如最多保留7天的数据超过就删除最旧的。这里有个细节时间戳一定要用UTC时间并且记录时区偏移。否则设备部署在不同时区的时候数据对不上。另外设备重启后要能恢复未上报的数据所以数据库要存在非易失性存储上不能放在tmpfs里。6. 实测中遇到的坑和优化技巧理论和架构讲完了下面说几个实际调试中遇到的问题和解决办法。这些内容在官方文档里基本找不到都是踩坑踩出来的。6.1 摄像头曝光和帧率不稳导致检测抖动最开始调试的时候发现人在画面里走动时检测框会忽大忽小有时候还会闪烁。排查了很久才发现是摄像头自动曝光的问题。室内光线变化的时候摄像头会自动调整曝光时间导致图像亮度突变检测模型对这种突变很敏感输出的框就不稳定。解决办法是关闭自动曝光改成手动曝光。如果光线变化确实很大可以用自动曝光但限制曝光时间的范围不要让它变化太剧烈。另外帧率也要固定不要用自动帧率。有些摄像头在光线暗的时候会自动降帧导致跟踪器的时间间隔不均匀卡尔曼滤波的预测就会出错。6.2 NPU推理延迟与帧率不匹配的解决NPU推理是异步的提交任务后需要等结果。如果推理时间比帧间隔还长就会出现帧堆积。比如摄像头出帧是30fps但NPU推理一帧需要50毫秒那每秒只能处理20帧剩下的10帧就堆在队列里。短时间堆积没关系但长时间堆积会导致延迟越来越大最后实时性完全丧失。解决办法是限制队列长度队列满了就丢最旧的帧。同时可以降低摄像头帧率比如降到15fps让推理和采集匹配上。如果还是跟不上就要考虑换更小的模型或者降低输入分辨率。我实测下来YOLOv5n量化到INT8输入320x320在1TOPS的NPU上跑单帧推理大概15毫秒加上前后处理总共25毫秒左右。这个速度处理15fps的摄像头绰绰有余。6.3 内存泄漏的排查过程跑长时间测试的时候发现系统内存会慢慢减少跑一天下来少了100多MB。这种内存泄漏在嵌入式系统上很致命时间长了系统就会OOM。排查内存泄漏可以用valgrind但在ARM上跑valgrind比较慢。更实用的方法是在代码里加内存统计定期打印各个模块的内存使用量。我一般会在帧池、轨迹列表、结果队列这几个地方加统计点看哪个模块的内存在增长。那次最后发现是轨迹删除的时候没有释放轨迹关联的外观特征缓冲区。每次新建轨迹都会分配一块内存存颜色直方图但删除轨迹的时候忘了释放。这种问题很隐蔽因为轨迹数量不多的时候看不出来只有长时间运行、轨迹频繁创建删除的时候才会暴露。6.4 长时间运行的稳定性优化除了内存泄漏长时间运行还会遇到其他稳定性问题。比如文件描述符泄漏、线程死锁、看门狗复位等。文件描述符泄漏通常是因为打开了设备或者文件没有关闭。在Linux上可以用ls /proc/pid/fd来查看进程打开了多少文件描述符如果数量持续增长就有问题。线程死锁一般是因为多个线程竞争同一把锁或者锁的顺序不一致。避免死锁的方法是尽量用无锁数据结构或者确保所有线程按相同的顺序获取锁。在检测驱动里帧池和结果队列可以用无锁环形缓冲区来实现避免锁竞争。看门狗复位是因为主线程卡住了没有及时喂狗。可以在主循环里加一个心跳检测如果某个线程超过一定时间没有响应就记录日志并尝试恢复。如果恢复不了就让看门狗复位系统至少能自动恢复。7. 这套方案还能怎么扩展这套驱动架构不只可以用在人流检测上稍微改一改就能支持其他场景。比如做区域入侵检测只需要把绊线逻辑换成区域判断——检测框中心点是否进入某个多边形区域。做停留时间分析可以在跟踪器里记录每个轨迹的存活时间超过阈值就触发事件。做人群密度估计可以把检测框的数量和画面面积做比值输出密度等级。如果算力允许还可以在检测的基础上加属性识别比如判断人的性别、年龄、是否戴帽子等。这些属性可以通过一个轻量级的分类网络来实现在检测框的基础上裁剪出人体区域送给分类网络推理。属性数据对于零售分析、安防布控等场景很有价值。另外多设备协同也是一个方向。多个门口的检测驱动可以通过局域网互相通信做跨镜头的跟踪。比如一个人在A门口进入在B门口离开系统可以关联这两条记录得到更完整的轨迹。这需要在驱动层之上再加一层协同层实现设备间的数据同步和轨迹关联。我在实际项目里还遇到过一个需求把检测结果实时叠加到视频画面上输出一路带标注的视频流。这个用GStreamer或者FFmpeg都可以实现在驱动层输出检测框的同时把框画到帧数据上然后编码输出RTSP流。这样用户在监控画面上就能直接看到检测效果调试的时候特别方便。最后说一个个人体会在ARM上做视觉检测驱动最难的不是算法而是资源管理和稳定性。算法可以调参、可以换模型但内存泄漏、线程死锁、帧率不稳这些问题一旦出现就很难排查。所以从一开始就要把资源管理做好该加的统计要加该做的测试要做。我一般会在开发阶段就跑72小时连续测试确保没有内存增长和性能下降然后再部署到现场。现场环境比实验室复杂得多温度、供电、网络都可能出问题只有驱动本身足够稳定才能扛得住这些外部因素。