
最近有个做零售的朋友找我说门店每天想在出入口数一下客流好算转化率。我当时第一个想到的就是用Jetson NANO来搭这套访客计数系统。这个板子虽然型号老了一点但做边缘端的视频流目标检测和统计性价比是真的高一张开发板加一个普通USB摄像头就能把活干完。整套系统不光能数人数还能区分进出方向、去掉重复计数、把数据存成报表甚至能在本地开个网页实时看曲线。这篇文章我就把整个方案从头到尾拆开讲包括硬件选型、环境部署、YOLOv5检测、跟踪去重、虚拟线计数逻辑还有我在实际调试中踩过的坑。适合想入门嵌入式AI、做智能零售、或者打算在低成本设备上落地轻量视觉应用的朋友参考。1. 系统整体设计与方案选型1.1 需求拆解不只是数人头访客统计看起来简单就是“摄像头对着门口画面里进来一个数就加一”但实际跑到现场会发现需求要比这句话复杂很多。首先你要回答几个问题是只统计进店人数还是进出都要统计是否需要区分“一个人反复在门口逗留”导致的重复计数检测到的是真人还是屏幕上的人影、推车上的模特数据要实时看还是每天汇总如果同一个顾客在门口踱步三分钟再走进来系统应该计几次如果没有一个清晰的业务规则后面算法写得再好看也没用。我这边最终定的需求是单入口门框区域统计进入和走出两个方向的人数去除在门口犹豫、徘徊、路过等无效目标识别范围只覆盖门口地面和门框附近区域不抓全店画面数据需要实时上传到本地网页并且每隔10分钟自动落盘一份CSV日志。把这些规则理清楚后整个系统的技术路线也就清楚了视频流输入 - 行人检测 - 多目标跟踪 - 进出方向判断 - 计数去重 - 展示和存储。1.2 为什么选Jetson NANO而不是树莓派或云服务器这套方案以前最常见的实现方式有两个一个是用树莓派加普通Opencv做背景差分但稍微复杂一点的光线变化、人员重叠就会把准确率拉到没法看的程度另一个是把视频传上云用云端GPU跑目标检测但门店带宽不一定撑得住而且每路摄像头持续推流流量费用很快会超过设备本身。Jetson NANO正好卡在中间。它自带128核心的Maxwell GPU能够本地跑CUDA加速的深度学习模型不用把视频传出去延迟和隐私都有保障。对比树莓派4BNANO在推理性能上有明显优势同样是跑YOLOv5s树莓派CPU很难达到实时处理但NANO配合TensorRT能轻松跑到25到30FPS。对比云服务器它是一次性硬件投入没有后续流量费断电重启后能自动恢复运行非常适合长期摆在线下门店。当然它的短板也很明显4GB内存对现在的模型动辄几百MB权重来说确实紧张而且CPU性能一般很多图像预处理操作不能全指望CPU。但这些通过合理的模型选择和推理引擎优化都是可以接受的。这套方案换到Jetson Orin NANO上也能无缝跑而且还能留出余量给大模型部署后面我会讲扩展方向。1.3 整体架构与数据流整个系统的数据流可以分成五段图像采集USB摄像头或CSI摄像头分辨率我建议720P或1080P帧率按实际检测速度来不用无脑拉高。预处理把摄像头帧从BGR转RGB缩放到模型输入尺寸做归一化。目标检测用YOLOv5或者自己训练的行人检测模型输出每个目标的边界框和置信度。多目标跟踪把相邻帧的检测框关联起来形成稳定轨迹这样同一个目标不会每帧都重复数一次。计数决策根据轨迹穿过虚拟线的方向判断朝内还是朝外进入“待确认”状态轨迹消失或越过第二条线时提交计数。其中最关键的是第4和第5步检测模型选得再猛如果没有跟踪和去重逻辑统计出来的人数一定是虚高的。1.4 硬件清单与选型要点以下是我这套系统实际用到的硬件以及选型时的注意事项部件型号建议备注主控板Jetson NANO 4GB B01版本性能比早期A02强一些内存必须4GB版摄像头USB免驱摄像头支持720P/30FPS优先选全局快门型号避免运动拖影存储64GB以上TF卡或USB3.0 U盘系统镜像加模型加日志空间至少要留够电源适配5V 4A DC电源或PoE分离线NANO对供电非常敏感供电不足是各种奇怪故障的元凶散热主动风扇加铝制散热片满载时芯片温度会飙到85度必须强制风冷网络有线网口优先无线网卡也能用但长期运行稳定性不如有线摄像头的位置最好装在门正上方或者侧上角能看到人的头顶和肩膀这样YOLOv5的检测框和人体轮廓会比较稳定。如果装成水平视角人脸占比大反倒会增加检测难度。2. 环境搭建与核心依赖部署2.1 JetPack版本选择与系统烧录Jetson NANO的系统不像树莓派那样随便装一个桌面版就行它需要NVIDIA官方提供的JetPack里面包含了Linux系统、CUDA、cuDNN、TensorRT这些基础组件。我这里用的是JetPack 4.6.1对应的Ubuntu版本是18.04CUDA版本是10.2TensorRT是8.2.1。这个版本组合经过很多项目验证踩坑最少。烧录方式有两种。最简单的是用SDK Manager通过PCUbuntu主机给NANO刷机等于用USB线把PC和NANO连起来SDK Manager会把系统直接写入TF卡或者eMMC模块。另一种是直接下载官方预制作的SD卡镜像用Etcher写入TF卡。我更推荐后者因为不需要专门一台Ubuntu电脑Windows下也能搞定。烧录完成后开机进入系统先在终端里确认一下基础环境版本nvcc --version tensorrt --version ls /usr/lib/aarch64-linux-gnu/libcudnn*如果这几个都能看到版本号说明JetPack被正确刷进去了。然后设置好用户密码、网络和远程连接。2.2 基础依赖与Python环境准备NANO默认识别的是GPU跑CUDA但对很多人来说第一步先是用Python写代码所以得先把Python环境弄好。JetPack 4.6自带的Python3.6版本比较老但配合PyTorch官方提供的aarch64轮子跑YOLOv5是没问题的。先更新系统并安装常用工具sudo apt update sudo apt install -y python3-pip python3-dev python3-wheel git cmake libopenblas-dev然后安装PyTorch。这一步不用去PyPI普通源装因为在aarch64架构下官方源里很多包没有对应版本最好用NVIDIA论坛发布的PyTorch 1.9.0 for JetPack 4.6pip3 install torch1.9.0 torchvision0.10.0装完验证一下GPU是否识别python3 -c import torch; print(torch.cuda.is_available())如果输出True说明PyTorch已经把CUDA调用打通了。接着安装YOLOv5依赖在YOLOv5源码目录下执行pip3 install -r requirements.txt不过这里有个容易出问题的地方requirements.txt里包含opencv-python这个包在aarch64下经常编译失败或安装很慢。可以先用apt装系统级OpenCV再从requirements里注释掉opencv那一行这样会省很多事sudo apt install -y python3-opencv2.3 YOLOv5模型准备与行人检测选择YOLOv5官方仓库默认的模型权重是COCO数据集训练的能检测80类物体其中第0类就是person所以直接用默认权重也能跑行人检测。但直接用COCO权重有两个问题一是模型体积偏大YOLOv5s也有14MB左右综合速度虽然还能接受但精度和目标范围其实没必要那么广二是COCO对行人的检测在某些场景下容易漏检特别是只露出半个身体、坐着、遮挡严重的场景。建议的优化方式是只挑出person这一类用YOLOv5的自动锚框重训练脚本在你的门店门口视角下做一轮微调。如果不想采集数据那就直接使用默认YOLOv5s权重配合跟踪算法在中等密度的门店入口场景已经够用。我实际用的是YOLOv5s模型输入尺寸640x640confidence阈值0.25NMS阈值0.45。选择YOLOv5s而不是YOLOv5m或l的原因很直接NANO的GPU算力有限m和l在推理时FPS会掉到个位数影响实时性。如果确实想要更高精度但不想牺牲速度可以换YOLOv5n这是一个更轻量的版本速度更快但精度略低适合人流不密集的场景。2.4 TensorRT引擎转换这一步是让Jetson NANO跑得更快的核心。直接用PyTorch模型在GPU上推理YOLOv5s可能只有8到10FPS但做了TensorRT推理引擎之后速度能提升到25FPS以上。原理很简单TensorRT会分析网络结构把一些可以合并的层合并起来同时根据GPU的算力去选择最优的kernel实现推理阶段不再依赖PyTorch动态图。YOLOv5官方提供了从PyTorch权重导出TensorRT引擎的脚本最简单的方式是在YOLOv5目录下执行python3 export.py --weights yolov5s.pt --include engine --device 0导出后得到的是yolov5s.engine文件之后推理就不要再加载pt文件了直接加载engine。engine文件是和当前TensorRT版本、当前GPU架构强绑定的换一张Orin NANO或者升级TensorRT版本后原来的engine文件不能直接用必须重新生成一遍。导出过程中常见一个坑报错缺少库或者直接卡死。多半是因为TensorRT版本和PyTorch的安装顺序有问题重装一下pycuda就能解决pip3 install pycuda我建议在代码里动态加载engine并做一次较长的预热循环这样能避免第一帧推理特别慢的假象。3. 访客计数核心逻辑实现3.1 行人检测与跟踪YOLOv5 ByteTrack检测只能回答“这一帧画面里有哪些行人”但访客计数不能只看单帧因为一个人连续出现在100帧画面里如果每帧都检测到就累加人数会爆炸。必须把不同帧里的同一个目标关联起来这一块就是多目标跟踪。常用的算法有DeepSORT和ByteTrack。DeepSORT依赖ReID模型提取外观特征在密集人群场景下效果不错但多一个模型会增加NANO的负担。ByteTrack胜在简单高效它只用检测框之间的IoU关联配合卡尔曼滤波预测轨迹对大多数门店入口场景完全够用而且它在低算力设备上跑得很轻松。我这里采用TypeTrack的思路先跑YOLOv5拿到每一帧所有行人检测框然后输入ByteTrack维护每个轨迹的ID、状态和坐标历史。ByteTrack会返回当前帧所有目标的活动轨迹ID和更新后的坐标这样后续计数只需要基于轨迹ID做判断。ByteTrack的安装也比较轻量它依赖cython_bbox在Jetson上编译可能需要先设置环境变量export CFLAGS-fPIC pip3 install cython_bbox如果没有安装成功也可以用纯Python方式实现一个简单的IoU匹配跟踪器逻辑并不复杂只是官方实现做了很多优化。对访客计数来说一个能用卡尔曼滤波的基础Tracker足够了。3.2 虚拟线与区域计数逻辑计数规则怎么设计直接决定统计数据的可信度。我的方案里没有用复杂的“进出区域面积变化法”而是用了一条虚拟线加两个辅助框。具体来说在画面里设定一条水平的计数线比如画面高度y520像素。系统把轨迹的中心点坐标作为目标位置每当一个轨迹的中心点从线的上方移动到下方就判断为“进入”反过来从下方移动到上方就判断为“走出”。但直接按中心点跨线有个问题人从画面旁边路过可能只有一只脚跨过线或者人在线旁边晃两下中心点来回跳动就会误计。所以我加了两个辅助区域分别叫Enter Zone和Exit Zone它们分别是计数线上方和下方的窄条区域。只有当轨迹中心点在连续5帧以上都稳定处于Enter Zone或Exit Zone并且在某一帧发生跨线才认为是一次有效事件。用伪代码表示for track in tracks: cx, cy track.center # 判断当前坐标相对线的位置 if cy line_y: current_side in else: current_side out if track.last_side is None: track.last_side current_side if current_side ! track.last_side: # 如果跨越方向为 in - out表示走出 if track.last_side in: exit_count 1 else: enter_count 1 track.last_side current_side这样即使一个目标在线附近来回试探也只会在一段时间内记录一次有效跨线。3.3 去重与防抖处理在实际门店场景里最影响统计数据质量的就是“一个人反复进出”和“检测框闪烁”。先说检测框闪烁。当人站在门口时YOLOv5的检测框可能忽大忽小中心点会抖动导致跨线判断反复触发。解决方法是引入轨迹稳定性判定。一个目标从出现到消失至少要连续被跟踪3帧以上才参与计数判断如果中间有某一帧检测丢失但预测轨迹还在就继续保持状态不立刻删除轨迹。再说反复进出。有些顾客走到门口看了一眼又走了这在业务上不应该算作进店。我会设置一条“确认线”或者在进入方向后面再画一条更深的区域只有从外部区域连续进入内部区域且停留时间超过1秒才会计数。如果轨迹只是从线外到线内又马上折返则取消这次计数。具体实现可以用计时器每个轨迹维护一个状态变量初始是“未计数”当它跨过计数线进入店内区域后状态变为“待确认”记录当前时间戳如果这个轨迹在店内区域存在超过1秒就确认计数如果1秒内又回到店外就重置为“未计数”。这么做的逻辑是真正进店的客人不会只闪一下而路过、探头、犹豫的人会被过滤掉。经过这个逻辑后实测准确率比单纯跨线计数提升了差不多10个百分点。3.4 数据存储与Web展示访客数据除了实时看还要能追溯。我在系统里用了一套轻量方案数据维护在内存里每10分钟写一次CSV同时用Flask开一个本地网页访问Jetson的IP地址就能看到今天的进店数、出店数和实时画面。CSV的记录字段包括时间戳、进店数、出店数、当前在店人数、平均停留时长。当前在店人数不需要额外计算用“累计进入数 - 累计走出数”就行但要注意摄像头开机前已经在店内的人无法统计所以需要在每天运营开始时手动清零或者重启服务。Flask网页端我用的是Server-Sent Events方式把计数事件推送过去这样前端不需要轮询数字能实时跳。展示页面用简单HTML加一点CSS配合一张检测画面的MJPEG视频流可以在浏览器看到摄像头画面和画在画面上的人数框。这里有一个小技巧MJPEG流会消耗额外解码资源和网络带宽所以我的视频流分辨率故意降到960x540帧率限制在5FPS不然本地访问的人一多Jetson会卡成PPT。4. 性能调优与实测效果4.1 TensorRT加速与INT8量化前面已经提过导出TensorRT engine但真正把NANO的性能榨干还要做INT8量化。FP16推理其实已经比FP32快很多了但INT8能进一步减少内存占用和计算量。INT8量化需要准备一组代表性图片官方脚本里叫校准数据。校准数据最好从摄像头画面里选几百张包含人物不同姿势、不同光照的帧不要全是空背景。量化的过程其实是统计激活值的分布范围然后映射到8bit整数范围。如果校准数据不具代表性量化后精度可能掉得离谱比如漏掉穿黑色衣服的行人。我在NANO上用Pytorch转INT8 engine时实际流程是用YOLOv5的scripts里工具跑一遍量化校准。如果用TensorRT自带的工具链就是trtexec配合校准缓存trtexec --onnxyolov5s.onnx --saveEngineyolov5s_int8.engine --int8 --calibyolov5s_calibration.cache需要注意的是INT8量化后YOLOv5某些层精度损失比较敏感特别是小目标检测。如果发现行人的检测框变少或者置信度降得很凶可以退回到FP16精度有时候FP16的收益比INT8更稳。实测下来在我的NANO上FP16推理单帧耗时大约35毫秒INT8大约25毫秒换算成FPS分别是28和40。但帧率的提升并不是线性的因为图像采集、预处理、后处理和显示都要吃CPU和内存实际运行时会受到制约。4.2 散热与稳定运行Jetson NANO满载运行时的发热量完全不能小看。我不止一次见过有人把NANO放在弱电箱里跑了一个小时就开始掉帧、卡顿甚至自动关机。因为芯片温度超过85度后系统会主动降频保护推理速度直接掉一半。解决方法是上主动散热风扇并且风扇不是随便接个5V常转最好通过PWM控制根据CPU/GPU温度调整转速。Jetson自带的风扇控制接口可以直接用GPIO也可以买一个带调速模块的散热套件。还有一个容易被忽略的点供电。NANO如果通过Micro USB供电只有5V 2A的输入能力一旦算力拉满电流需求会超过4AMicro USB口很容易压降导致系统重启。我建议用DC电源适配器直接接在NANO的DC引脚上保证稳定5V 4A。实际部署时我把NANO放在门厅顶部的铝合金挡板上周围留出10厘米以上的散热空间夏天开一天温度稳定在70度上下FPS基本不掉。4.3 实测结果FPS与准确率我拿这套系统在一个5米宽的店铺门口测了两天摄像头角度是斜朝下45度光线以自然光为主偶尔有人打伞、推婴儿车。测试数据大致如下场景实际人数系统进店计数系统出店计数准确率单人进出47464697.9%双人并排进入23222395.6%门口停留后进入15131386.7%推婴儿车/抱小孩12101083.3%背光/逆光环境31282790.3%单人场景表现最好基本不会漏。双人并排偶尔会被漏掉一个因为两个目标重叠度太高检测框合并成了一个。推婴儿车时有时婴儿车被当成了人或者大人被当成非人物体漏检多一点。逆光环境下检测整体变弱我后面通过调曝光参数和改用带宽动态功能的摄像头后有所改善。综合下来误差率基本能控制在5%到15%之间对于活动客流趋势分析来说完全够用。4.4 不同场景的调参建议如果是更稀疏的办公室入口可以把检测置信度从0.25调到0.4减少误检人数统计会更干净如果是拥挤的商场出入口置信度调到0.15到0.2避免漏检但多目标跟踪的压力会变大需要缩短轨迹失效时间防止大量轨迹堆积。摄像头角度也影响很大。理想角度是俯视45度到60度这样人的头顶、肩膀能被完整看到检测框高度变化比较稳定跨线判断的误差小。如果摄像头水平正对门口人走近时检测框会突然变大中心点下降容易被误判为“走出”这就是俯视角度为什么重要的原因。灯光方面门口的灯最好装在摄像头侧后方避免直射镜头。如果避不开逆光就在代码里把图像预处理改成自适应直方图均衡化或者直接换一颗支持宽动态的摄像头效果提升非常明显。5. 常见问题与排查技巧实录5.1 Jetson NANO内存不足崩溃这个是我遇到最多的坑。Jetson NANO只有4GB内存而PyTorch加载模型、TensorRT推理、OpenCV图像缓冲区、多个Python进程加起来很容易把内存吃满。表现就是命令行直接卡死SSH连不上或者系统会自动杀掉Python进程。排查时先看内存占用free -h如果发现可用内存不足500MB就要开始瘦身了。常见的瘦身方法关掉桌面环境GNOME桌面会吃掉接近1GB内存我直接设置开机进入多用户命令行模式省出来的内存给推理用YOLOv5推理时限制GPU显存分配比例防止显存一次性占满不要再开多个摄像头流一个写入队列就够了。另外建议在启动脚本里加一个看门狗每隔30秒检测Python进程是不是还活着如果挂掉就自动重启。配合systemd服务可以实现无人值守。5.2 检测框乱跳与重复计数检测框乱跳通常有三个原因一是模型训练场景和实际场景差别大二是帧率太低目标在相邻两帧的位置变化太大跟踪跟不上三是后处理NMS阈值设置不合理导致同一目标产生多个重叠框。解决方法是先把FPS稳定在15以上否则跟踪效果很难做好。其次把ByteTrack中的框匹配IoU阈值调低一点比如从0.2调到0.1允许轨迹匹配到位置变化更大的检测框。还有就是检测置信度别调太低0.2到0.3之间比较好太低的话一个行人会被拆成两个框因为模型的局部置信度会产生多个peak。重复计数还跟轨迹生命周期有关。我把轨迹的最大丢失帧数从30调到了10这样目标短暂被遮挡后重新出现不会因为轨迹重新创建而重复计数。但代价是遮挡时间超过10帧的目标会被当成新目标例如并排人走过去互相挡了一下出现重复计数的概率会增加。这个需要按实际人流密度来平衡。5.3 夜间和背光场景识别不准夜间室外或者门内灯光强烈时YOLOv5会很吃力。我自己遇到的问题是晚上开灯后门口地面有白色高光行人走过去时身体轮廓和高光混在一起检测置信度直接掉到0.1以下。我的解决方案分三步第一步把摄像头曝光模式改成手动固定曝光度不要让它自动追着高光调整第二步在预处理里增加图像增强使用Gamma校正把暗部亮度拉高同时压一下高光溢出第三步如果条件允许在门口装一盏向地面打光的筒灯把地面均匀照亮而不是让灯光从背后直射摄像头。实际调完以后夜间检测率从50%提升到了85%左右虽然做不到白天水平但客流趋势已经能看出来了。5.4 模型部署常见坑很多初学者栽在Engine转换上。我整理几个最容易踩的坑。第一个是onnx模型导出的opset版本。YOLOv5的export脚本默认导出opset 12但TensorRT 8.2对高版本opset支持有些层会报错如果转换失败直接在export.py后面加--opset 11试试。第二个是engine文件不匹配。把在Orin NANO上生成的engine拷贝到普通NANO上去用加载必报错。engine文件绑定了硬件架构和TensorRT版本必须在目标机器上现场生成。第三个是pycuda安装失败。如果pip3 install pycuda卡很久可以先安装libboost-python再编译或者直接下载NVIDIA论坛里别人预编译好的whl包省得自己折腾。第四个是推理时出现黑白画面或错乱颜色。多半是预处理时没有把BGR转成RGB或者归一化方式不对。YOLOv5训练时用的是RGB0到1归一化推理时一定要保持一致。5.5 后续扩展从NANO到Orin NANO与更复杂的模型这套系统架构不只是为Jetson NANO服务的换到Jetson Orin NANO上整体代码不用改只需要重新生成TensorRT engine推理速度会有明显提升而且运行更大的模型也没问题。Orin NANO的算力比普通NANO高很多除了继续跑YOLOv5还能尝试YOLOv8甚至轻量级大模型部署。我之前用Orin NANO试过把Qwen这类小型语言模型跑在本地做数据问答比如对着客流统计结果问“今天下午两点到三点进了多少人”模型可以根据查询生成自然语言回答。这个方向对门店运营很有价值相当于把“计数系统”升级成“数据助手”。如果你现在还在用NANO想往这个方向试建议代码层面提前做好解耦检测、跟踪、计数、报表这四块互相独立后续把检测模块从YOLOv5换成YOLOv8或者把计数模块的数据接到大模型接口都不需要重写整个系统。最后再分享一个小技巧访客统计这类系统的价值不在于精确到个位数的“人数”而在于趋势和相对变化。就算系统偶尔漏一个两个只要误差稳定你依然可以用它对比不同时段、不同促销活动的客流差异。所以别把精力全花在追求100%准确率上把采集、存储、展示这条链路稳定跑起来才是这套系统真正值钱的地方。