3、RTX传感器之相机:Camera Prim创建、分辨率与帧率设置、RGB与深度图输出、相机内参标定

相机是仿真中最重要的传感器,没有之一。不管是做自动驾驶、机器人抓取,还是数字孪生,你都得先让机器「看见」世界。

在Isaac Sim里,RTX渲染管线下的相机传感器,说白了就是一套虚拟的物理相机。它能模拟真实相机的成像过程,包括镜头畸变、曝光、景深这些效果。我个人习惯把这种相机叫做「可编程的视觉传感器」——你给它什么参数,它就给你什么数据。

核心要点:RTX相机不是简单的截图工具。它通过路径追踪或光线追踪,逐像素计算光照、反射、阴影,生成接近真实照片的RGB图,同时还能同步输出深度、法线、语义分割等多通道数据。

3.1 Camera Prim的创建与基础配置

在Isaac Sim里,相机也是一个Prim(基本体)。你可以通过UI手动添加,也可以用Python脚本动态创建。我建议你在项目初期就用脚本方式创建,方便后续批量配置和复用。

3.1.1 手动创建方式

  1. 打开Stage面板,右键 → Create → Camera → Camera
  2. 选中新创建的Camera,在Property面板中调整Transform(位置、朝向)
  3. 在Render Settings中启用RTX渲染模式

3.1.2 脚本创建方式

我在项目中遇到过需要同时部署12个相机的情况,手动创建显然不现实。下面是我常用的脚本模板:

import omni.usd
from pxr import UsdGeom, Gf

# 获取当前Stage
stage = omni.usd.get_context().get_stage()

# 创建Camera Prim
camera_path = "/World/Camera_RGB"
camera_prim = UsdGeom.Camera.Define(stage, camera_path)

# 设置位置和朝向
camera_prim.AddTranslateOp().Set(Gf.Vec3d(2.0, 0.0, 1.5))
camera_prim.AddRotateXYZOp().Set(Gf.Vec3d(-30, 0, 0))

# 设置焦距(等效35mm相机)
camera_prim.GetFocalLengthAttr().Set(24.0)
camera_prim.GetFocusDistanceAttr().Set(100.0)
camera_prim.GetFStopAttr().Set(2.8)

print(f"Camera created at {camera_path}")
小技巧:创建相机后,记得在Viewport中按Ctrl+Shift+F将当前视角对齐到相机,方便预览成像效果。

3.2 分辨率与帧率设置

分辨率决定图像细节,帧率决定时间分辨率。两者直接决定了你的数据质量和计算开销。嗯,这里有个平衡点需要你根据实际场景来把握。

3.2.1 分辨率设置

分辨率通过相机的Resolution属性控制。我一般这样设置:

# 设置分辨率
camera_prim.GetHorizontalApertureAttr().Set(36.0)  # 水平孔径(mm)
camera_prim.GetVerticalApertureAttr().Set(24.0)    # 垂直孔径(mm)

# 更直接的方式:通过Render Product设置
from omni.syntheticdata import SyntheticData
synthetic_data = SyntheticData()
render_product = synthetic_data.create_render_product(camera_path, resolution=(1920, 1080))
应用场景 推荐分辨率 说明
目标检测 640×480 或 1280×720 平衡精度与速度
语义分割 1920×1080 需要边缘细节
深度估计 1024×768 深度图对分辨率不敏感
高保真渲染 3840×2160 (4K) 用于最终展示或标注

3.2.2 帧率控制

帧率设置其实是在Viewport或Render Product层面控制的。我曾经踩过一个坑:相机本身没有直接的帧率属性,帧率是由渲染循环控制的。

# 设置渲染帧率(通过Timeline控制)
import omni.timeline
timeline = omni.timeline.get_timeline_interface()
timeline.set_target_fps(30)  # 目标30FPS

# 或者通过Render Product设置采集间隔
render_product.set_execution_interval(2)  # 每2帧采集一次
注意:高分辨率+高帧率会极大消耗GPU资源。我测试过4K@60FPS的RTX渲染,一张RTX 4090只能同时跑2-3个相机。建议先做压力测试,再确定最终配置。

3.3 RGB与深度图输出

RTX相机最强大的地方在于,它可以同时输出多种数据通道。你不需要分别渲染RGB和深度图,一次渲染就能拿到所有数据。

3.3.1 配置数据输出

使用SyntheticData模块来配置输出通道:

from omni.syntheticdata import SyntheticData, SensorType

synthetic_data = SyntheticData()

# 创建Render Product
render_product = synthetic_data.create_render_product(
    camera_path, 
    resolution=(1280, 720)
)

# 启用RGB输出
synthetic_data.enable_sensor(
    render_product, 
    SensorType.RgbSensor, 
    "rgb_output"
)

# 启用深度图输出
synthetic_data.enable_sensor(
    render_product, 
    SensorType.DepthSensor, 
    "depth_output"
)

# 设置深度范围(米)
synthetic_data.set_sensor_property(
    render_product, 
    SensorType.DepthSensor, 
    "depth_range", 
    (0.1, 100.0)
)

3.3.2 数据获取与保存

数据采集通常放在渲染循环中。我习惯这样写:

import numpy as np
from PIL import Image

# 在渲染循环中获取数据
def on_frame():
    # 获取RGB数据(HWC格式,uint8)
    rgb_data = synthetic_data.get_sensor_data(
        render_product, 
        SensorType.RgbSensor
    )
    
    # 获取深度数据(HW格式,float32,单位米)
    depth_data = synthetic_data.get_sensor_data(
        render_product, 
        SensorType.DepthSensor
    )
    
    # 保存RGB图
    rgb_img = Image.fromarray(rgb_data)
    rgb_img.save(f"frame_{frame_count:06d}_rgb.png")
    
    # 保存深度图(归一化到0-255用于可视化)
    depth_normalized = (depth_data / depth_data.max() * 255).astype(np.uint8)
    depth_img = Image.fromarray(depth_normalized)
    depth_img.save(f"frame_{frame_count:06d}_depth.png")

# 注册回调
import omni.kit.app
app = omni.kit.app.get_app()
app.update_event_stream.append(on_frame)
经验之谈:深度图保存时,建议同时保存原始float32数据和归一化后的可视化图。原始数据用于算法处理,可视化图用于人工检查。我曾经因为只保存了可视化图,导致后续算法无法恢复真实距离,白白浪费了两天时间。

3.4 相机内参标定

仿真相机也需要标定?是的,你没看错。虽然仿真环境中的相机参数是已知的,但为了与真实相机对齐,或者为了验证标定算法,我们仍然需要「标定」仿真相机。

3.4.1 内参矩阵的获取

从Camera Prim中可以直接计算出内参矩阵:

import numpy as np

def get_camera_intrinsics(camera_prim, image_width, image_height):
    """从Camera Prim获取内参矩阵"""
    # 获取焦距(mm)
    focal_length = camera_prim.GetFocalLengthAttr().Get()
    
    # 获取传感器尺寸(mm)
    horizontal_aperture = camera_prim.GetHorizontalApertureAttr().Get()
    vertical_aperture = camera_prim.GetVerticalApertureAttr().Get()
    
    # 计算像素焦距
    fx = focal_length * image_width / horizontal_aperture
    fy = focal_length * image_height / vertical_aperture
    
    # 主点(通常为图像中心)
    cx = image_width / 2.0
    cy = image_height / 2.0
    
    # 构建内参矩阵
    K = np.array([
        [fx, 0, cx],
        [0, fy, cy],
        [0, 0, 1]
    ])
    
    return K

# 使用示例
K = get_camera_intrinsics(camera_prim, 1280, 720)
print("Camera Intrinsics:\n", K)

3.4.2 畸变参数

RTX相机默认是无畸变的。但如果你想模拟真实相机的畸变效果,可以手动添加:

# 设置径向畸变参数(k1, k2, k3)
camera_prim.GetProjectionAttr().Set("perspective")
# 注意:Isaac Sim原生不支持直接设置畸变参数
# 需要通过自定义Shader或后处理实现

# 替代方案:在数据后处理中添加畸变
def apply_distortion(rgb_image, k1=0.1, k2=-0.05):
    """模拟径向畸变"""
    # 这里可以使用OpenCV的cv2.undistort或自定义实现
    # 具体代码略,原理是像素坐标重映射
    pass
重要提醒:如果你需要精确的相机标定,建议在仿真环境中放置标定板(如棋盘格),然后用OpenCV的标定流程走一遍。这样得到的内参和畸变系数才是「真实」的仿真数据。我做过对比,理论计算和实际标定结果会有微小差异,主要来自渲染管线的近似处理。

3.5 知识结构总览

下面这张图总结了RTX相机配置的完整流程和关键节点,你可以把它当作操作地图:

RTX相机配置知识结构图 RTX相机配置知识结构图 Camera Prim创建 分辨率与帧率设置 RGB与深度图输出 相机内参标定 分辨率选择 帧率控制 RGB通道 深度通道 内参矩阵 畸变参数 关键流程:创建Prim → 配置分辨率/帧率 → 启用数据通道 → 获取内参 → 采集数据 所有配置均可通过Python脚本批量完成,建议建立标准配置模板

3.6 实战建议与避坑指南

  • 分辨率不是越高越好:我见过有人一上来就设4K,结果显存爆了。先根据你的GPU显存和场景复杂度做测试,再确定最终分辨率。
  • 深度图单位:Isaac Sim输出的深度图默认单位是米。如果你需要毫米或厘米,记得做单位转换。
  • 帧率与采集频率:仿真帧率和数据采集帧率可以不同。比如仿真跑60FPS,但数据采集只取30FPS,这样可以减少IO压力。
  • 内参一致性:如果你同时输出RGB和深度图,确保它们使用相同的内参矩阵。否则后续的RGB-D融合会出问题。
我的习惯:每次开始新项目前,我都会先跑一个「相机校准脚本」,输出一张包含内参、分辨率、帧率、数据格式的配置清单。这样团队协作时,每个人都知道数据是怎么来的,避免后期扯皮。