☰
嵌入式视觉实战:基于摄像头的人体接近检测与距离估算
2026/10/20 9:39:56 网站建设 项目流程

做嵌入式视觉和智能硬件交叉方向这几年,“摄像头接近传感器”是被反复问到最多的需求之一。简单说,就是让普通摄像头变成一只会测距的眼睛:画面里有人靠近时,系统能感知到“有人来了”,判断他从哪个方向来、大概走到多远,然后触发亮屏、播放提示音、联动开关门这类动作。

之前有几个项目,需求本来只是“检测门口有没有人”,老板嫌传统红外方案太蠢——只能给一个0/1信号,分不清走过来的是人还是路过的猫,更不知道人到了哪个距离。换摄像头方案之后,这些问题基本都解决了,代价是算法和调试复杂度上了一个台阶。这篇就把我从方案对比、硬件选型、算法原理到现场调试踩坑的完整过程写出来,做同类项目的同学可以直接按这条路线来走。

1. 方案选型:为什举最终选了摄像头

1.1 传统接近方案的能力边界

先盘一下市面上常见的接近检测方案,因为很多人立项时第一反应并不是摄像头,而是更便宜的红外对射或反射式传感器。这类传感器的确便宜,几块钱到十几块钱就能买到,响应速度毫秒级,硬件结构也很简单。

但它有两个硬伤。第一,输出是一个开关量,只能告诉你“感应区域里有东西”,给不了“这东西离我多远”的信息。第二,它分不清靠近的是人还是宠物、纸箱,太阳光强一点还会误触发。放在门口迎宾这种场景,一只小狗路过和一个人走进来在红外传感器眼里没有任何区别,体验很糟糕。

超声波能输出距离值,原理是声波往返时间,测距很直接。但超声波也有两个软肋:目标表面如果是软的、吸声的,回波就弱;目标斜着走过来时反射波方向不对,测距会直接失败。人穿的衣物通常吸声,斜着走进门也是常态,所以超声波的漏检率在实际场景里并不低。

ToF和毫米波雷达能做高精度测距,也能区分目标类别,但成本比红外高一个数量级。对大多数智能硬件来说,“接近检测”只是众多功能里的一个,为它单独拉高整机成本并不划算。综合下来,如果需求不仅是“有没有人”,而是“人走到哪了、是不是人”,摄像头方案的性价比优势就非常明显。

1.2 摄像头方案的三个独特价值

摄像头方案最有价值的一点是目标类别识别。画面里能直接看出靠近的是人还是猫,这个能力是红外、超声都给不了的。智能家居里“只有人靠近才唤醒设备,宠物经过不亮屏”这类需求,用视觉方案做起来很自然。

第二个价值是距离与空间关系。单目摄像头也能估算距离,系统能判断人站在门口还是已经走到沙发边,由此把接近等级划分为远、中、近,然后触发不同的联动动作。比如远距离设备待机不亮,中距离屏幕提前预热,近距离才播报欢迎语。

第三个价值是区域性覆盖。红外点传感器只能覆盖一个点,一个摄像头可以覆盖整条过道或整面墙,一台设备管一片区域。软件里随手画一个ROI,还能把检测范围限制在重点区域,灵活度比硬件传感器高很多。

方案输出信息能否测距能否分类抗环境干扰成本量级
红外反射有无否否差(易受环境光)低
超声波距离值是否中(目标材质影响大)低
ToF/毫米波点云/距离高精度有限好中高
摄像头视觉图像+距离单目近似强中(依赖光照)中

1.3 视觉方案的适用边界

也要泼一盆冷水,摄像头方案不是万能的。如果需求只是“桌面30厘米内有人挥手就亮灯”,红外的成本和功耗优势大得多,完全没必要上视觉。

原因是摄像头实时图像处理需要一定算力,哪怕只做最简单的背景差分,也至少需要一块能跑Linux的处理器,而不是几块钱的MCU能扛下来的。另外视觉方案对光照敏感,光照突变会引发误检,安装位置、补光方案必须在项目初期就考虑进去。

选不选摄像头,我的判断标准就两条:要不要区分目标类别,要不要测距。两个都占,才值得上视觉方案;只占一个,可以继续考虑传统传感器。

2. 核心原理与设计思路

2.1 整条检测链路是怎么运转的

完整的处理链路由五步组成:图像采集、图像预处理、目标检测、距离估算、触发判定。很多人第一次做都会把重点放在目标检测上,觉得只要把人从画面里“找出来”就完了,实际上距离估算和触发判定才是决定现场体验的关键环节。

链路每一步都可能产生误差,而误差会在后续环节被放大。比如预处理不校正镜头畸变,画面边缘的像素位置会扭曲,距离估算就会出偏差;目标检测框抖动一下,距离值跟着跳,触发判定就会不稳定。所以工程上要从头控制每一步的确定性。

这五步里,图像采集决定数据源质量,预处理负责降噪和提速,目标检测负责把人形或运动目标从背景中分离,距离估算负责把图像尺寸和位置换算成物理距离,触发判定负责用滞回和连续帧数消除抖动、输出最终事件。前两步是通用环节,后三步决定了整个系统的体验水平。

2.2 单目摄像头为什么能测距

摄像头本质是透镜投影系统,三维空间里的物体投影到二维图像上后,目标在图像中的像素尺寸和物体到镜头的距离成反比。针孔相机模型里有一个非常简洁的关系式:

D = (f × H) / h_pixel

其中 D 是目标到摄像头的距离,f 是镜头焦距(以像素为单位),H 是目标的真实尺寸(比如身高),h_pixel 是目标在图像中的像素尺寸。

举例说明:一个640×480的画面,摄像头水平视场角60°,图像宽度640像素,可以算出水平方向的像素焦距 f ≈ (640 / 2) / tan(30°) ≈ 554像素。假设一个身高1.7米的人站在摄像头前,画面里他的高度是300像素,那么距离就是 D = 554 × 1.7 / 300 ≈ 3.1米。

这个公式看起来头头是道,但实际工程里直接套用误差很大。因为人的姿态会变,身高也不是严格1.7米,镜头还存在畸变。所以真实项目里我更推荐另一个方法:底部位置映射法。

底部位置映射法的思路是:摄像头安装在固定高度和俯角后,人在画面中脚部轮廓底边的y坐标,与水平距离之间存在单调对应关系。安装好以后,让人分别站在0.5米、1米、2米、3米的位置,记录对应的y坐标,生成一张标定表。运行时通过查表和插值直接输出距离,不需要知道身高,也不怕人弯腰。

这一步是典型的“用一次性现场标定,换后续每一帧的稳定性”。公式法适合拿到初始值,现场标定才是决定精度的核心动作。

2.3 从距离值到“接近事件”的工程化处理

距离值是连续变化的浮点数,直接拿它当触发条件,会在阈值附近遇到一个非常经典的问题:触发反复开关。人在1米阈值附近小幅晃动,系统一会儿进入“接近”状态,一会儿又退出,联动动作跟着频繁切换,体验很差。

解决做法是引入“滞回比较”,类似空调温控器的原理。设定近距离阈值为1.0米,进入这个范围就触发亮屏;但离开时不是立刻关闭,而是等距离超过1.3米并持续一段时间才关闭。这样人在1米附近晃动时不会被反复触发。

更稳妥的做法是叠加持续时间判断:连续5帧都满足“距离小于1米”才确认进入近区,连续15帧不满足才确认离开。5帧在10fps的帧率下就是0.5秒的确认时间,人正常走进来速度有限,0.5秒不会漏检;但这个时间窗足以滤掉多数由噪声、飞虫、小动物快速穿过造成的误触发。这套组合逻辑虽然简单,现场稳定性的提升非常明显。

3. 硬件选型与安装要点

3.1 摄像头模组怎么选

选型原则是“够用就好”。分辨率640×480到1280×720都可以,接近检测对图像细节要求不高,分辨率拉高只会增加处理器压力,帧率反而上不去。我一般用640×480,处理速度快,距离估算的分辨率对接近检测场景来说足够了。

帧率建议10到15fps。人正常行走速度约1.5m/s,10fps下相邻两帧画面里人只移动0.15米,对接近判定完全够用。一味追求高帧率会让处理器持续满载,功耗和散热都是问题,没有必要。

视角选水平60度左右比较舒服。视角太窄只能盖住一扇门的一小块区域,视角太广人形在画面里会变得很小,距离估算的分辨率随之下降。接口方面,如果主控是Linux单板电脑,USB摄像头最省事,OpenCV几步就能读到图像;如果要做成嵌入式一体机,优先选MIPI CSI或DVP接口的模组,画面延迟和稳定性比USB好一些。

3.2 主控算力与补光方案

主控方面,跑Python加OpenCV做背景差分或肤色检测,一款几百元的Linux单板电脑就能流畅运行。如果要跑深度学习人形检测模型,建议选择带NPU加速的方案,或者降低输入分辨率和帧率,否则推理耗时会让整个系统响应变慢。

补光这块最容易在项目中期被忽略。室内有顶灯还好,但如果是窗户边、走廊尽头,或者晚上关灯之后,摄像头基本就是全黑一片。我建议在接近检测场景里直接配置850纳米红外补光灯板,并把摄像头换成不带红外滤光片的型号。普通摄像头模组大多带红外截止滤镜,可以拆掉或者直接选对应的无IR-cut版本。

这样白天用自然光,晚上用红外光,同一套算法不需要修改。补光范围也不用太大,覆盖检测区域即可,功率几瓦就够,长时间运行的散热压力也不大。

3.3 安装高度、俯角与检测区域设计

摄像头安装位置直接决定后续算法难度,这一步值得多花时间。我的经验是:安装在1.2米到1.5米高度,镜头下俯10到15度。这样人的上半身和脚底都能进画面,而且脚底位置与距离的映射关系最稳定。如果装太高且镜头平视,人的脚底很容易被遮挡,底部位置映射法就失效了。

水平覆盖范围可以简单计算:W = 2 × D × tan(FOV/2)。FOV是60度时,两米外能覆盖约2.3米宽的走道,对单人接近检测场景来说是足够的。装好之后在软件里划一个ROI,只保留门框或走道区域,这样画面外围的背景变化不会干扰检测。

安装还有一个需要规避的点:不要让摄像头逆光对着窗口。逆光时画面过曝,人脸和身体细节全部丢失,再好的算法也救不回来。

4. 软件算法实现全流程

4.1 图像预处理三板斧

不管后面跑哪种检测算法,预处理步骤都差不多,也是提高运行速度的关键。第一件事是缩小分辨率:1280×720缩到640×360,图像面积直接降到四分之一,处理速度能提升约四倍,接近检测对细节不敏感,损失一点清晰度完全可接受。

第二件事是转灰度加高斯模糊。三通道彩色图转成单通道灰度图有利于降低计算量,5×5的高斯核可以去除传感器噪声,让后续轮廓检测结果更干净。第三件事是直方图均衡,这张牌对光照突变很有用:画面整体亮度变化时,直方图均衡会重新展布亮度范围,减少背景差分把光照变化误判成运动目标的情况。

参考写法:

frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (5, 5), 0) gray = cv2.equalizeHist(gray)

细节提示:直方图均衡一定放在缩放之后做。它基于整幅图的灰度统计,分辨率小意味着统计速度更快,后期实时性更好。这个顺序看着小事,实际影响帧率。

4.2 目标检测的三条可用路线

第一条路线是背景差分法,适合摄像头固定不动的场景。MOG2(混合高斯背景建模)是很经典的实现,先对每个像素建立背景模型,再把前景像素提取出来,后续通过找轮廓锁定运动目标。优点是实现简单、CPU占用低,缺点也很明显,光照突变时容易把整个画面都判成前景。做固定出入口的接近检测时,这个路线性价比最高。

第二条路线是肤色加运动约束,特别适合检测人手或人脸靠近的屏幕互动场景。先把RGB转换到YCbCr空间,在Cb和Cr通道上筛肤色范围,再和帧差结果做“与”运算,只保留“像肤色且正在运动”的区域。墙上海报的人脸因为静止不动会被帧差过滤,穿红衣的背景墙因为不是肤色也会被过滤。缺点是只对肤色敏感,如果目标戴手套、全身遮盖,就会漏检。

第三条路线是轻量级深度学习人形检测,用MobileNet SSD或其他级别相近的人形检测模型,直接输出目标框和置信度。优势是能区分人和猫狗,而且不要求目标正在运动,人站在门口静止不动也能检出。代价是模型推理本身占用算力和功耗,在Linux单板电脑上大约能跑到10fps,对接近检测场景来说是够用的。

检测路线实现难度CPU占用能否区分人/物是否依赖运动典型场景
背景差分(MOG2)低低否是固定门禁、走廊
肤色+运动约束中低部分(肤色)是屏幕前伸手/人脸靠近
轻量级深度学习高中高是否复杂背景、静态人体检测

4.3 距离估算与触发判定的参考实现

结合前面说的底部位置映射法,我给出一个精简可跑的参考代码。流程是:预处理、背景差分提取前景、取面积最大的轮廓、用轮廓底边的y坐标查标定表得到距离,最后做滞回和连续帧判断。代码可以直接跑通,换成深度学习检测时只需要把得到人形框送进同一套距离查表和滞回逻辑,后半段完全复用。

import cv2 import numpy as np # 标定表:轮廓底边y坐标(像素) -> 距离(米),需在实际安装场景实测标定 CALIB = [(450, 0.5), (398, 1.0), (356, 1.5), (318, 2.0), (276, 3.0)] def y_to_distance(y_bottom): ys = [row[0] for row in CALIB] dists = [row[1] for row in CALIB] if y_bottom >= ys[0]: return dists[0] if y_bottom <= ys[-1]: return dists[-1] for i in range(len(ys) - 1): if ys[i] >= y_bottom >= ys[i+1]: ratio = (ys[i] - y_bottom) / (ys[i] - ys[i+1]) return dists[i] + (dists[i+1] - dists[i]) * ratio return dists[-1] NEAR_TH = 1.0 # 进入“接近”阈值,单位米 RELEASE_TH = 1.3 # 释放阈值,单位米 NEED_NEAR_FRAMES = 5 # 进入接近状态需连续满足的帧数 NEED_FAR_FRAMES = 15 # 离开接近状态需连续满足的帧数 cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) bg_subtractor = cv2.createBackgroundSubtractorMOG2() state = "idle" near_cnt = 0 far_cnt = 0 while True: ok, frame = cap.read() if not ok: break # 预处理 gray = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) gray = cv2.cvtColor(gray, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (5, 5), 0) # 前景提取与轮廓合并 fg_mask = bg_subtractor.apply(gray) fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8)) contours, _ = cv2.findContours(fg_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) dist = None if contours: max_contour = max(contours, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(max_contour) if cv2.contourArea(max_contour) > 500: y_bottom = y + h # 注意这里用了缩放后的坐标,查表需保持一致 dist = y_to_distance(y_bottom) # 滞回 + 持续时间判定 if dist is not None: if dist < NEAR_TH: near_cnt += 1 far_cnt = 0 else: far_cnt += 1 near_cnt = 0 if state != "near" and near_cnt >= NEED_NEAR_FRAMES: state = "near" print("TRIGGER_ON") # 实际项目替换为GPIO电平或串口指令 elif state == "near" and far_cnt >= NEED_FAR_FRAMES: state = "idle" print("TRIGGER_OFF") cv2.imshow("fg", fg_mask) if cv2.waitKey(1) & 0xFF in (27, ord('q')): break cap.release() cv2.destroyAllWindows()

代码里几个点需要特别说明。第一,如果裁剪了ROI,y坐标就变成了相对ROI的局部坐标系,查表CALIB也要改用ROI坐标系里的标定数据,否则距离会整体偏移。第二,轮廓面积阈值500是一个经验初值,实际要根据画面里的具体分辨率调整,目标太远面积变小,阈值设太高会漏检。第三,距离查表的结果最好再做一次中值滤波,把偶发跳边滤掉,输出会平滑得多。

5. 常见问题、避坑与排查技巧

5.1 光照突变导致整片误触发

这是我实际测试中遇到频率最高的一个问题。走廊灯突然打开、窗外云层飘过遮住太阳,都会让整帧画面亮度瞬间变化,背景差分会误以为画面里出现大范围运动,把整块区域都判定成前景。有一阵子我调了很久参数都没效果,后来才意识到问题根本不在目标检测参数上。

解决方案是两条腿走路。第一,加光照突变检测:每帧计算全图灰度均值,当均值相对上一帧变化超过阈值时,先暂停触发判断,同时重置背景模型或让它重新学习一两秒。人走路造成的局部亮度变化通常远小于整帧的亮度突变,这个阈值设到20/255上下比较合适。第二,配合前面说的直方图均衡,两者叠加之后误触发率能下降很多。

5.2 夜间全黑、白天逆光测不到

没有红外补光的摄像头,晚上就是黑屏。想稳解决就换红外补光灯加无IR-cut的红外摄像头,一套组合下来白天晚上都能用。如果不想动硬件,只能拉高曝光时间,但画面会非常糊且拖影严重,接近检测勉强能用,效果一般。

安装时注意红外灯和摄像头之间不要有遮挡,补光灯尽量和摄像头同向。白天逆光是另一个方向的问题,正对窗口安装会让目标完全背光,脸部细节全丢。此时低成本的改法是调整安装位置,或者在软件里加大曝光补偿,但最稳妥的还是从一开始就避开逆光方位。

5.3 距离值跳变和轮廓分裂

人在较远距离时,前景轮廓经常是一块一块的,头和肩膀会分裂成多个小轮廓。直接用最大轮廓估算底边y坐标,距离结果会在几个值之间跳来跳去。处理方法是用形态学闭运算先合并距离近的轮廓,代码里的MORPH_CLOSE就是干这个的。

如果闭运算还不够,可以把多个轮廓按“外接矩形重叠面积”进一步合并,或者干脆取所有轮廓中“最靠近画面底部”的那个点作为脚部参考点。合并之后再做距离查表,输出会平滑很多。还有一个经验:远距离目标本来轮廓就不稳定,距离阈值和释放阈值之间留一点余量,能减少很多临界抖动。

5.4 误识别目标与触发延迟权衡

背景差分会把飞虫、飘落的树叶、扫地机器人都当成运动目标,这是方案自带的短板。有条件就上轻量级深度学习人形检测去替代背景差分,它在目标类别判断上更可靠。没有算力跑模型时,可以用宽高比约束来滤掉一部分非目标:普通人的外接矩形宽高比大致在0.2到0.8之间,飞虫和树叶不在这范围,直接丢弃。

触发延迟也不是越小越好。很多人拿到系统第一反应是把确认帧数调小,觉得响应更快体验更好,实际现场误触发率会立刻升高。5帧和15帧的确认时间是刻意留的,目的是用小代价的时序延迟换掉高频率的误触发。这个参数建议留到联调后期再根据现场效果微调。

现场现象常见原因处理建议
人坐下或蹲下后距离跳变底部位置映射依赖脚部位置改用更高位置的参考点,或增大确认帧数
风扇、窗帘引起误触发背景动态区域大缩小ROI,排除动态背景区域
玻璃反光导致误检反射形成伪运动目标调整安装角度,避免正对玻璃面
白天正常、夜晚失效无补光或IR-cut未去除增加红外补光,更换无IR-cut模组
响应有约半秒延迟确认帧数设得偏长误触发可控的前提下逐步缩短确认帧数

最后聊点实在的。这个项目做下来,我的体会是“摄像头接近传感器”本身的技术门槛并不高,最难的反而是把系统从实验室里的“能用”变成现场环境下的“稳定”。光照突变、轮廓分裂、目标误识别这些问题,算法书上不会教你,都是装机以后一个个暴露出来的。

根据我的经验,最值得多花时间的两个环节,一个是现场标定:距离标定表一定要在最终安装高度和角度下实测得到,纸上公式只能用来定初始值;另一个是触发策略:距离阈值加滞回加持续帧数这三层约束,是现场防误触发的定心丸。如果你正准备做类似项目,我建议先把这两步做扎实,剩下的算法细节都可以慢慢调。希望这篇完整复盘能让你少走几步弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询