自动驾驶-M2 感知与 BEV-2.2 BEV 数据处理管线
采到的数据不能直接喂模型。这一节把磁盘上的 jpg + json, 变成模型能吃的张量批次——并且每一步都可视化验证, 确保没有静默错误。学习课程 端到端自动驾驶实战 | zhijia.dev
第一步:理解要产出什么

第二步:完整 Dataset 实现
bev_dataset.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
BEV 训练数据集 —— 读取 2.1 采集的数据,产出模型可用的张量
关键设计:
1. lidar2img 矩阵预计算(自车系 3D → 像素,BEVFormer 直接用)
2. 变长真值用自定义 collate_fn(每帧框数不同,不能 stack)
3. 数据增强时图像/点云/真值必须联动变换
"""
import json
import math
import os
import numpy as np
import cv2
import torch
from torch.utils.data import Dataset
# ImageNet 统计量(骨干网络预训练时用的,必须一致)
IMG_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
IMG_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
# CARLA(UE4) → 标准相机 的轴重排(1.4 节推导过)
UE4_TO_CAM = np.array([
[0, 1, 0],
[0, 0, -1],
[1, 0, 0],
], dtype=np.float64)
CATEGORIES = ["car", "pedestrian", "bicycle", "motorcycle"]
CAT2ID = {c: i for i, c in enumerate(CATEGORIES)}
def build_lidar2img(K, cam_translation, cam_yaw_deg):
"""
构造「自车系 3D 点 → 图像像素」的 4×4 变换矩阵
变换链(1.4 节讲过):
自车系 --相机外参逆--> CARLA相机系 --轴重排--> 标准相机系 --K--> 像素
返回 4×4 矩阵 M,用法:
p_img = M @ [x, y, z, 1]
u, v = p_img[0]/p_img[2], p_img[1]/p_img[2]
depth = p_img[2]
"""
# --- 相机在自车系的位姿(外参)---
yaw = math.radians(cam_yaw_deg)
c, s = math.cos(yaw), math.sin(yaw)
R_cam2ego = np.array([[c, -s, 0],
[s, c, 0],
[0, 0, 1]], dtype=np.float64)
t_cam2ego = np.array(cam_translation, dtype=np.float64)
# --- 求逆:自车系 → 相机系 ---
# 旋转矩阵正交,逆 = 转置
R_ego2cam = R_cam2ego.T
t_ego2cam = -R_ego2cam @ t_cam2ego
# --- 组合:轴重排 · 外参逆 ---
R_total = UE4_TO_CAM @ R_ego2cam
t_total = UE4_TO_CAM @ t_ego2cam
# --- 左乘内参 ---
K = np.array(K, dtype=np.float64)
M = np.eye(4, dtype=np.float64)
M[:3, :3] = K @ R_total
M[:3, 3] = K @ t_total
return M
def scale_intrinsic(K, scale_x, scale_y):
"""
图像缩放后,内参必须同步缩放 —— 这是最容易漏的一步
忘了这步的后果:投影全部错位,且不报错。
"""
K = np.array(K, dtype=np.float64).copy()
K[0, :] *= scale_x # fx, cx
K[1, :] *= scale_y # fy, cy
return K
class BEVDataset(Dataset):
def __init__(self, root, split="train", img_size=(800, 450),
pc_range=51.2, augment=False, val_ratio=0.2):
"""
root : 2.1 产出的数据集根目录
img_size : (W, H) 送入模型的尺寸
augment : 训练开启,验证关闭
"""
self.root = root
self.img_w, self.img_h = img_size
self.pc_range = pc_range
self.augment = augment
info = json.load(open(os.path.join(root, "dataset_info.json")))
self.cam_names = list(info["camera_rig"].keys())
self.src_w, self.src_h = info["image_size"]
# --- 建立样本索引 ---
# 按场景切分 train/val,避免同场景相邻帧泄漏到验证集
scenes = [s["name"] for s in info["scenes"]]
n_val = max(1, int(len(scenes) * val_ratio))
if split == "train":
use_scenes = scenes[:-n_val] if len(scenes) > n_val else scenes
else:
use_scenes = scenes[-n_val:]
self.samples = []
self.scene_calib = {}
for sc in use_scenes:
sdir = os.path.join(root, sc)
fpath = os.path.join(sdir, "frames.json")
if not os.path.exists(fpath):
continue
data = json.load(open(fpath))
self.scene_calib[sc] = json.load(
open(os.path.join(sdir, "calib.json")))
for fr in data["frames"]:
self.samples.append((sc, fr))
# --- 预计算 lidar2img(标定场景内固定,不必每次算)---
self.l2i_cache = {}
for sc, calib in self.scene_calib.items():
mats = []
sx = self.img_w / self.src_w
sy = self.img_h / self.src_h
for name in self.cam_names:
c = calib["cameras"][name]
K = scale_intrinsic(c["intrinsic"], sx, sy)
mats.append(build_lidar2img(
K, c["translation"], c["rotation_yaw_deg"]))
self.l2i_cache[sc] = np.stack(mats) # (6, 4, 4)
print(f"[{split}] {len(use_scenes)} 场景, {len(self.samples)} 帧")
def __len__(self):
return len(self.samples)
# ---------------- 核心 ----------------
def __getitem__(self, idx):
scene, frame = self.samples[idx]
sdir = os.path.join(self.root, scene)
fid = frame["index"]
# ---- 1) 读 6 路图像 ----
imgs = []
for name in self.cam_names:
path = os.path.join(sdir, name, f"{fid:06d}.jpg")
img = cv2.imread(path)
if img is None:
raise FileNotFoundError(path)
if (img.shape[1], img.shape[0]) != (self.img_w, self.img_h):
img = cv2.resize(img, (self.img_w, self.img_h))
# BGR → RGB,归一化到 [0,1] 再标准化
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
img = (img - IMG_MEAN) / IMG_STD
imgs.append(img.transpose(2, 0, 1)) # HWC → CHW
imgs = np.stack(imgs) # (6, 3, H, W)
# ---- 2) 读点云并裁剪到 BEV 范围 ----
lpath = os.path.join(sdir, "LIDAR_TOP", f"{fid:06d}.bin")
pts = np.fromfile(lpath, dtype=np.float32).reshape(-1, 4)
keep = ((np.abs(pts[:, 0]) < self.pc_range) &
(np.abs(pts[:, 1]) < self.pc_range))
pts = pts[keep]
# ---- 3) 真值 → 数组 ----
# 格式: [x, y, z, l, w, h, yaw] 共 7 维
boxes, labels = [], []
for b in frame["boxes"]:
if b["category"] not in CAT2ID:
continue
boxes.append(b["center"] + b["size"] + [b["yaw"]])
labels.append(CAT2ID[b["category"]])
boxes = (np.array(boxes, dtype=np.float32)
if boxes else np.zeros((0, 7), dtype=np.float32))
labels = (np.array(labels, dtype=np.int64)
if len(labels) else np.zeros((0,), dtype=np.int64))
lidar2img = self.l2i_cache[scene].copy() # (6, 4, 4)
# ---- 4) 数据增强(图像/点云/真值/矩阵 必须联动)----
if self.augment:
boxes, pts, lidar2img = self._augment(boxes, pts, lidar2img)
return {
"imgs": torch.from_numpy(imgs),
"lidar2img": torch.from_numpy(lidar2img.astype(np.float32)),
"points": torch.from_numpy(pts),
"gt_boxes": torch.from_numpy(boxes),
"gt_labels": torch.from_numpy(labels),
"meta": {"scene": scene, "frame": fid,
"ego_speed": frame.get("ego_speed", 0.0)},
}
# ---------------- 数据增强 ----------------
def _augment(self, boxes, pts, lidar2img):
"""
BEV 空间增强 —— 关键是「所有东西一起变」
只变真值不变 lidar2img,会导致图像和真值对不上,训练必然失败。
这是本节最容易踩的坑。
"""
# --- 随机绕 z 轴旋转 ---
angle = np.random.uniform(-np.pi / 12, np.pi / 12) # ±15°
c, s = np.cos(angle), np.sin(angle)
R = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]], dtype=np.float64)
# --- 随机缩放 ---
scale = np.random.uniform(0.95, 1.05)
# 组合成 4×4 变换(作用于自车系)
T = np.eye(4, dtype=np.float64)
T[:3, :3] = R * scale
# 1) 变换点云
if len(pts):
pts = pts.copy()
pts[:, :3] = (T[:3, :3] @ pts[:, :3].T).T
# 2) 变换真值框
if len(boxes):
boxes = boxes.copy()
boxes[:, :3] = (T[:3, :3] @ boxes[:, :3].T).T
boxes[:, 3:6] *= scale # 尺寸也要缩放
boxes[:, 6] += angle # 朝向加上旋转量
# 归一化 yaw 到 [-pi, pi]
boxes[:, 6] = np.arctan2(np.sin(boxes[:, 6]),
np.cos(boxes[:, 6]))
# 3) 关键:lidar2img 要右乘 T 的逆
# 因为 3D 点被 T 变换了,投影矩阵要反向补偿才能对应同一张图像
T_inv = np.linalg.inv(T)
lidar2img = lidar2img @ T_inv
return boxes, pts, lidar2img
# ---------------- 变长真值的 collate ----------------
def bev_collate_fn(batch):
"""
自定义批处理
为什么需要:每帧的框数不同(有的 3 个,有的 20 个),
默认 collate 会尝试 stack 成同形状张量而报错。
解法:定长的 stack,变长的保持 list。
"""
return {
# 定长 → stack
"imgs": torch.stack([b["imgs"] for b in batch]),
"lidar2img": torch.stack([b["lidar2img"] for b in batch]),
# 变长 → list
"points": [b["points"] for b in batch],
"gt_boxes": [b["gt_boxes"] for b in batch],
"gt_labels": [b["gt_labels"] for b in batch],
"meta": [b["meta"] for b in batch],
}
# ---------------- 快速自测 ----------------
if __name__ == "__main__":
import argparse
from torch.utils.data import DataLoader
ap = argparse.ArgumentParser()
ap.add_argument("--root", default="../dataset_v4")
args = ap.parse_args()
ds = BEVDataset(args.root, split="train", augment=True)
dl = DataLoader(ds, batch_size=2, shuffle=True,
collate_fn=bev_collate_fn, num_workers=2)
batch = next(iter(dl))
print("\n=== 批次结构 ===")
print(f"imgs : {tuple(batch['imgs'].shape)}")
print(f"lidar2img : {tuple(batch['lidar2img'].shape)}")
for i, (p, b) in enumerate(zip(batch["points"], batch["gt_boxes"])):
print(f"样本{i}: {p.shape[0]} 个点, {b.shape[0]} 个框")
# 数值健康检查
im = batch["imgs"]
print(f"\n图像统计: 均值 {im.mean():.3f}, 标准差 {im.std():.3f}")
print("(标准化后均值应接近 0,标准差接近 1)")
▸ 两个必须理解的设计
① 为什么按场景切 train/val,不按帧随机切?
因为同一场景相邻帧高度相似。随机切会让验证集里出现训练集的"近邻帧", 验证指标虚高,你以为模型很好,实际泛化很差。这是很隐蔽但很致命的数据泄漏。
② 增强时为什么 lidar2img 要右乘 T 的逆?
你把 3D 点旋转了 15°,但图像没有变。为了让"旋转后的 3D 点"仍能投影到 图像上的正确位置,投影矩阵必须做反向补偿。漏了这一步,图像和真值就对不上了。
执行:
(ad) leoking@leoking-desktop:~/carla/work/courses/M2/2.2$ python bev_dataset.py --root ../dataset_v4
[train] 1 场景, 50 帧
=== 批次结构 ===
imgs : (2, 6, 3, 450, 800)
lidar2img : (2, 6, 4, 4)
样本0: 20035 个点, 19 个框
样本1: 19902 个点, 14 个框
图像统计: 均值 0.034, 标准差 0.969
(标准化后均值应接近 0,标准差接近 1)
frames.json 数据
/home/xx/carla/work/courses/M2/dataset_v4/scene_0000/frames.json:
{"fps": 10, "pc_range": 51.2, "frames": [{"index": 0, "carla_frame": 399293, "timestamp": 0.0, "ego_pose": {"translation": [-2.791, -60.993, 0.002], "rotation": [-0.001, -0.038, 0.542]}, "ego_speed": 25.72, "num_lidar_points": 20303, "boxes": [{"category": "car", "center": [31.172, -7.207, 0.819], "size": [3.988, 1.851, 1.617], "yaw": 3.1325, "num_lidar_pts": 17, "distance": 31.99, "actor_id": 503}, {"category": "car", "center": [22.472, -3.624, 0.751], "size": [4.792, 2.163, 1.488], "yaw": 3.1322, "num_lidar_pts": 41, "distance": 22.76, "actor_id": 500}, {"category": "car", "center": [-38.734, 27.7, 1.887], "size": [8.468, 2.891, 3.827], "yaw": -1.5635, "num_lidar_pts": 54, "distance": 47.62, "actor_id": 499}, {"category": "car", "center": [-42.2, 20.402, 0.705], "size": [3.806, 1.97, 1.475], "yaw": -1.5709, "num_lidar_pts": 6, "distance": 46.87, "actor_id": 498}, {"category": "car", "center": [31.046, -0.2, 1.929], "size": [8.468, 2.891, 3.827], "yaw": -0.0104, "num_lidar_pts": 35, "distance": 31.05, "actor_id": 495}, {"category": "car", "center": [-38.771, 19.987, 0.734], "size": [3.633, 1.845, 1.501], "yaw": -1.5727, "num_lidar_pts": 5, "distance": 43.62, "actor_id": 494}, {"category": "car", "center": [17.445, -6.952, 2.134], "size": [10.273, 3.944, 4.253], "yaw": 3.1317, "num_lidar_pts": 397, "distance": 18.78, "actor_id": 493}, {"category": "car", "center": [6.822, -3.518, 0.754], "size": [4.902, 2.128, 1.511], "yaw": -3.1412, "num_lidar_pts": 516, "distance": 7.68, "actor_id": 492}, {"category": "car", "center": [18.29, 3.419, 0.81], "size": [3.988, 1.851, 1.617], "yaw": -0.0099, "num_lidar_pts": 59, "distance": 18.61, "actor_id": 491}, {"category": "car", "center": [14.802, -0.048, 0.95], "size": [4.605, 1.932, 1.855], "yaw": -0.0098, "num_lidar_pts": 74, "distance": 14.8, "actor_id": 490}, {"category": "car", "center": [-21.648, -0.051, 0.745], "size": [3.633, 1.845, 1.501], "yaw": 0.0009, "num_lidar_pts": 31, "distance": 21.65, "actor_id": 489}, {"category": "motorcycle", "center": [0.077, -7.056, 0.718], "size": [2.191, 0.866, 1.53], "yaw": -3.1406, "num_lidar_pts": 232, "distance": 7.06, "actor_id": 488}, {"category": "motorcycle", "center": [-21.505, 3.41, 0.775], "size": [1.817, 0.866, 1.59], "yaw": 0.001, "num_lidar_pts": 10, "distance": 21.77, "actor_id": 487}, {"category": "car", "center": [-7.924, -3.535, 0.712], "size": [4.674, 1.812, 1.442], "yaw": -3.1402, "num_lidar_pts": 334, "distance": 8.68, "actor_id": 486}, {"category": "car", "center": [-19.08, -6.99, 0.738], "size": [4.902, 2.128, 1.511], "yaw": -3.1298, "num_lidar_pts": 44, "distance": 20.32, "actor_id": 485}, {"category": "car", "center": [-17.68, -3.565, 1.197], "size": [6.366, 2.351, 2.431], "yaw": 3.1414, "num_lidar_pts": 128, "distance": 18.04, "actor_id": 484}, {"category": "car", "center": [-1.251, 3.498, 0.642], "size": [4.718, 1.895, 1.301], "yaw": 0.0001, "num_lidar_pts": 1247, "distance": 3.72, "actor_id": 483}], "rejected": [{"category": "bicycle", "center": [-49.077, 48.996, 0.846], "size": [1.843, 0.659, 1.776], "yaw": 1.561, "num_lidar_pts": 0, "distance": 69.35, "actor_id": 513}, {"category": "car", "center": [-42.121, 42.797, 0.713], "size": [4.892, 1.837, 1.49], "yaw": -1.5768, "num_lidar_pts": 0, "distance": 60.05, "actor_id": 511}, {"category": "car", "center": [-45.596, 48.626, 1.235], "size": [5.915, 1.988, 2.561], "yaw": 1.5609, "num_lidar_pts": 0, "distance": 66.66, "actor_id": 509}, {"category": "car", "center": [-38.64, 39.655, 0.69], "size": [4.674, 1.812, 1.442], "yaw": -1.5738, "num_lidar_pts": 1, "distance": 55.37, "actor_id": 507}, {"category": "bicycle", "center": [-49.234, 32.601, 0.763], "size": [1.656, 0.582, 1.62], "yaw": 1.5689, "num_lidar_pts": 0, "distance": 59.05, "actor_id": 505}, {"category": "car", "center": [-42.19, 30.363, 0.864], "size": [4.553, 2.097, 1.767], "yaw": -1.5678, "num_lidar_pts": 0, "distance": 51.98, "actor_id": 504}, {"category": "bicycle", "center": [-45.751, 30.968, 0.848], "size": [1.843, 0.659, 1.776], "yaw": 1.5573, "num_lidar_pts": 0, "distance": 55.25, "actor_id": 502}, {"category": "bicycle", "center": [-49.148, 22.854, 0.846], "size": [1.843, 0.659, 1.776], "yaw": 1.5732, "num_lidar_pts": 1, "distance": 54.2, "actor_id": 501}, {"category": "motorcycle", "center": [29.206, 3.315, 0.788], "size": [2.044, 0.797, 1.523], "yaw": -0.0099, "num_lidar_pts": 2, "distance": 29.39, "actor_id": 497}, {"category": "car", "center": [-45.673, 23.135, 0.755], "size": [5.366, 1.801, 1.575], "yaw": 1.5688, "num_lidar_pts": 0, "distance": 51.2, "actor_id": 496}, {"category": "pedestrian", "center": [-5.463, 10.815, 1.083], "size": [0.375, 0.375, 1.86], "yaw": 0.4986, "num_lidar_pts": 0, "distance": 12.12, "actor_id": 580},
debug:
第三步:可视化验证(本节最重要的一步)
写完管线不能直接开训。把处理后的数据画出来,肉眼确认对了再往下走。
verify_pipeline.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据管线验证 —— 把 Dataset 输出画出来,确认没有静默错误
产出两张图:
1. 六路图像 + 投影的 3D 框 → 验证 lidar2img 正确
2. BEV 俯视图(点云 + 框) → 验证真值坐标系正确
"""
import argparse
import os
import numpy as np
import cv2
from bev_dataset import BEVDataset, IMG_MEAN, IMG_STD, CATEGORIES
BOX_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)]
COLORS = [(80,220,120), (100,160,255),
(255,200,80), (255,120,200)]
def box_to_corners(box):
"""[x,y,z,l,w,h,yaw] → (8,3) 角点(自车系)"""
x, y, z, l, w, h, yaw = box
# 局部系下 8 个角点
xs = l / 2 * np.array([1,1,1,1,-1,-1,-1,-1])
ys = w / 2 * np.array([1,-1,-1,1,1,-1,-1,1])
zs = h / 2 * np.array([1,1,-1,-1,1,1,-1,-1])
corners = np.stack([xs, ys, zs]) # (3,8)
c, s = np.cos(yaw), np.sin(yaw)
R = np.array([[c,-s,0],[s,c,0],[0,0,1]])
corners = R @ corners
corners += np.array([[x],[y],[z]])
return corners.T # (8,3)
def denormalize(img_chw):
"""标准化的张量 → 可显示的 BGR 图"""
img = img_chw.transpose(1, 2, 0)
img = img * IMG_STD + IMG_MEAN
img = np.clip(img * 255, 0, 255).astype(np.uint8)
return cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
def draw_cameras(sample, cam_names, out_path):
"""六路图像 + 投影 3D 框,验证 lidar2img 是否正确"""
imgs = sample["imgs"].numpy()
l2i = sample["lidar2img"].numpy()
boxes = sample["gt_boxes"].numpy()
labels = sample["gt_labels"].numpy()
panels = []
for ci in range(len(cam_names)):
img = denormalize(imgs[ci])
h, w = img.shape[:2]
M = l2i[ci]
n_drawn = 0
for bi, box in enumerate(boxes):
corners = box_to_corners(box) # (8,3)
pts = np.hstack([corners, np.ones((8,1))]).T # (4,8)
proj = M @ pts # (4,8)
depth = proj[2]
# 全部角点必须在相机前方
if np.any(depth < 0.5):
continue
uv = proj[:2] / depth
# 至少部分在画面内
if (uv[0].max() < 0 or uv[0].min() > w or
uv[1].max() < 0 or uv[1].min() > h):
continue
color = COLORS[int(labels[bi]) % len(COLORS)]
for i, j in BOX_EDGES:
p1 = (int(uv[0,i]), int(uv[1,i]))
p2 = (int(uv[0,j]), int(uv[1,j]))
cv2.line(img, p1, p2, color, 2, cv2.LINE_AA)
n_drawn += 1
cv2.rectangle(img, (0,0), (250,26), (0,0,0), -1)
cv2.putText(img, f"{cam_names[ci]} ({n_drawn})", (6,18),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 1)
panels.append(img)
# 拼成 2×3 网格
while len(panels) < 6:
panels.append(np.zeros_like(panels[0]))
top = np.hstack(panels[:3])
bot = np.hstack(panels[3:6])
cv2.imwrite(out_path, np.vstack([top, bot]))
print(f" 相机视图 → {out_path}")
def draw_bev(sample, out_path, pc_range=51.2, size=800):
"""BEV 俯视图,验证真值坐标系是否正确"""
canvas = np.full((size, size, 3), 18, dtype=np.uint8)
scale = size / (2 * pc_range)
def to_px(x, y):
# 自车系 x 向前 → 画面向上;y 向右 → 画面向右
u = int(size / 2 + y * scale)
v = int(size / 2 - x * scale)
return u, v
# 距离环
for r in [10, 20, 30, 40, 50]:
cv2.circle(canvas, (size//2, size//2),
int(r*scale), (45,52,63), 1)
cv2.putText(canvas, f"{r}m",
(size//2+4, size//2-int(r*scale)+14),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (80,90,105), 1)
# 点云
pts = sample["points"].numpy()
for p in pts[::3]: # 抽稀加速
u, v = to_px(p[0], p[1])
if 0 <= u < size and 0 <= v < size:
canvas[v, u] = (70, 80, 95)
# 真值框(俯视 → 只画底面 4 角)
boxes = sample["gt_boxes"].numpy()
labels = sample["gt_labels"].numpy()
for bi, box in enumerate(boxes):
corners = box_to_corners(box)[:4] # 顶面 4 角
poly = np.array([to_px(c[0], c[1]) for c in corners], np.int32)
color = COLORS[int(labels[bi]) % len(COLORS)]
cv2.polylines(canvas, [poly], True, color, 2, cv2.LINE_AA)
# 朝向指示线(从中心指向车头)
cx, cy = to_px(box[0], box[1])
hx = box[0] + np.cos(box[6]) * box[3] / 2
hy = box[1] + np.sin(box[6]) * box[3] / 2
cv2.line(canvas, (cx,cy), to_px(hx,hy), color, 1, cv2.LINE_AA)
# 自车
cv2.rectangle(canvas,
(size//2-7, size//2-14),
(size//2+7, size//2+14), (255,190,60), -1)
# 信息
m = sample["meta"]
cv2.putText(canvas, f"{m['scene']} frame {m['frame']} | "
f"{len(boxes)} boxes | {len(pts)} pts",
(10, 22), cv2.FONT_HERSHEY_SIMPLEX,
0.5, (220,230,240), 1)
cv2.putText(canvas, "UP = vehicle forward (+x)",
(10, size-14), cv2.FONT_HERSHEY_SIMPLEX,
0.42, (110,120,135), 1)
cv2.imwrite(out_path, canvas)
print(f" BEV 视图 → {out_path}")
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--root", default="./dataset")
ap.add_argument("--out", default="./verify")
ap.add_argument("--n", type=int, default=3)
ap.add_argument("--augment", action="store_true")
args = ap.parse_args()
os.makedirs(args.out, exist_ok=True)
ds = BEVDataset(args.root, split="train", augment=args.augment)
for i in range(min(args.n, len(ds))):
idx = i * max(1, len(ds) // max(args.n, 1))
print(f"\n样本 {idx}:")
sample = ds[idx]
draw_cameras(sample, ds.cam_names,
os.path.join(args.out, f"{i:02d}_cameras.jpg"))
draw_bev(sample, os.path.join(args.out, f"{i:02d}_bev.jpg"))
print("\n检查要点:")
print(" ① 相机图上的框应精确套住车辆 → lidar2img 正确")
print(" ② BEV 图上框应贴合点云簇 → 真值坐标系正确")
print(" ③ 朝向线应指向车头方向 → yaw 定义正确")
print(" ④ 开 --augment 后两图仍应对齐 → 增强联动正确")
执行:
(ad) leoking@leoking-desktop:~/carla/work/courses/M2/2.2$ python verify_pipeline.py --root ../dataset_v4 --out ./verify
[train] 1 场景, 50 帧
样本 0:
相机视图 → ./verify/00_cameras.jpg
BEV 视图 → ./verify/00_bev.jpg
样本 16:
相机视图 → ./verify/01_cameras.jpg
BEV 视图 → ./verify/01_bev.jpg
样本 32:
相机视图 → ./verify/02_cameras.jpg
BEV 视图 → ./verify/02_bev.jpg
检查要点:
① 相机图上的框应精确套住车辆 → lidar2img 正确
② BEV 图上框应贴合点云簇 → 真值坐标系正确
③ 朝向线应指向车头方向 → yaw 定义正确
④ 开 --augment 后两图仍应对齐 → 增强联动正确
查看输出结果:


为者常成,行者常至
自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)