4、Carla仿真结果可视化与报告生成:使用Matplotlib绘制LiDAR点云(2D鸟瞰图与3D散点图)

做自动驾驶仿真,最头疼的事情是什么?

我个人觉得,不是算法调不通,而是你跑了一整天仿真,最后拿什么跟老板汇报?

光说「我的模型效果不错」,谁信啊?

你得拿出图来,拿出数据来。尤其是LiDAR点云这种三维数据,光看数字根本看不出门道。所以今天这一讲,我们就来聊聊怎么用Matplotlib把Carla仿真出来的LiDAR点云,变成漂亮的2D鸟瞰图和3D散点图。

4.1 为什么非得可视化LiDAR点云?

我在做第一个自动驾驶项目时,犯过一个低级错误。

当时我直接从Carla的传感器接口拿数据,丢给模型训练。跑了三天三夜,loss降得挺漂亮。结果一上路测试,车直接往树上撞。

后来我回头一看原始点云——好家伙,LiDAR的安装角度偏了5度,车顶的点云全打到天上去了。如果当时我先把点云画出来看一眼,这三天时间根本不会浪费。

所以,可视化不是「锦上添花」,而是「保命手段」。

核心观点: 点云可视化能帮你快速发现数据采集问题、标定误差、以及场景覆盖盲区。这是调试自动驾驶系统的第一道防线。

4.2 Carla中LiDAR数据的原始格式

先说说Carla给我们的数据长什么样。

Carla的LiDAR传感器返回的是一个numpy数组,形状是(N, 4)。N是点的数量,4个通道分别是:

通道索引 含义 说明
0 X 前向距离(米)
1 Y 侧向距离(米)
2 Z 高度(米)
3 Intensity 反射强度(0-1)

嗯,这里要注意:Carla的坐标系是右手系,X朝前,Y朝左,Z朝上。跟ROS的坐标系不太一样,画图时别搞混了。

4.3 2D鸟瞰图:把三维压成二维

鸟瞰图(BEV,Bird's Eye View)说白了就是从上往下看。我们把X和Y保留,把Z丢掉或者用颜色表示。

为什么先做鸟瞰图?

因为快。3D图转起来卡得要命,但2D图一秒钟就能画出来。调试阶段,我习惯先用鸟瞰图扫一眼。

4.3.1 基础鸟瞰图代码

import matplotlib.pyplot as plt
import numpy as np

def plot_bev_basic(points, title="LiDAR Bird's Eye View"):
    """
    绘制基础鸟瞰图
    points: (N, 4) numpy数组
    """
    # 提取X和Y
    x = points[:, 0]
    y = points[:, 1]
    
    # 用反射强度作为颜色
    intensity = points[:, 3]
    
    plt.figure(figsize=(10, 10))
    scatter = plt.scatter(x, y, c=intensity, cmap='viridis', 
                         s=1, alpha=0.6)
    plt.colorbar(scatter, label='Intensity')
    plt.xlabel('X (m)')
    plt.ylabel('Y (m)')
    plt.title(title)
    plt.axis('equal')  # 保持比例
    plt.grid(True, alpha=0.3)
    plt.show()

这段代码很简单,但有个坑——我刚开始用的时候,忘记加axis('equal'),结果画出来的车是扁的,怎么看怎么不对劲。

小技巧: 鸟瞰图一定要加 axis('equal'),否则X和Y的比例不一致,你看到的障碍物形状是扭曲的。

4.3.2 带高度信息的鸟瞰图

有时候光看强度不够,你还想知道哪些点是地面、哪些点是障碍物。这时候可以用Z值来着色。

def plot_bev_height(points, z_min=-2, z_max=3):
    """
    用高度着色的鸟瞰图
    """
    x = points[:, 0]
    y = points[:, 1]
    z = points[:, 2]
    
    plt.figure(figsize=(10, 10))
    scatter = plt.scatter(x, y, c=z, cmap='jet', 
                         s=1, alpha=0.6,
                         vmin=z_min, vmax=z_max)
    plt.colorbar(scatter, label='Height (m)')
    plt.xlabel('X (m)')
    plt.ylabel('Y (m)')
    plt.title('BEV with Height Coloring')
    plt.axis('equal')
    plt.grid(True, alpha=0.3)
    plt.show()

你想想看,用高度着色之后,地面点(Z≈0)是绿色的,车辆点(Z≈1.5-2)是红色的,树冠(Z>3)是紫色的。一眼就能看出场景里有什么。

4.4 3D散点图:还原真实空间感

鸟瞰图虽然快,但丢失了高度信息。有时候你需要看一个障碍物的真实轮廓,比如一辆卡车到底有多高、会不会刮到天桥——这时候就得用3D散点图。

4.4.1 基础3D散点图

from mpl_toolkits.mplot3d import Axes3D

def plot_3d_scatter(points, title="LiDAR 3D Point Cloud"):
    """
    绘制3D散点图
    """
    x = points[:, 0]
    y = points[:, 1]
    z = points[:, 2]
    intensity = points[:, 3]
    
    fig = plt.figure(figsize=(12, 10))
    ax = fig.add_subplot(111, projection='3d')
    
    scatter = ax.scatter(x, y, z, c=intensity, 
                        cmap='viridis', s=1, alpha=0.5)
    
    ax.set_xlabel('X (m)')
    ax.set_ylabel('Y (m)')
    ax.set_zlabel('Z (m)')
    ax.set_title(title)
    plt.colorbar(scatter, label='Intensity')
    plt.show()

这段代码跑起来,你会看到一个可以拖拽旋转的3D点云。说实话,我第一次看到自己采集的点云在屏幕上转起来的时候,还挺激动的。

注意: 3D散点图在点云数量超过10万点时,Matplotlib会变得非常卡。Carla的32线LiDAR一帧大约3.2万个点,勉强能跑。如果是64线或128线,建议先降采样再画。

4.4.2 降采样技巧

我曾经在项目里直接画了128线LiDAR的满帧点云,结果电脑卡了五分钟。后来学乖了,加了个降采样:

def downsample_points(points, target_count=20000):
    """
    随机降采样点云
    """
    if len(points) <= target_count:
        return points
    
    indices = np.random.choice(len(points), target_count, replace=False)
    return points[indices]

嗯,这里要注意:降采样会丢失细节。如果你要检查某个小障碍物,最好还是用原始数据。

4.5 进阶:给点云加上语义信息

如果你在Carla里开了语义LiDAR(Semantic LiDAR),那数据里还带了一个通道——语义标签。我们可以用不同颜色表示不同物体:

def plot_semantic_bev(points, labels, label_map):
    """
    绘制语义鸟瞰图
    points: (N, 4)
    labels: (N,) 语义标签
    label_map: 字典,{标签ID: 颜色}
    """
    x = points[:, 0]
    y = points[:, 1]
    
    colors = np.array([label_map.get(l, [0.5, 0.5, 0.5]) 
                       for l in labels])
    
    plt.figure(figsize=(10, 10))
    plt.scatter(x, y, c=colors, s=1, alpha=0.6)
    plt.xlabel('X (m)')
    plt.ylabel('Y (m)')
    plt.title('Semantic BEV')
    plt.axis('equal')
    plt.grid(True, alpha=0.3)
    plt.show()

这样做的好处是,你可以一眼看出「这堆点到底是车还是树」。我在做障碍物检测时,经常用这个图来验证标注数据的质量。

4.6 保存图片到报告

画完图,总得保存下来吧?

def save_figure(fig, filename, dpi=150):
    """
    保存图片,自动创建目录
    """
    import os
    os.makedirs(os.path.dirname(filename), exist_ok=True)
    fig.savefig(filename, dpi=dpi, bbox_inches='tight')
    print(f"Figure saved to {filename}")

我个人习惯把每帧的鸟瞰图和3D图都存下来,然后写个脚本批量生成报告。这样老板问起来,直接甩一个PDF过去,省事。

4.7 完整示例:从Carla数据到可视化

最后,给你一个完整的流程:

# 假设你已经从Carla拿到了LiDAR数据
lidar_data = np.random.randn(30000, 4)  # 模拟数据
lidar_data[:, 3] = np.abs(lidar_data[:, 3])  # 强度非负

# 1. 降采样
sampled = downsample_points(lidar_data, 20000)

# 2. 画鸟瞰图
plot_bev_basic(sampled, "My First BEV")

# 3. 画3D图
plot_3d_scatter(sampled, "My First 3D Point Cloud")

# 4. 保存
fig = plt.gcf()
save_figure(fig, "output/bev_sample.png")

你试试看,跑完这段代码,你就能看到自己的第一个点云可视化结果了。

我的建议: 刚开始别追求花哨。先把基础鸟瞰图和3D图跑通,再慢慢加颜色、加语义、加交互。一步一个脚印,比什么都强。

好了,这一讲就到这里。下一讲我们会聊聊怎么把这些图片组织成一份像样的PDF报告。到时候见。