1. 项目概述:从概念到现实的“社交距离雷达”
几年前,一个关于公共空间安全距离监测的想法在我脑海里挥之不去。当时,我们都在经历一段特殊的时期,公共场所保持安全距离成为了一种新常态。然而,仅仅依靠地面贴纸和广播提醒,效果有限且被动。我就在想,能不能做一个更直观、更智能的“眼睛”,来实时感知和提示人群的密度与间距?这就是“社交距离雷达”项目的起点。
简单来说,这个项目旨在构建一个软硬件结合的系统,它能像雷达扫描空域一样,实时监测特定区域内(如商店入口、银行柜台、办公区走廊)人与人之间的距离。当检测到有人违反了预设的安全距离(例如1.5米或2米)时,系统会通过视觉(如屏幕高亮、灯光变色)或听觉(如温和的提示音)方式发出警报,从而辅助管理者进行疏导,或提醒个人自觉遵守规则。它不是一个用于追踪个人的监控工具,而是一个聚焦于“间距”这个宏观匿名指标的辅助系统。
这个项目非常适合对物联网、计算机视觉和嵌入式开发感兴趣的开发者、创客,甚至是社区或小型商户的管理者。通过它,你不仅能学习到从传感器选型、数据处理到告警联动的完整链路,更能深入理解如何将技术应用于解决具体的现实问题。下面,我将完整分享从设计思路到代码实现的每一步,包括我踩过的那些坑和最终验证有效的方案。
2. 核心设计思路与方案选型
在动手之前,明确设计目标至关重要。我们的核心需求是:非接触、实时、低成本、易部署地监测静态区域内的社交距离。围绕这四点,我评估了几种主流技术路线。
2.1 技术路线对比与抉择
最初我考虑了三种方案:基于Wi-Fi或蓝牙信标的方案、基于热成像摄像头的方案,以及基于普通RGB摄像头和计算机视觉的方案。
Wi-Fi/蓝牙信标方案:需要每个人携带开启蓝牙的设备(如手机),通过信号强度(RSSI)估算距离。这个方案的硬伤在于依赖用户设备且精度极差。RSSI受环境干扰大,在复杂空间里误差可达数米,完全无法满足1-2米级的精确测距需求,首先被排除。
热成像摄像头方案:可以在完全黑暗环境下工作,且能天然保护隐私(不显示人脸细节)。听起来很理想,但最大的问题是成本。一台分辨率够用的热成像相机价格昂贵,远超项目预算。同时,单纯的热成像图像缺乏丰富的纹理特征,在多人重叠时,准确分割个体轮廓的算法复杂度高。
RGB摄像头+计算机视觉方案:这是最终选择的路线。其优势非常明显:硬件成本极低(一个普通的USB网络摄像头或树莓派摄像头模块即可),信息丰富(彩色图像包含大量可识别特征),并且有成熟的开源算法库(如OpenCV, YOLO, Deep SORT)支持。它的挑战在于算法精度和隐私处理,但这两个问题都有成熟的解决思路。
注意:隐私是此类项目的红线。在设计之初就必须明确,系统输出的是“两点之间距离是否过近”的告警事件,而不是识别出“张三和李四靠得太近”。在图像处理流程中,可以采用“检测框中心点”代替完整人体框、实时模糊化人脸区域、或直接在服务器端处理不存储原始图像等技术手段来保护隐私。
2.2 系统架构设计
确定了技术路线,整个系统的架构就清晰了。我设计了一个分层模块化的架构,便于调试和扩展。
[硬件层:摄像头] -> [边缘计算层:单板计算机] -> [业务逻辑层:距离计算与告警] -> [展示层:UI/声光提示]- 硬件层:采用树莓派4B作为核心,搭配树莓派官方摄像头模块v2。选择树莓派是因为它兼顾了足够的计算性能(用于运行轻量级AI模型)、丰富的GPIO接口(用于连接外部提示设备)和较低的功耗。摄像头选择官方模块是为了保证兼容性和驱动便利。
- 边缘计算层:所有图像采集、人体检测和跟踪算法都在树莓派上本地运行。这避免了将视频流上传到云端带来的延迟和带宽压力,实现了真正的实时处理,也进一步保护了隐私(数据不出本地)。
- 业务逻辑层:这是项目的“大脑”。它接收检测到的人体位置(通常是边界框的中心点坐标),根据摄像头的标定参数,将这些图像坐标转换为真实世界中的近似距离,然后计算每两个人之间的距离,并与阈值进行比较。
- 展示层:为了提供直观反馈,我设计了两种输出方式。一是在树莓派连接的屏幕上显示实时画面,并将距离过近的两人用红色线条连接;二是通过树莓派的GPIO控制一个RGB LED灯环,正常时显示绿色,有违规时显示红色并闪烁,同时触发一个蜂鸣器发出短促提示音。
这套架构的优点在于闭环本地化,响应速度快,且硬件成本可控(树莓派+摄像头+LED等总成本在几百元内)。接下来,我们深入每个核心环节的实操细节。
3. 核心模块实现与关键技术解析
3.1 轻量级人体检测模型部署
在资源受限的树莓派上运行YOLOv5或更大型的模型非常吃力,会导致帧率(FPS)极低,失去实时性。经过测试,我选择了MobileNet-SSD作为检测模型。它在速度和精度之间取得了很好的平衡,并且OpenCV的DNN模块原生支持其Caffe模型,部署非常简单。
首先,需要下载预训练的MobileNet-SSD模型文件(.caffemodel权重文件和.prototxt网络结构文件)。在OpenCV中加载并运行推理的代码核心如下:
import cv2 import numpy as np # 加载模型 net = cv2.dnn.readNetFromCaffe('deploy.prototxt', 'mobilenet_iter_73000.caffemodel') # 建议使用CUDA(如果你有)或OpenCL加速,树莓派上通常用CPU # net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) # net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) def detect_people(frame): (h, w) = frame.shape[:2] # 准备输入Blob blob = cv2.dnn.blobFromImage(cv2.resize(frame, (300, 300)), 0.007843, (300, 300), 127.5) net.setInput(blob) detections = net.forward() person_boxes = [] conf_threshold = 0.5 # 置信度阈值 for i in range(detections.shape[2]): confidence = detections[0, 0, i, 2] if confidence > conf_threshold: idx = int(detections[0, 0, i, 1]) # MobileNet-SSD的类别标签中,'person'通常是15 if idx == 15: box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) (startX, startY, endX, endY) = box.astype("int") person_boxes.append((startX, startY, endX, endY, confidence)) return person_boxes实操心得:在树莓派上,使用OpenCV的CPU推理,处理一张300x300的图片大约需要0.1-0.2秒。为了提升实时性,我将输入图像分辨率固定为300x300,并且不是每一帧都进行检测,而是采用“检测+跟踪”的策略。即每5-10帧做一次完整的检测,中间的帧使用跟踪器(如OpenCV的CSRT或更轻量的KCF)来更新人的位置,这能大幅提升整体FPS。
3.2 从像素到实际距离的转换
这是项目的核心算法难点。摄像头捕捉的是二维图像,而我们需要的是三维空间中的实际距离。这里涉及相机标定和透视几何。
一个简化但有效的方法是假设地面是平面,并且所有人都站立在地面上。这样,每个人脚部接触地面的点(在图像中近似为检测框的底边中心点)就对应了实际地面的一个位置。我们需要建立一个从图像坐标(u, v)到地面实际坐标(X, Y)的映射关系。
我采用的方法是单应性矩阵(Homography)变换。具体步骤如下:
- 现场标定:在监控区域的实地地面上,测量一个矩形(例如2m x 2m的正方形)的四个顶点的实际坐标(以米为单位)。同时,在摄像头拍摄的画面中,找到这四个顶点对应的像素坐标。
- 计算单应性矩阵:利用这四组对应点,使用OpenCV的
cv2.findHomography()函数,可以计算出一个3x3的变换矩阵H。 - 坐标转换:对于检测到的每个人体框底边中心点(像素坐标
(u, v)),通过矩阵H将其变换到地面平面坐标(X, Y)。变换公式为:[x', y', w']^T = H * [u, v, 1]^T 实际坐标 X = x'/w', Y = y'/w' - 计算距离:获得了每个人的
(X, Y)坐标后,任意两人之间的实际距离就可以用欧几里得距离公式sqrt((X1-X2)^2 + (Y1-Y2)^2)轻松计算。
import cv2 import numpy as np # 假设我们测量了一个2x2米的正方形,其实际顶点坐标(单位:米) # 顺序为:左上、右上、右下、左下 world_points = np.array([[0, 0], [2, 0], [2, 2], [0, 2]], dtype=np.float32) # 从图像中手动获取上述四个点的像素坐标(需要从一帧静态图片中获取) image_points = np.array([[120, 80], [520, 90], [510, 350], [130, 340]], dtype=np.float32) # 计算单应性矩阵 H H, _ = cv2.findHomography(image_points, world_points) def pixel_to_world(pixel_point): """将像素坐标转换到世界坐标(地面平面)""" px, py = pixel_point # 转换为齐次坐标 src = np.array([px, py, 1]).reshape(3, 1) dst = H.dot(src) dst = dst / dst[2] # 归一化 world_x, world_y = dst[0, 0], dst[1, 0] return world_x, world_y # 示例:计算两人距离 person1_bottom_center = (200, 300) # 像素坐标 person2_bottom_center = (400, 310) # 像素坐标 p1_world = pixel_to_world(person1_bottom_center) p2_world = pixel_to_world(person2_bottom_center) distance = np.sqrt((p1_world[0]-p2_world[0])**2 + (p1_world[1]-p2_world[1])**2) print(f"两人实际距离约为:{distance:.2f} 米")注意事项:这个方法的精度严重依赖于标定。地面必须尽可能平坦,标定用的矩形要测量准确。摄像头一旦安装固定,就不要移动,否则需要重新标定。对于有高度变化的场景(如楼梯),此方法误差会增大。
3.3 多目标跟踪与ID保持
为了不让同一个人在连续帧中被认为是不同的人,从而产生距离计算混乱,必须引入多目标跟踪(MOT)。我采用了轻量级的SORT(Simple Online and Realtime Tracking)算法。它结合了卡尔曼滤波(预测目标下一帧的位置)和匈牙利算法(关联当前检测与已有跟踪轨迹),效率很高。
在Python中,有现成的sort库可以使用。其集成代码如下:
from sort import Sort # 初始化SORT跟踪器 tracker = Sort(max_age=5, min_hits=3, iou_threshold=0.3) track_dict = {} # 用于存储跟踪ID对应的世界坐标 # 在每一帧中 def process_frame(frame, detections): # detections格式: [[x1, y1, x2, y2, conf], ...] dets = np.array(detections) # 使用SORT更新跟踪器,得到跟踪结果 # trackers格式: [[x1, y1, x2, y2, track_id], ...] trackers = tracker.update(dets) current_positions = {} for trk in trackers: trk_id = int(trk[4]) # 获取检测框底边中心点(像素) box = trk[:4].astype(int) bottom_center = ((box[0]+box[2])//2, box[3]) # 转换到世界坐标 world_x, world_y = pixel_to_world(bottom_center) current_positions[trk_id] = (world_x, world_y) # 更新到字典,保留历史用于速度计算等(可选) track_dict[trk_id] = (world_x, world_y) # 基于current_positions计算所有track_id两两之间的距离 check_distances(current_positions)max_age参数表示一个跟踪目标丢失多少帧后会被删除,min_hits表示一个检测目标被关联多少次后才确认为有效跟踪ID。合理调节这两个参数可以平衡对新目标的敏感度和对短暂遮挡的鲁棒性。
3.4 告警触发与交互反馈
当计算出两人距离小于设定的安全阈值(如1.5米)时,需要触发告警。告警逻辑需要加入去抖动处理,避免因检测框微小抖动造成的误报。
violation_pairs = [] # 记录当前违规的ID对 violation_frame_count = {} # 记录每个违规对持续的帧数 ALERT_THRESHOLD = 1.5 # 米 DEBOUNCE_FRAMES = 10 # 连续10帧违规才确认告警 def check_distances(positions): global violation_pairs, violation_frame_count ids = list(positions.keys()) current_violations = set() # 计算所有两两组合的距离 for i in range(len(ids)): for j in range(i+1, len(ids)): id1, id2 = ids[i], ids[j] pos1, pos2 = positions[id1], positions[id2] distance = np.sqrt((pos1[0]-pos2[0])**2 + (pos1[1]-pos2[1])**2) if distance < ALERT_THRESHOLD: pair = tuple(sorted((id1, id2))) # 排序使(id1, id2)和(id2, id1)视为同一对 current_violations.add(pair) # 在画面上绘制红色连线 pt1_pixel = world_to_pixel(pos1) # 需要实现逆变换函数 pt2_pixel = world_to_pixel(pos2) cv2.line(frame, pt1_pixel, pt2_pixel, (0, 0, 255), 2) # 更新持续帧数计数器 for pair in list(violation_frame_count.keys()): if pair in current_violations: violation_frame_count[pair] += 1 else: violation_frame_count[pair] = 0 # 判断哪些违规对需要触发告警(持续超过DEBOUNCE_FRAMES) alert_pairs = [] for pair, count in violation_frame_count.items(): if count >= DEBOUNCE_FRAMES: alert_pairs.append(pair) # 触发硬件告警(例如控制GPIO) if alert_pairs: trigger_hardware_alert(True) else: trigger_hardware_alert(False) violation_pairs = list(current_violations)硬件告警部分,通过树莓派的GPIO控制外设。这里以控制一个RGB LED灯环为例:
import RPi.GPIO as GPIO import time # 假设使用PWM控制RGB灯 PIN_RED = 17 PIN_GREEN = 27 PIN_BLUE = 22 GPIO.setmode(GPIO.BCM) GPIO.setup([PIN_RED, PIN_GREEN, PIN_BLUE], GPIO.OUT) pwm_red = GPIO.PWM(PIN_RED, 100) # 100Hz频率 pwm_green = GPIO.PWM(PIN_GREEN, 100) pwm_blue = GPIO.PWM(PIN_BLUE, 100) pwm_red.start(0) pwm_green.start(0) pwm_blue.start(0) def set_color(red, green, blue): """设置RGB颜色,参数范围0-100""" pwm_red.ChangeDutyCycle(red) pwm_green.ChangeDutyCycle(green) pwm_blue.ChangeDutyCycle(blue) def trigger_hardware_alert(is_alert): if is_alert: set_color(100, 0, 0) # 红色 # 可以同时让蜂鸣器响一下 # GPIO.output(BUZZER_PIN, GPIO.HIGH) # time.sleep(0.1) # GPIO.output(BUZZER_PIN, GPIO.LOW) else: set_color(0, 100, 0) # 绿色4. 系统集成、部署与优化
4.1 树莓派环境搭建与依赖安装
在树莓派上部署,首先需要安装系统(推荐Raspberry Pi OS Lite)和基础依赖。
# 更新系统 sudo apt-get update && sudo apt-get upgrade -y # 安装Python3和pip sudo apt-get install python3 python3-pip python3-venv # 创建虚拟环境(推荐) python3 -m venv sd_radar_env source sd_radar_env/bin/activate # 安装核心Python库 pip install opencv-python-headless numpy scipy # 安装GPIO控制库 pip install RPi.GPIO # 安装SORT跟踪算法 pip install filterpy # SORT的依赖 # 可以从GitHub克隆SORT源码,或直接复制其核心文件到项目目录 git clone https://github.com/abewley/sort.gitOpenCV的完整安装(如果需要GUI功能)在树莓派上比较耗时,opencv-python-headless是一个更轻量的选择,适合无桌面环境。如果需要在树莓派上显示实时画面,可以安装opencv-python(但编译安装可能需要数小时),或者使用picamera库直接操作摄像头并通过其他方式(如VNC)查看。
4.2 主程序流程与性能调优
将上述所有模块整合到一个主循环中。性能是关键,以下是优化后的主循环伪代码结构:
import cv2 from sort import Sort # ... 其他导入和函数定义 ... def main(): # 初始化摄像头、跟踪器、GPIO等 cap = cv2.VideoCapture(0) # 或使用picamera cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) tracker = Sort() # ... GPIO初始化 ... frame_count = 0 DETECT_INTERVAL = 5 # 每5帧做一次完整检测 while True: ret, frame = cap.read() if not ret: break frame_count += 1 do_detection = (frame_count % DETECT_INTERVAL == 0) if do_detection: # 完整检测流程 person_boxes = detect_people(frame) dets = [[x1, y1, x2, y2, conf] for (x1, y1, x2, y2, conf) in person_boxes] dets = np.array(dets) if dets else np.empty((0,5)) else: # 仅跟踪流程,使用上一帧的跟踪结果预测新位置 dets = np.empty((0,5)) # 传递空检测,SORT会仅进行预测 # 更新跟踪器 trackers = tracker.update(dets) # 计算世界坐标、检查距离、触发告警 current_positions = {} for trk in trackers: # ... 提取ID和坐标,转换 ... current_positions[trk_id] = (world_x, world_y) check_distances(current_positions) # 在画面上绘制框、ID和违规连线 draw_frame(frame, trackers, violation_pairs) # 显示画面(如果安装了GUI) # cv2.imshow('Social Distancing Radar', frame) # if cv2.waitKey(1) & 0xFF == ord('q'): # break # 清理 cap.release() cv2.destroyAllWindows() GPIO.cleanup() if __name__ == '__main__': main()性能调优技巧:
- 降低分辨率:将摄像头输入分辨率从默认的1920x1080降至640x480,能极大减轻后续处理负担。
- 跳帧检测:如前所述,采用“检测+跟踪”策略。
- 模型优化:尝试更小的模型输入尺寸,如将300x300降至200x200,但需测试精度损失是否可接受。
- 使用多线程:可以将图像采集、检测/跟踪、UI显示/GPIO控制放在不同线程,避免阻塞。
4.3 现场部署与校准要点
部署不是把设备放上去就行,校准决定了系统的可用性。
- 摄像头安装:摄像头应俯视监控区域,视角尽可能垂直于地面。安装高度建议在2.5米到4米之间,太低视野窄,太高则人体在图像中太小。确保安装稳固,无振动。
- 标定流程:
- 在摄像头视野内,在地面上布置一个已知尺寸的矩形(如用胶带贴出一个2m x 2m的正方形)。
- 运行一个校准脚本,该脚本会捕获一帧图像,并提示你用鼠标依次点击矩形的四个顶点(顺序必须固定,如左上、右上、右下、左下)。
- 脚本将像素坐标和你输入的实际坐标(米)保存下来,计算并保存单应性矩阵H到一个配置文件(如
calibration_data.npy)中。
- 阈值调整:安全距离阈值
ALERT_THRESHOLD需要根据实际场景微调。考虑到测量误差和人体宽度,可以设置为略低于规定距离(如规定1米,阈值设0.9米)。 - 光照适应:室内环境要避免摄像头正对强光源(窗户、灯光),防止过曝。可以考虑使用带自动宽动态(WDR)功能的摄像头,或者在代码中加入自动曝光调整。
5. 常见问题排查与实战心得
在实际搭建和测试过程中,我遇到了不少问题,这里总结一下最常见的几个及其解决方案。
5.1 检测不稳定,人时隐时现
- 现象:同一个人在画面中,检测框闪烁,ID频繁切换。
- 原因:
- 检测置信度阈值
conf_threshold设置过高。 - 光照条件差,或者人物穿着与背景颜色相近。
- 模型输入图像分辨率太低,丢失细节。
- 检测置信度阈值
- 解决:
- 适当降低
conf_threshold(如从0.5调到0.3)。 - 改善光照,或尝试在代码中做图像预处理,如直方图均衡化
cv2.equalizeHist(针对灰度图)或CLAHE。 - 尝试稍高的输入分辨率,或在树莓派上使用更高效的模型,如YOLO-Fastest或NanoDet。
- 最重要的是:调优SORT跟踪器的参数。增加
max_age(如从5调到10),让跟踪轨迹在丢失后能保持更久;降低min_hits(如从3调到1),让新检测更快被确认为轨迹。
- 适当降低
5.2 距离测量不准,误差大
- 现象:实际距离1米,系统测出1.5米或0.7米。
- 原因:
- 标定不准:地面标定矩形测量不精确,或图像上的四个顶点点选位置有偏差。
- 地面非平面:监控区域有坡度或台阶。
- 参考点错误:使用人体框中心点而非脚底点进行转换。当人弯腰、蹲下时,中心点会严重偏离地面接触点。
- 解决:
- 重新进行精细标定,使用更大的、边角清晰的标定物(如棋盘格标定板),并确保测量绝对准确。
- 尽量选择平坦区域部署。对于轻微坡度,可以尝试用多个平面分段标定,但复杂度激增。
- 坚持使用人体框底边中心作为参考点。尽管人可能叉开腿,但底边中心依然是脚部位置的最佳近似。
5.3 树莓派处理速度慢,卡顿严重
- 现象:帧率(FPS)低于5,画面卡顿,告警延迟高。
- 原因:树莓派CPU算力有限,同时进行检测、跟踪、坐标转换、绘图等操作负载过重。
- 解决:
- 启用OpenCV的NEON优化:确保编译安装OpenCV时启用了NEON指令集支持(Raspberry Pi OS自带的版本通常已开启)。
- 使用
picamera库:如果使用树莓派官方摄像头,picamera库比OpenCV的VideoCapture效率更高,能直接获取numpy数组。 - 简化显示:如果不必要,可以不调用
cv2.imshow显示画面,这是很大的开销。通过VNC远程桌面查看,或者只将违规截图保存下来。 - 考虑硬件升级:树莓派5的性能有显著提升。或者使用带有NPU(神经网络处理单元)的开发板,如Jetson Nano,可以流畅运行更复杂的模型。
5.4 误报与漏报
- 误报:两人距离明明足够,却触发告警。
- 原因:标定矩阵H在图像边缘畸变较大;两人在垂直方向上有高度差(如一人站着一人坐着),但系统假设他们脚在同一平面。
- 缓解:将监控区域限定在图像中心畸变小的范围;在距离计算中引入高度修正因子(如果知道摄像头高度和人的大致身高,可通过几何估算),但这会引入复杂度。
- 漏报:两人距离过近,但没有告警。
- 原因:检测模型漏检了其中一人;两人重叠(遮挡)严重,被检测为一个人。
- 缓解:使用召回率更高的模型;尝试多角度部署摄像头以减少遮挡。
我个人最深的一个体会是:这类感知型项目,算法精度只占一半,现场工程部署和调优占另一半。一个在测试视频里表现完美的模型,放到真实环境中,可能会因为光线、视角、背景复杂度的变化而大打折扣。因此,必须留出充足的时间进行现场调试和参数微调。最好的测试,就是让几个同事在监控区域内来回走动,观察系统的反应,然后根据问题日志(可以简单打印到控制台或写入文件)逐一调整阈值和参数。这个过程虽然繁琐,但却是项目从“玩具”走向“可用”的关键一步。