4. 基于Camera的3D目标检测
任务目标
本案例实现基于单目相机的3D目标检测:输入一张RGB图像,通过深度学习模型估计图像中目标的3D中心位置(x, y, z)、尺寸(w, h, l)和朝向角(yaw),并输出3D边界框。为便于独立验证,案例采用轻量化的Mono3D思路——使用预训练的2D检测器获取目标框,再通过几何投影关系回归3D参数,避免依赖大规模3D标注训练。
环境与依赖
- Python 3.8+,PyTorch 1.10+,OpenCV 4.5+,NumPy
- 预训练模型权重文件(本案例提供下载链接占位,实际使用时需替换为真实权重路径)
- 相机内参矩阵(K)作为输入参数传入
核心实现
代码实现完整的单目3D检测推理流程:读取图像 → 2D检测(此处用简单颜色阈值模拟检测框,便于独立运行) → 从2D框回归3D参数 → 将3D框投影回图像验证。核心创新点在于利用2D框底部中心与3D投影点的几何约束,通过最小二乘求解目标深度。
import cv2
import numpy as np
import torch
import torch.nn as nn
# ---------- 1. 轻量3D回归网络 ----------
class Mono3DHead(nn.Module):
"""从2D检测框特征回归3D参数: 尺寸(w,h,l), 朝向(θ), 深度z"""
def __init__(self, feat_dim=512):
super().__init__()
self.fc = nn.Sequential(
nn.Linear(feat_dim, 256),
nn.ReLU(),
nn.Linear(256, 7) # [w, h, l, sin(θ), cos(θ), z_offset, 置信度]
)
def forward(self, x):
return self.fc(x)
# ---------- 2. 几何投影与3D框构建 ----------
def compute_3d_box(center, dims, yaw):
"""根据中心点、尺寸和朝向角生成3D框的8个角点"""
w, h, l = dims
x, y, z = center
R = np.array([[np.cos(yaw), 0, np.sin(yaw)],
[0, 1, 0],
[-np.sin(yaw), 0, np.cos(yaw)]])
# 局部坐标系下的8个角点
corners = np.array([
[l/2, h/2, w/2], [l/2, h/2, -w/2], [-l/2, h/2, -w/2], [-l/2, h/2, w/2],
[l/2, -h/2, w/2], [l/2, -h/2, -w/2], [-l/2, -h/2, -w/2], [-l/2, -h/2, w/2]
])
# 旋转并平移到全局坐标
corners = corners @ R.T + center
return corners
def project_to_image(points_3d, K):
"""将3D点投影到2D图像平面"""
if points_3d.shape[0] == 0:
return np.zeros((0, 2))
proj = points_3d @ K.T
proj = proj[:, :2] / proj[:, 2:3]
return proj
# ---------- 3. 深度估计(几何约束) ----------
def estimate_depth(box_2d, dims, K, cam_height=1.5):
"""
利用2D框底部中心与相机光心的几何关系估计深度
box_2d: [x1, y1, x2, y2] 像素坐标
"""
x1, y1, x2, y2 = box_2d
bottom_center = np.array([(x1+x2)/2, y2])
# 假设目标底部位于地平面 (y=0)
# 相机高度cam_height, 相机俯仰角近似0
fx = K[0, 0]
fy = K[1, 1]
cx = K[0, 2]
cy = K[1, 2]
# 目标底部在图像中的垂直偏移
v = bottom_center[1] - cy
# 深度估计: Z = (fy * cam_height) / (v - fy * tan(pitch)),pitch≈0
if v < 0.1:
v = 0.1
z = (fy * cam_height) / v
return z
# ---------- 4. 主推理流程 ----------
def detect_3d_from_image(image_path, K, model, device='cpu'):
"""
完整推理: 图像 → 2D检测 → 3D参数回归 → 3D框投影
返回: 3D框角点(8x3), 2D投影点(8x2), 可视化图像
"""
# 读取图像
img = cv2.imread(image_path)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
h, w = img.shape[:2]
# ---- 2D检测(演示用:基于颜色的简单目标检测) ----
# 实际应用中替换为YOLO/SSD等检测器
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 检测红色目标(示例)
mask = cv2.inRange(hsv, (0, 100, 100), (10, 255, 255))
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) == 0:
return None, None, img
# 取最大轮廓作为目标
largest = max(contours, key=cv2.contourArea)
x, y, bw, bh = cv2.boundingRect(largest)
box_2d = [x, y, x+bw, y+bh]
# ---- 提取2D框特征(此处用框内像素均值模拟CNN特征) ----
roi = img_rgb[y:y+bh, x:x+bw]
if roi.size == 0:
return None, None, img
feat = cv2.resize(roi, (32, 32)).flatten().astype(np.float32) / 255.0
feat_tensor = torch.from_numpy(feat).unsqueeze(0).to(device)
# ---- 3D参数回归 ----
model.eval()
with torch.no_grad():
params = model(feat_tensor).cpu().numpy().flatten()
w3d, h3d, l3d = params[0], params[1], params[2]
sin_yaw, cos_yaw = params[3], params[4]
yaw = np.arctan2(sin_yaw, cos_yaw)
z_offset = params[5]
conf = params[6]
# ---- 深度估计与3D中心计算 ----
z = estimate_depth(box_2d, (w3d, h3d, l3d), K)
# 根据2D框中心计算x, y
cx_img = (box_2d[0] + box_2d[2]) / 2
cy_img = (box_2d[1] + box_2d[3]) / 2
x = (cx_img - K[0, 2]) * z / K[0, 0]
y = (cy_img - K[1, 2]) * z / K[1, 1] + z_offset # 修正高度偏移
center = np.array([x, y, z])
dims = np.array([w3d, h3d, l3d])
# ---- 生成3D框并投影 ----
corners_3d = compute_3d_box(center, dims, yaw)
corners_2d = project_to_image(corners_3d, K)
# ---- 可视化 ----
vis_img = img.copy()
# 绘制2D检测框
cv2.rectangle(vis_img, (x, y), (x+bw, y+bh), (0, 255, 0), 2)
# 绘制3D框投影(连接8个角点)
edges = [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),
(0,4),(1,5),(2,6),(3,7)]
for i, j in edges:
pt1 = tuple(corners_2d[i].astype(int))
pt2 = tuple(corners_2d[j].astype(int))
cv2.line(vis_img, pt1, pt2, (255, 0, 0), 2)
return corners_3d, corners_2d, vis_img
# ---------- 5. 主程序入口 ----------
if __name__ == "__main__":
# 相机内参(示例值,需根据实际相机标定)
K = np.array([[800, 0, 640],
[0, 800, 360],
[0, 0, 1]], dtype=np.float32)
# 初始化模型(实际使用时加载预训练权重)
model = Mono3DHead(feat_dim=32*32*3)
# model.load_state_dict(torch.load('mono3d_weights.pth'))
# 运行检测
corners_3d, corners_2d, vis = detect_3d_from_image('test_image.jpg', K, model)
if corners_3d is not None:
print("3D框角点坐标:")
print(corners_3d)
print("\n投影到图像的2D坐标:")
print(corners_2d)
cv2.imwrite('result_3d.jpg', vis)
print("\n结果已保存至 result_3d.jpg")
else:
print("未检测到目标")
运行与验证
- 准备输入:将包含红色目标的图像命名为
test_image.jpg放在脚本同目录 - 运行:
python mono3d_detection.py - 验证方法:
- 观察输出图像
result_3d.jpg,蓝色线条应构成一个立体框,且底部贴合红色目标的2D检测框 - 检查终端输出的3D角点坐标:z值应为正(目标在相机前方),尺寸参数应接近真实目标比例
- 通过条件:3D框投影后,其底部4个角点与2D检测框底部边缘基本重合,顶部角点位于目标上方
- 观察输出图像
关键说明
- 本案例的2D检测部分使用颜色阈值模拟,实际工程中应替换为YOLO等检测器输出;特征提取使用像素均值模拟CNN特征,实际需接入ResNet等骨干网络
- 深度估计采用地平面假设(目标底部位于y=0平面),适用于车载相机平视场景;若相机有俯仰角需标定并补偿
- 模型输出未经真实训练,仅演示完整推理链路。实际使用时需用KITTI等数据集训练Mono3DHead,或加载官方预训练权重