光线投射传感器#
光线投射器(raycaster)以几何方式测量场景:它从传感器坐标系投射一组固定的光线,找出每条光线首次命中场景几何体的位置,并返回命中点及其距离。两种具体传感器共用这套机制,区别只在于如何解读结果:
gs.sensors.Lidar返回原始命中集合(点云与逐条光线的距离),用于测距、建图和避障。gs.sensors.DepthCamera投射针孔相机的光线网格,并把距离重塑为深度图像。
光线数量和方向由模式(gs.sensors.RaycastPattern)决定。本页以两个可运行示例为准:一个安装在 Go2 上的遥操作传感器,见 examples/sensors/lidar_teleop.py;以及作用在刚体和可变形几何体上的深度相机,见 examples/sensors/depth_camera_custom_vverts.py。
最小示例#
在机器人上安装一个旋转式激光雷达,给光线添加一个可供命中的物体,然后每一步读取点云:
import genesis as gs
gs.init(backend=gs.gpu)
scene = gs.Scene(show_viewer=True)
scene.add_entity(gs.morphs.Plane())
robot = scene.add_entity(gs.morphs.URDF(file="urdf/go2/urdf/go2.urdf", fixed=True))
scene.add_entity(gs.morphs.Box(size=(0.5, 0.5, 1.0), pos=(2.0, 0.0, 0.5), fixed=True))
lidar = scene.add_sensor(
gs.sensors.Lidar(
pattern=gs.sensors.SphericalPattern(), # 360deg x 60deg fov, 128 x 64 rays
entity_idx=robot.idx, # mount on the robot's base link
pos_offset=(0.3, 0.0, 0.1), # meters, in the link frame
max_range=20.0, # meters
return_world_frame=True,
draw_debug=True, # draw hit points in the viewer
)
)
scene.build()
for _ in range(1000):
scene.step()
result = lidar.read() # RaycasterReturnType(points, distances)
points = result.points # shape (128, 64, 3), meters, Z-up world frame
distances = result.distances # shape (128, 64), meters
激光雷达需要有几何体可供投射:如果刚体求解器未启用,且没有任何实体选择启用视觉光线投射,光线投射器会在构建阶段报错。
光线投射的工作原理#
模式是对光线的纯局部描述:它在传感器自身坐标系中为每条光线固定一个起点和一个单位方向,与传感器最终位于世界何处无关。模式的 return_shape(例如球面扫描的 (n_horizontal, n_vertical))决定每次读取都保持的布局。
在每次 read() 时,Genesis World 通过把所连接连杆的位姿与传感器的 pos_offset、euler_offset 组合,将模式放置到世界中,然后向基于场景几何体构建的加速结构投射每一条光线。每条光线报告它命中的第一个表面。若光线在 max_range 内未命中任何物体,或命中距离小于 min_range,则报告 no_hit_value(默认为 max_range),从而让返回的张量保持固定形状。
通过设置 entity_idx 和 link_idx_local 把传感器连接到连杆;光线随后随该连杆一起移动。若将 entity_idx 留空(或设为 None),传感器固定在世界坐标系中,此时 pos_offset 和 euler_offset 在世界坐标系下生效。
读取数据#
read() 返回 RaycasterReturnType,即由 points 和 distances 组成的 NamedTuple:
result = lidar.read()
points = result.points # shape ([n_envs,] *return_shape, 3), meters
distances = result.distances # shape ([n_envs,] *return_shape), meters
distances是沿光线方向的命中距离,单位为米,未命中处由no_hit_value填充。points是每条光线的命中位置。当return_world_frame=True时位于世界坐标系(Z 轴向上,单位米);默认为return_world_frame=False,此时位于传感器局部坐标系。return_points=False只测量命中距离:read().points为None,传感器的内存占用和每步开销降到约四分之一。仅需距离的传感(例如深度图像)可以用它;需要点云时保持默认值True。
末尾的轴与模式自身的布局一致:默认球面扫描为 (128, 64),深度相机为 (height, width),网格为 (n_x, n_y)。只有当场景在构建时包含多个环境,才会出现前导的 [n_envs,] 轴(参见多环境)。
光线模式#
模式决定了传感器的形态。在 gs.sensors 下构造以下三者中的任意一个,并作为 pattern 参数传入。
模式 |
光线布局 |
典型硬件 |
|---|---|---|
方位角 × 仰角扫描线 |
3D 激光雷达(Velodyne、Ouster) |
|
针孔图像网格 |
深度相机(RealSense、Kinect) |
|
平面上的平行光线 |
高度图、平面传感 |
SphericalPattern#
光线在水平(方位角)和垂直(仰角)视场角内展开。可以按光线数量、按角度分辨率或按显式角度数组来指定模式:
gs.sensors.SphericalPattern(
fov=(360.0, 60.0), # (horizontal, vertical) degrees; a scalar is centered on 0deg,
# a (min, max) tuple gives an asymmetric range
n_points=(128, 64), # (horizontal, vertical) ray counts -> return_shape (128, 64)
# angular_resolution=(0.25, 0.5), # degrees per ray; overrides n_points
# angles=(h_angles, v_angles), # explicit angle arrays; overrides the rest
)
要建模真实设备,可根据其数据手册设置 fov 和光线数量。例如 Velodyne VLP-16 为 fov=(360.0, 30.0), n_points=(1800, 16)。
DepthCameraPattern#
DepthCameraPattern 是一个针孔相机,其光轴为传感器的局部 +X 轴。可通过视场角或显式内参来配置:
gs.sensors.DepthCameraPattern(
res=(128, 96), # (width, height) in pixels -> return_shape (96, 128)
fov_horizontal=90.0, # degrees; fov_vertical is derived from the aspect ratio
# fov_vertical=None, # set instead to derive fov_horizontal, or set both
# fx=None, fy=None, # focal lengths in pixels, override the fov
# cx=None, cy=None, # principal point in pixels, defaults to the image center
)
GridPattern#
GridPattern 从平面上沿单一方向投射平行光线,例如传感器下方的高度图:
gs.sensors.GridPattern(
resolution=0.1, # grid spacing, meters (>= 1 mm)
size=(2.0, 2.0), # (length, width) of the grid, meters
direction=(0.0, 0.0, -1.0), # ray direction in the sensor frame (here: straight down)
)
自定义模式#
通过继承 gs.sensors.RaycastPattern 来投射任意光线集合:在 _get_return_shape() 中返回布局形状,并在 compute_ray_dirs() 中把传感器坐标系下的单位方向填入 _ray_dirs。
import torch
class CrossPattern(gs.sensors.RaycastPattern):
def _get_return_shape(self):
return (4,)
def compute_ray_dirs(self):
self._ray_dirs[:] = torch.tensor(
[
[1.0, 0.0, 0.0], # forward
[0.0, 1.0, 0.0], # left
[-1.0, 0.0, 0.0], # back
[0.0, -1.0, 0.0], # right
],
dtype=gs.tc_float,
device=gs.device,
)
sensor = scene.add_sensor(gs.sensors.Lidar(pattern=CrossPattern(), entity_idx=robot.idx))
深度相机#
gs.sensors.DepthCamera 是使用 DepthCameraPattern 的光线投射器。它提供激光雷达的全部能力,另外还有 read_image(),用于把逐条光线的距离重塑为深度图像:
depth_cam = scene.add_sensor(
gs.sensors.DepthCamera(
pattern=gs.sensors.DepthCameraPattern(
res=(96, 72), # (width, height) in pixels
fov_horizontal=90.0, # degrees
),
entity_idx=go2.idx,
link_idx_local=0,
pos_offset=(0.3, 0.0, 0.1), # meters, in the link frame
max_range=5.0, # meters
return_world_frame=True,
),
)
scene.build()
scene.step()
depth = depth_cam.read_image() # shape ([n_envs,] 72, 96), meters
read_image() 只返回 distances 字段,并重塑为 ([n_envs,] height, width);未命中处携带 no_hit_value。点云仍可通过 read().points 获取,除非创建相机时设置了 return_points=False——在只需要深度时,这会跳过点云以节省内存。由于深度相机与场景中的任何激光雷达共享同一套光线投射后端,二者针对相同的几何体投射。
运行 python examples/sensors/lidar_teleop.py --pattern depth 可在 Go2 上遥操作一台深度相机(--pattern 还接受 spherical 和 grid):
通用选项#
Lidar 和 DepthCamera 共用下列光线投射器选项(挂载字段对所有挂载式传感器通用):
gs.sensors.Lidar(
pattern=pattern,
entity_idx=robot.idx, # attach to this entity; omit for a world-fixed sensor
link_idx_local=0, # which link of the entity to attach to
pos_offset=(0.0, 0.0, 0.15), # meters, in the link frame
euler_offset=(0.0, 0.0, 0.0),# extrinsic x-y-z, degrees
min_range=0.0, # meters; hits closer than this are dropped
max_range=20.0, # meters
no_hit_value=None, # value for a miss; defaults to max_range
return_world_frame=False, # world frame if True, else the sensor's local frame
return_points=True, # False measures distances only; read().points is then None
draw_debug=False, # draw ray starts and hit points in the viewer
)
多环境#
使用 n_envs > 0 构建后,每次读取都会多出一个位于最前面的批量轴;模式布局保持不变:
scene.build(n_envs=4)
scene.step()
result = lidar.read()
result.points.shape # (4, 128, 64, 3)
result.distances.shape # (4, 128, 64)