首页 / 资讯中心 / 文章详情

YOLO+深度估计实现低成本3D目标检测:从原理到边缘部署

YOLO+深度估计实现低成本3D目标检测:从原理到边缘部署 ★ FEATURED ARTICLE
简介一套YOLO与深度估计相结合的3D目标检测实现面向自动驾驶、机器人导航与安全监控等需要空间感知的场景适合计算机视觉研究人员、算法工程师及相关专业学生用作实战参考。压缩包共7个文件包含5个Python脚本分别覆盖深度估计模型、检测模型、相机参数加载、3D边界框解算工具与主运行入口另有依赖清单文件和说明文档用于环境配置与使用指引整个包仅20KB结构紧凑。目前已有119人学习下载。通过运行源码可以理解如何借助深度信息把YOLO输出的二维边界框扩展为三维框掌握从相机标定参数读取、深度图生成到三维坐标解算的完整链路并能在现有代码基础上调整网络结构或针对不同检测目标进行优化。项目中模块化的设计也便于单独复用深度估计或检测部分为后续二次开发节省时间。1. 基于YOLO深度估计的3D目标检测不要激光雷达也能出3D框很多人一听3D目标检测第一反应就是激光雷达点云。实际上基于YOLO深度估计做3D目标检测是这两年低成本视觉方案里冒头很快的一条路——拿一个普通RGB摄像头让YOLO先把物体框出来再用单目深度估计模型把框内像素的深度捞出来两路结果一拼就能反投影出带位置和尺寸的3D框。它能解决什么问题给预算买不起激光雷达、又不想上双目视觉标定的项目一个快速跑通的3D感知起点。适合谁已经在用YOLO做2D检测、想往3D方向升级的算法工程师以及要在树莓派、RK3588这类边缘设备上做视觉避障的开发者。2. 从2D框到3D空间YOLO检测头、深度估计与反投影拆解这条技术路线的核心思路很直白2D检测负责“知道物体在哪一坨像素里”深度估计负责“知道这一坨像素离我多远”剩下的坐标变换只是数学。但真到落地时三个环节各自都有坑先把原理拆透后面调参才不慌。2.1 YOLO侧输出解析坐标、置信度与类别过滤YOLO在这套方案里的角色是提供“图像平面上的位置先验”。以最常见的YOLOv8为例用ultralytics库加载一个yolov8n.pt预训练模型推理一张图后拿到的结果不是一张图而是一个对象集合。里面有检测框坐标、置信度和类别id这三个字段决定了后面深度计算要用哪些像素。from ultralytics import YOLO # 加载预训练权重首次使用会自动下载到 ~/.cache/ultralytics/ model YOLO(yolov8n.pt) results model.predict(scene.jpg, conf0.35, imgsz640, verboseFalse) for r in results: # xyxy: 左上角(x1,y1)和右下角(x2,y2)单位是像素 boxes_xyxy r.boxes.xyxy.cpu().numpy() # conf: 每个框的置信度范围0~1 confs r.boxes.conf.cpu().numpy() # cls: 类别id需要用names映射成字符串 cls_ids r.boxes.cls.cpu().numpy().astype(int) names r.names这段代码先把YOLO输出从GPU张量搬到CPU并转成numpy后续做深度图裁剪会频繁索引这些坐标numpy比Tensor方便得多。有两个参数要特别留意。conf0.35对室内大目标够用但到了遮挡严重的交通场景我会压到0.25代价是误检变多。imgsz640是精度和速度之间的默认平衡点想提小目标召回就上960但显存占用和推理耗时会明显增加。另外如果你用自己的数据集重训过模型yolo损失函数里的box_loss权重会直接影响框的紧致度——框定得松松垮垮后面裁出来的深度点云就会混入背景像素3D框自然不准。2.2 深度估计侧模型选型MiDaS与Depth Anything的取舍单目深度估计这几年迭代很快从MiDaS到DPT再到Depth Anything。在基于YOLO深度估计做3D检测的项目里深度模型的选择直接决定“框的深度准不准”而几乎不影响“框出现在图像哪里”。MiDaS是稳定且历史包袱最少的选择torch.hub一行能加载输出是相对深度图。Depth Anything是2024年之后常见的替代品特别是small版本能在不损失太多精度的情况下跑出接近实时的帧率而且对物体边缘的保留比MiDaS好。如果你的运行环境显存很小直接用Depth Anything small即可。from transformers import pipeline # 加载Depth Anything small版本约25M参数量级 depth_estimator pipeline( taskdepth-estimation, modelLiheYoung/depth-anything-small-hf, device0 ) result depth_estimator(scene.jpg) depth result[depth] # PIL Image需要转成numpy import numpy as np depth_map np.array(depth).astype(np.float32)这里走的是HuggingFace的pipeline好处是不用自己拼预处理。depth返回的PIL Image尺寸和输入图一致直接np.array就能得到单通道深度图。注意这个输出不是真实的米制深度而是归一化的相对深度。基于YOLO深度估计做3D检测的常见做法是先把它当作比例深度用后面要么做尺度校准要么直接用相对深度做避障。如果你在项目文档里看到“YOLO和Transformer结合”这种描述多半不是YOLO结构本身改了而是深度估计侧换成了ViT类主干——Depth Anything的encoder就是ViT所以这条链路严格说是YOLO加Transformer深度分支。2.3 融合与反投影把2D框变成3D点云的完整计算深度图和检测框都拿到后最稳的融合方式不是只取框中心的深度值而是把整张深度图反投影成相机坐标下的点云再用2D框裁剪出物体点云最后对点云做统计。这样能抵消深度图在单个像素上的噪点。import numpy as np def depth_to_points(depth_map, fx, fy, cx, cy): 把单通道深度图转成相机坐标的点云输出形状(H, W, 3) h, w depth_map.shape # 生成像素网格 u np.arange(w).reshape(1, -1).repeat(h, axis0) v np.arange(h).reshape(-1, 1).repeat(w, axis1) # 像素坐标转相机坐标(u-cx)/fx 把像素换算成长度单位 x (u - cx) * depth_map / fx y (v - cy) * depth_map / fy z depth_map points np.stack([x, y, z], axis-1) return points def box_to_object(points, box_xyxy): 从点云里切出2D框对应的物体点云返回中心点和三维尺寸 x1, y1, x2, y2 box_xyxy.astype(int) obj_points points[y1:y2, x1:x2].reshape(-1, 3) # 过滤无效深度和异常点 valid_mask ( np.isfinite(obj_points).all(axis1) (obj_points[:, 2] 0) ) obj_points obj_points[valid_mask] # 中心用中位数尺寸用5%~95%分位数避免离群点拉偏 center np.median(obj_points, axis0) low np.percentile(obj_points, 5, axis0) high np.percentile(obj_points, 95, axis0) size high - low return center, size逻辑说明depth_to_points里的公式就是针孔相机模型(u-cx)/fx把像素坐标换算到相机坐标系再乘上深度z就得到三维坐标。这里先把整张深度图转成一次点云后面每个框只需要做一次裁剪比每框单独反投影更省计算。参数说明fx、fy是相机焦距单位是像素cx、cy是主点一般接近图像中心。如果你没有标定数据用fxfy图像宽度乘1.2这种估算值也能跑通但别指望高精度。z0这个过滤条件看着多余但很多深度模型在图像边缘会输出负值或无穷大这步能直接避免后续nan污染。注意单目方案只能确定物体中心到相机的深度尺寸里z方向物体厚度对单帧图像来说本质上是不唯一的因为照片里看不到物体背面。工程上要么接受厚度误差要么给物体预设先验长宽比后面验证章节会专门提这个问题。3. 复现与跑通环境搭建、权重准备与端到端推理最小脚本原理归原理真到复现源码包时环境配置往往是劝退第一步。这套基于YOLO深度估计的项目实战环境最怕的是torch版本和CUDA版本对不上以及深度模型权重下载路径不明确。3.1 yolo环境搭建conda依赖与torch版本搭配常见做法是用conda单独建一个python 3.10环境避免污染系统Python。安装顺序有讲究先装torch再装ultralytics。conda create -n yolo3d python3.10 -y conda activate yolo3d # 按自己GPU驱动选CUDA版本这里以CUDA 11.8为例 pip install torch2.3.1 torchvision0.18.1 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python numpy matplotlib pip install transformers为什么先装torch因为ultralytics会声明torch依赖如果你先pip install ultralytics它会顺手拉一个CPU版本的torch后面推理速度会让你怀疑人生。先装好GPU版本的torchultralytics检测到torch已存在就不会重复装。参数说明torch版本和CUDA的对应关系是yolo环境配置里最常见的翻车点。RTX 30系显卡配cu118没问题40系建议cu121。拿不准就看nvidia-smi里的CUDA Version字段那个数字代表驱动支持的最高版本向下兼容。conda环境名取yolo3d是为了和base环境隔离。3.2 权重与内参准备自动下载路径和相机参数估算YOLO权重基本不用手动操心。第一次执行YOLO(yolov8n.pt)时ultralytics会自动下载预训练模型到~/.cache/ultralytics/weights/。如果你想离线部署去ultralytics官方assets页手动下载yolov8n.pt拷贝到那目录即可。深度模型如果走transformers权重会落在~/.cache/huggingface/hub/。网络不稳定时建议提前把权重文件拷贝到离线机器省得每次都要重新拉。这两个cache目录是源码包跑不通时最该先查的地方——很多人报错“下载失败”其实是根本没注意到权重在后台下载。相机内参没有标定数据时用估算公式fx_pixel focal_mm * image_width / sensor_width_mm。多数手机主摄焦距在26mm左右传感器宽度大约6.4mm所以fx约等于image_width乘4。但手机sensor的裁切情况各异这个计算值只能用来跑通流程不能直接做高精度定位。3.3 端到端推理脚本检测、深度、反投影一段跑完把上面环节拼成一个脚本我一般会保持单文件结构方便调试。import cv2 import numpy as np from ultralytics import YOLO from transformers import pipeline # 复用2.3节的两个函数定义 def depth_to_points(depth_map, fx, fy, cx, cy): ... def box_to_object(points, box_xyxy): ... # 1. 初始化检测和深度模型 yolo YOLO(yolov8n.pt) depth_pipe pipeline( taskdepth-estimation, modelLiheYoung/depth-anything-small-hf, device0 ) # 2. 读图YOLO保持BGR深度模型用RGB img cv2.imread(scene.jpg) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 3. 检测传入原图返回的xyxy坐标已映射回原图坐标系 results yolo.predict(img, conf0.35, imgsz640, verboseFalse) boxes results[0].boxes.xyxy.cpu().numpy().astype(int) # 4. 深度估计深度图尺寸与输入图一致 depth_out depth_pipe(img_rgb) depth_map np.array(depth_out[depth]).astype(np.float32) # 5. 坐标对齐确认框坐标范围和深度图尺寸 print(num boxes:, len(boxes)) print(depth shape:, depth_map.shape) # 6. 反投影fx、fy按图像宽度估算 h, w depth_map.shape fx fy w * 1.2 cx, cy w / 2.0, h / 2.0 points depth_to_points(depth_map, fx, fy, cx, cy) # 7. 输出每个目标的3D中心与尺寸 for box in boxes: center, size box_to_object(points, box) print(中心:, center, 尺寸:, size)逻辑说明YOLO传原图返回的xyxy已经经过缩放回原图坐标系的处理所以检测框坐标和深度图天然对齐。这里不要手动把图resize成640x640再送进去那样坐标就错位了。depth_pipe输出的depth_map尺寸取决于输入图因此整个链路里唯一的坐标参考系就是原图。参数说明这一步用估算内参跑通后如果发现3D框的宽高比明显失真物体是方的却被拉成扁的多半是fx和fy的比例不对而不是代码问题。常见做法是先核对内参再调深度平滑顺序别反了。4. 参数联动调优置信度、深度平滑与相机内参的三处联动很多源码包能跑通但出的3D框“没法用”问题往往不在模型而在三个参数没联动起来。置信度决定了哪些框值得算深度深度平滑决定了框内点云干不干净内参决定了反投影的形状对不对。4.1 置信度与类别过滤什么场景该把conf压到0.3以下置信度影响的是“哪些框进入深度计算”。框错了后面的3D全错。室内大目标conf0.5够用但自动驾驶场景目标小且遮挡多压到0.25能多召回不少代价是多出来的误检框会带来深度估计上的杂讯。# 只保留行人和车且放宽置信度 results yolo.predict(img, conf0.25, classes[0, 2], verboseFalse)classes参数在源码包里的使用频率不高但非常实用。如果你只关心行人和车直接过滤掉其他类别能省掉很多误检框也不会让猫狗干扰深度计算。如果你拿自己的数据集微调过YOLOclasses参数同样适用自定义类别id只是id顺序要和训练时的data.yaml保持一致。置信度和类别过滤调完之后如果3D框数量还是太多我会检查是不是有大量重叠框。YOLO默认的NMS阈值iou0.45可以降到0.3让重叠框合并得更激进但要注意两个挨得很近的物体可能会被合并成一个框。4.2 深度图平滑中值滤波与双边滤波的窗口参数深度图噪声是3D框抖动的最大来源。常见做法是先用中值滤波把孤立噪点去掉再考虑是否上双边滤波保边缘。# 中值滤波窗口5x5去孤立噪点 depth_blur cv2.medianBlur(depth_map, 5) # 双边滤波保留边缘的同时平滑 depth_bf cv2.bilateralFilter(depth_map, d7, sigmaColor50, sigmaSpace7)中值滤波窗口3x3去噪弱但保留细节5x5见效快但会让边缘变圆。双边滤波的sigmaColor设多大取决于深度图的值域。Depth Anything这类模型输出的深度图是0~255的归一化值sigmaColor50合适如果你已经做了尺度恢复转成米制sigmaColor得调成0.05这种量级否则边缘会被磨平。还有一个容易被忽略的操作对框内点云做统计之前先去掉深度值为0或接近0的像素。很多模型在图像边缘和遮挡边界会输出无效深度这些像素一旦混进框内中位数都会被打偏。4.3 相机内参影响标定棋盘格与估算公式怎么选内参影响的是x和y方向的比例关系。用估算值时3D框在近距离还能看远距离就会因为fx不准而整体偏移。如果有条件用棋盘格标定更可靠十几张不同角度的照片就够了。import cv2 # 棋盘格内角点数常见9x6 checker_size (9, 6) objp np.zeros((np.prod(checker_size), 3), np.float32) objp[:, :2] np.mgrid[0:9, 0:6].T.reshape(-1, 2) objpoints, imgpoints [], [] for fname in calibration_imgs: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, checker_size, None) if ret: objpoints.append(objp) imgpoints.append(corners) _, K, dist, _, _ cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None) print(内参矩阵:\n, K)逻辑说明K矩阵里的K[0,0]就是fxK[1,1]是fyK[0,2]和K[1,2]是cx、cy。标定一次能管很久只要镜头焦距不变。如果你的镜头是变焦的标定结果只在固定焦距下有效变焦之后必须重新标定。我的血泪经验是不要为了省事用估算内参去验证算法精度否则你会花三天时间怀疑深度模型有问题最后发现只是fx偏了20%。5. 避坑记录YOLO深度估计实战中的5个典型翻车现场这个方向看起来简单实际跑起来处处是坑。我按自己踩过的顺序整理五条每条都按现象、原因、解决的顺序说清楚。5.1 检测框和深度图错位3D框漂到背景里现象3D框不从物体上长出来而是歪到物体右侧或下方的背景上框的中心点明显偏了半个身位。原因这是复现源码包时最常见的低级错误。有的人会在送进YOLO前手动把图resize成640x640YOLO输出的框坐标是resize后的坐标而深度图还是原图像素两边坐标系不一致反投影出来的位置自然偏。解决不要手动resize。直接用ultralytics的predict传原图它内部做letterbox后会把坐标映射回原图坐标系。如果非要自己预处理必须把预处理前后的缩放比例和padding记录下来在得到框坐标后做一次逆映射。判断是否错位有一个快速方法打印boxes里的最大x坐标再对比深度图宽度如果框的最大x比深度图宽度还大说明坐标参考系没对上。5.2 3D框尺寸跳变相邻帧长宽高不一致现象同一个物体连着拍几帧3D框的长宽高每帧差异超过30%看起来像物体在呼吸。原因直接在框内点云上取min和max来算尺寸。深度图里只要有一个离群点比如远处背景像素混进框内框就会被瞬间拉长一倍。解决把min/max改成5%到95%的分位数同时对深度图做中值滤波。改完这两个地方尺寸跳变能压下去大半。如果还跳检查输入帧是不是有运动模糊运动模糊会直接毁掉深度估计的边缘质量。5.3 小目标深度噪声大近处行人定位都偏现象框越小中心深度和真实深度差得越多行人贴着脸走过3D框却在两米开外。原因深度模型对纹理稀少的小目标不敏感框内有效像素本来就少少数错误深度值在统计里占的比重大中位数也救不回来。解决框内点云先做一次离群剔除用IQR四分位距过滤掉低于Q1-1.5IQR和高于Q31.5IQR的点再取中位数。另一招是限制最小框面积面积小于16x16像素的框直接跳过深度计算用前后帧的深度差值来补。对小目标时序信息比单帧深度可靠得多。5.4 加了深度模型后FPS崩盘推理速度比想象中慢现象单独跑YOLO有50FPS加上深度估计后只剩5FPS视频根本没法实时处理。原因深度模型没有开半精度也没有换轻量版backbone是ViT时计算量比YOLO大一个量级尤其在树莓派这类边缘设备上CPU跑transformer就是灾难。解决先换small版本深度模型再开fp16半精度推理输入分辨率降到384x288。在树莓派上我习惯先把整帧降采样做深度再把深度图放大回原尺寸反投影精度损失不大速度提升明显。如果是RK3588这类带NPU的板子优先把深度模型转成RKNN格式再部署比在CPU上硬跑快一个数量级。5.5 深度模型尺度不一致同一物体远近比例对不上现象物体从1米移到3米3D框的z方向变化不是3倍而是忽大忽小。原因单目深度估计输出的是相对深度没有物理尺度。不同模型、不同输入尺寸下的相对深度比例都不一致。这不是bug是这类方案的先天限制。解决选一个知道“尺度”的模型或者用已知尺寸物体校准。一个常见做法是拍一张已知高度的杆子用它的像素高度和深度图反推出全局scale 真实高度 / 深度图高度然后把整张深度图乘上这个scale。这个scale只在同一个相机和同一个深度模型参数下有效换模型就要重新标。尺度校准这块有点玄学但它确实是整个3D检测精度最关键的杠杆。6. 进阶验证与压缩用KITTI地面真值评估3D框并把模型塞进边缘设备6.1 BEV视角的IoU验证跑通之后怎么证明3D框是准的常见做法是用KITTI数据集的3D标注做鸟瞰图BEVIoU验证。KITTI的3D框带rotation_y旋转角需要先把预测框和真实框投影到地面平面再算两个多边形的IoU。from shapely.geometry import Polygon def bev_iou(box_a, box_b): box_a/box_b: 四个地面角点坐标按顺时针排列 poly_a Polygon(box_a) poly_b Polygon(box_b) inter poly_a.intersection(poly_b).area union poly_a.union(poly_b).area return inter / union if union 0 else 0不用追求KITTI官方排行榜的分数单目方案本来就不是为那个赛道设计的。只要BEV IoU能在近处达到0.5以上就说明反投影链路没有系统性错误。注意KITTI相机的内参是已知的换到你自己的相机上必须重新标定否则验证结果没有参考意义。6.2 模型压缩与边缘部署把模型塞进边缘设备的核心技巧深度模型不需要每帧全分辨率推理。对固定相机场景先用低分辨率深度图做一次粗略估计再在检测框内的小patch上做高分辨率refine速度和精度都能照顾到。配合半精度转换常见做法是把深度模型导出成ONNX再用TensorRT或RKNN做量化FP16下模型体积和显存占用几乎减半。部署到监控设备上如果发现误检率高先别急着换大模型检查是不是帧率不足导致运动模糊模糊帧里的深度估计基本不可信。我在第一个3D项目里犯过的最大错误是上来就追求高精度买了标定板、上了大模型结果在真实产线上发现最难的不是精度差几个百分点而是深度尺度不稳定。后来我把深度分支换成轻量模型、把输入分辨率降下来先让整个链路每帧都能跑再回头慢慢抠精度项目才真正交付出去。如果你在复现基于YOLO深度估计的3D目标检测项目时遇到类似问题不妨先从最小可行闭环开始把尺度校准和坐标系对齐做扎实再谈模型升级。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站