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("未检测到目标")

运行与验证

  1. 准备输入:将包含红色目标的图像命名为 test_image.jpg 放在脚本同目录
  2. 运行:python mono3d_detection.py
  3. 验证方法:
    • 观察输出图像 result_3d.jpg,蓝色线条应构成一个立体框,且底部贴合红色目标的2D检测框
    • 检查终端输出的3D角点坐标:z值应为正(目标在相机前方),尺寸参数应接近真实目标比例
    • 通过条件:3D框投影后,其底部4个角点与2D检测框底部边缘基本重合,顶部角点位于目标上方

关键说明

  • 本案例的2D检测部分使用颜色阈值模拟,实际工程中应替换为YOLO等检测器输出;特征提取使用像素均值模拟CNN特征,实际需接入ResNet等骨干网络
  • 深度估计采用地平面假设(目标底部位于y=0平面),适用于车载相机平视场景;若相机有俯仰角需标定并补偿
  • 模型输出未经真实训练,仅演示完整推理链路。实际使用时需用KITTI等数据集训练Mono3DHead,或加载官方预训练权重