随着自动化程度的加深,用户对于获取目标物体3D数据的需求也越来越多。截止目前,还是有相当一部分的物体识别算法是仅基于2D摄像头数据进行的,并没有3D空间感知。主要是因为用户还没有建立起一个能够利用激光雷达数据的算法体系。
但是,大家有没有想过直接将2D算法用于3D数据上。
接下来我们就向大家展示如何通过2D算法和Ouster激光雷达3D数据来实现社交距离监测。
项目概述
本次项目的目标是通过 OS0-128 激光雷达识别目标人,并当两人间距小于1.8 m时发出警报。
下图是最终结果的预览,可以看到,该模型能够对场景中的人物进行识别和追踪,并实时计算出两者间的相对距离。当两人相距小于1.8 m时,就会触发警报,同时识别框变为红色。

以下是原始点云数据(.PCAP 文件)经算法处理后最终输出目标人之间距离的流程图,下文将详细介绍。

我们建议您跟随我们一同完成整个操作,在此之前您需要准备:
- 我们为此项目训练的自定义 YOLOv5 存储库 (基于原始 YOLOv5 存储库)
- Ouster Python SDK
- 示例数据 (请同时下载 PCAP 和 JSON 文件)
Ouster 数据介绍
首先,让我们快速回顾一下Ouster 激光雷达数据的独特性,这也是本项目成功的重要原因。当其他激光雷达仅输出3D数据的情况下,Ouster同时提供了2D和3D数据,且每个2D图像中的像素点均与3D数据1:1完美空间对应。
Ouster 数据层
每一个像素点都包括了4个数据层:
⇒ 距离:该像素点至雷达原点的距离,使用激光脉冲的飞行时间计算
⇒ 信号:给定点返回激光雷达的光的强度,Ouster 数字激光雷达的信号数据以检测到的光子数表示
⇒ 近红外:给定点收集的阳光强度,也表示为检测到的不是由雷达本身激光脉冲产生的光子数量
⇒ 反射率:激光雷达检测到的表面(或物体)的反射率

在这一项目中,我们使用反射率来识别目标人,同时通过距离数据来计算目标人之间的相对距离。我们选择反射率作为目标识别依据的原因是因为反射率包含了有关物体固有反射特性的信息。信号数据会随着距离的变化而变化(越远的物体返回的光越少),近红外数据会随着阳光强度的变化而变化(在夜间或室内则为不可见),但反射率数据在不同光照条件和距离下都是一致的。
完美的1:1空间对应关系
Ouster 激光雷达独有的特性之一就是数据点的 1:1 空间对应。2D结构化数据中的每一个像素点都对应着激光雷达数据中的一个3D点,无任何离散或重采样发生。

因此,在感知过程中不会存在任何不必要的噪点或伪影,这可以帮助提高准确性、显著减少计算需求。这也是2D算法能直接使用Ouster激光雷达输出的3D数据的原因。
YOLOv5 + Ouster
本次案例中我们所使用的算法是 YOLOv5 一种基于轻量级卷积神经网络 (CNN) 的对象检测算法。 YOLOv5 是目前常用的 2D 感知算法之一,速度快(GPU的推理速度高达每秒100+帧)、精度高。但是,YOLOv5无法适用PCAP数据格式,因此,我们的第一步是修改YOLOv5,使其能够读取Ouster激光雷达的反射率数据。
使用YOLOv5进行目标探测
在此之前,我们先看一下如何使用YOLOv5对RGB图像中的目标物进行探测。
首先,复制 存储库 并安装所需文件,操作环境需为 Python>=3.7.0,包括 PyTorch>=1.7。
git clone https://github.com/fisher-jianyu-shi/yolov5_Ouster-lidar-example
cd yolov5_Ouster-lidar-example #go to source directory
pip install -r requirements.txt #install required packages
然后在各种数据源(图像、视频、视频流、网络摄像头等)上运行源目录中的 detect.py 文件(来自原始 YOLOv5 存储库)。例如,使用 40% 置信度阈值的已训练好的 YOLOv5s 模型检测图像中的人,我们只需在源目录的终端中运行以下命令:
python detect.py --class 0 --weights Yolov5s.pt --conf-thres=0.4 --source example_pic.jpeg --view-img
--class 0: #detection class (0 = people)
--weights Yolov5s.pt #pre-trained weights; s stands for small. You can also try the smaller model YOLOv5n, or bigger models like YOLOv5m/l/x
--conf-thres=0.4 #confidence threshold, any prediction with a confidence level lower than specified will not be considered a valid prediction
--source example_pic.jpeg #path to the image; can also be video, stream, webcam
--view-img #show results
运行之后,结果将自动保存在目录 runs/detect/exp 中,最终我们获得是带有标注框和预测置信度的图像。

YOLOv5 + Ouster 激光雷达数据
如前所述,YOLOv5 可适用于图像、视频、网络摄像头或视频流,但它无法识别PCAP文件,因此我们需要先将雷达的数据层转换为图像,Ouster Python SDK 实现并简化了这一过程。借助该SDK,我们对detect.py 文件进行修改,便可以在PCAP的反射率数据层上成功运行,修改后的检测脚本称为 detect_pcap.py。
因而我们需要先安装Ouster Python SDK( 安装指南), 下载 示例数据,同时将PCAP和JSON文件都保存于源目录下,然后运行新脚本:
python detect_pcap.py --class 0 --weights yolov5s.pt --conf-thres=0.4 --source Ouster-YOLOv5-sample.pcap --metadata-path Ouster-YOLOv5-sample.json --view-img
运行后,便会在runs/detect/exp下自动创建以下视频:

尽管 YOLOv5 从未用激光雷达反射率数据训练过,但该模型的结果还是可圈可点的。这一结果表明我们完全可以直接将在相机图像上训练的算法应用于带有反射率的激光雷达数据。当然我们还有很大的提升空间,从下面的截图中可以看到,在使用默认的 YOLOv5 权重时,某些时刻目标人中的一个被识别的置信度为 42%(超过 40% 的阈值),但另一个人却没有被识别。

接下来让我们看一下详细的过程。
Ouster激光雷达共提供两个文件:一个是PCAP文件,即激光雷达获得的原始 UDP 数据包,另一个是JSON文件,包含解译数据包所需的元数据。首先,我们通过SDK在客户端模块加载激光雷达元数据。
from ouster import client
metadata_path = '<DATA_JSON_PATH>'
with open(metadata_path, 'r') as f:
metadata = client.SensorInfo(f.read())
借助元数据,我们便可以通 过pcap模组来解译PCAP文件,以pcap.Pcap为例:
from ouster import pcap
pcap_path = '<DATA_PCAP_PATH>'
pcap_file = pcap.Pcap(pcap_path, metadata)
现在,让我们使用扫描模块将 PCAP 解析为单帧的扫描图,扫描图即为数据重组的 360º 视图。接下来,我们再将反射率和距离数据层从每帧扫描图中提取出来。调用如下所示:
import numpy as np
with closing(client.Scans(PCAP_file)) as scans:
for scan in scans:
ref_field = scan.field(client.ChanField.REFLECTIVITY)
ref_val = client.destagger(PCAP_file.metadata, ref_field) #the destagger function is to adjust for the pixel staggering that is inherent to Ouster lidar sensor raw data
ref_img = ref_val.astype(np.uint8) #convert to numpy array for image processing later
range_field = scan.field(client.ChanField.RANGE)
range_val = client.destagger(PCAP_file.metadata, range_field)
这里需要指出的是 destagger 功能。大家可能已经知道,旋转式激光雷达并不能一次捕捉到整个画面。因为光束在不同时刻以不同角度发射和接收,激光雷达捕获的原始图像是模糊或交错的。此时元数据文件就提供了数据包的接收时间和方位,使我们能够对原始数据进行重构和去错列,以完成准确的可视化。更多信息,可参考此页面。
用激光雷达数据重新训练YOLOv5
YOLOv5s 默认模型是在 COCO 数据集 上训练的,该数据集主要包含的是 RGB 图像(例如 640 x 640),与反射率图像(例如 512/1024/2048 x 128)完全不同。因而,该模型在反射率图像上运行的表现不如典型的 RGB 图像。
因而我们将尝试使用一些自定义数据重新训练该模型并提高其表现。要使用激光雷达数据重新训练 YOLOv5 模型,我们就需要构建自己的数据集。
从PCAP文件中获得数据
我们收集了多个OS0-128 的 PCAP 文件(查看如何 搜集数据),然后使用 Ouster Python SDK 获取每帧扫描图的反射率信息,并利用 OpenCV 的 cv2.imwrite 函数将反射率数据层保存为灰度图以用于数据标注:
import cv2
import numpy as np
from contextlib import closing
from ouster import client
from ouster import pcap
with open(metadata_path, 'r') as f:
metadata = client.SensorInfo(f.read())
source = pcap.Pcap(pcap_path, metadata)
counter = 0
with closing(client.Scans(source)) as scans:
for scan in scans:
counter += 1
ref_field = scan.field(client.ChanField.REFLECTIVITY)
ref_val = client.destagger(source.metadata, ref_field)
ref_img = ref_val.astype(np.uint8)
filename = 'extract'+str(counter)+'.jpg'
cv2.imwrite(img_path+filename, ref_img)
为了节约时间,我们制作了一个700+张反射率图像的小数据集:

数据标注
接下来我们又通过 Roboflow 对反射率图像进行标注,这是一个免费的工具,用来组织、标注和托管数据集,且可与 YOLOv5 无缝集成。

标注完成后,数据集将被分成训练集、验证集和测试集,其中训练集占80%。

使用Google Colab进行模型训练
接下来我们通过使用 YOLOv5_train_Ouster.ipynb Jupyter notebook(以官方 YOLOv5-Custom-Training 版本为基础进行微调后得到)来训练我们的自定义权重。
此次训练是使用支持 GPU 的 Google Colab 帐户完成的。我们首先使用 Roboflow Python 包将自定义标注后的数据下载到 Google Colab 的工作区:
from roboflow import Roboflow
rf = Roboflow(model_format="yolov5", notebook="ultralytics")
然后你将获得一个链接以获取 API KEY。获取 API KEY 后,运行以下代码下载 Roboflow 托管的标注数据集:
rf = Roboflow(api_key="YOUR API KEY HERE")
project = rf.workspace().project("YOUR PROJECT")
dataset = project.version("YOUR VERSION").download("yolov5")
数据下载后,我们使用 train.py 文件(来自原始 YOLOv5 存储库)开始训练模型。我们是基于预训练后的 YOLOv5 权重进行 迁移学习 的,这样就不需要从头开始训练权重。
!python train.py --img 640 --single-cls --batch 16 --epochs 30 --data '<DATA_PATH>' --weights yolov5s.pt --cache
借助GPU,整个训练仅用了约7分钟:

使用训练后的自定义权重
完成训练之后,表现最优的最新权重将以 best.pt 自动保存在runs/train/exp/weights/下。接下来我们来运行它:
python detect_pcap.py --class 0 --weights best.pt --conf-thres=0.4 --source Ouster-YOLOv5-sample.pcap --metadata-path Ouster-YOLOv5-sample.json --view-img
自定义权重下的运行结果:

通过下图比较,我们可以看到在自定义模型下,运行结果有了大幅提高,两个目标人能够被准确的探测识别,且置信区间更高:


如果我们想进一步优化目标检测性能,一方面可以通过扩大训练数据集,或者使用更大的网络(例如 YOLOv5m 或 YOLOv5l,但是网络越大,训练所需的时间会越长)。
相对距离计算
以下图片为我们展示了目标人之间距离的计算过程:

详细过程如下:
因为Ouster激光雷达输出的数据中,2D和3D数据是1:1空间对应的,因此每个2D图像中的像素点都可以通过 Ouster Python SDK 和元数据轻松转化为 笛卡尔坐标 。在元数据文件中包含了一个预先计算好的查询表,能够将每个像素点投影到笛卡尔坐标中:
from ouster import client
xyzlut = client.XYZLut(metadata) #call cartesian lookup table
xyz_destaggered = client.destagger(metadata, xyzlut(scan)) #to adjust for the pixel staggering that is inherent to Ouster lidar sensor raw data
现在我们有了每个点的 X、Y、Z 坐标,接下来需要获得标注框内的距离数值。我们只需要提取在反射率数据层中标注框的 (x,y) 坐标,然后用相同的 (x,y) 坐标在距离数据层中获取对应的距离读数。
range_roi = range_val[int(xyxy[1]):int(xyxy[3]), int(xyxy[0]):int(xyxy[2])] #xyxy is the X Y coordinates of the bounding box
在 range_roi 矩阵中,我们会获得标注框内的所有距离值。简单起见,我们选择用标注框中与激光雷达最近的兴趣点 (POI) 来代表已识别的目标人。
poi = np.unravel_index(range_roi.argmin(), range_roi.shape) #take the (x,y) coordinates of closest point within roi
现在对于每个标注框,我们都有了一个点来表示检测到的人。这样我们就可以使用预先计算好的查询表去获得该点的 X、Y、Z 坐标:
xyz_val = xyz_destaggered[poi] #get the (x,y,z) coordinates with the lookup table
xyz_list.append(xyz_val) #save the (x,y,z) coordinates for distance calculation
将这些点的 X、Y、Z 坐标保存在 xyz_list 中,便可计算出两个目标人之间的距离。
xyz_1 = xyz_list[0] #person 1 coordinates (closest point in bounding box 1)
xyz_2 = xyz_list[1] #person 2 coordinates (closest point in bounding box 2)
dist = math.sqrt((xyz_1[0] - xyz_2[0])**2 + (xyz_1[1] - xyz_2[1])**2 + (xyz_1[2] - xyz_2[2])**2) #distance between two X Y Z coordinates)
接下来在 detect_pcap.py 中再添加了一个社交距离参数:
python detect_PCAP.py --class 0 --weights best.pt --conf-thres=0.4 --source Ouster-YOLOv5-sample.pcap --metadata-path Ouster-YOLOv5-sample.json --view-img --social-distance
以下为最终结果:

总结
上述项目演示了如何像使用摄像头数据一样使用Ouster激光雷达数据,以及如何使用Ouster Python SDK直接获取每个像素点的准确深度信息。这只是计算机视觉算法与Ouster 激光雷达数据相结合的一个简单示例。由于此项目更多只是用作一种证明,因此我们为了节省时间采取了很多简单操作,实际上仍有一些方面我们可以改进,例如:
- 用于训练自定义模型权重的数据库只有700张图片,我们完全可以通过增加数据量以提高不同场景下结果的准确性;
- 我们可以通过调整色彩空间和反射层数据的纵横比,提高检测精度;
- 我们使用了标注框中最靠近激光雷达的像素点。显然,如果目标人之间有物体,这样的方法便不可行。为了使距离的计算结果更加可靠,我们可以尝试筛去标注框中的背景点,然后计算并使用目标人的质心;
- 目前,该代码仅适用于录制好的数据。我们可以尝试对实时数据进行对象检测和距离计算。
但上述案例仍然能够非常典型的展示出Ouster Python SDK 是如何帮助客户将2D算法应用于3D空间数据上的。大家可以进行更多的尝试:
- 使用3个数据层(反射率+距离+近红外)而非1个来训练YOLOv5模型;
- 使用YOLOv5模型探测其他目标,例如汽车、自行车或行人,计算他们的间距或者时间,用于交通意外监测;
- 使用更大的 YOLOv5 模型 (例如 YOLOv5x) 以提高准确性,或更小的 YOLOv5 模型 (例如 YOLOv5n) 以获得更好的推理速度;
- 使用其他2D目标探测模型等 (例如 Transformers目标探测) 或2D像素语义分割模型 (例如 Nvidia LidarNet).
更多信息,请访问我们的 GitHub页面,期待与您共同探索创造!