Skip to content

Commit f0699d3

Browse files
authored
Adds IMU sensor (isaac-sim#619)
# Description Add `IMU` sensor with cfg class `IMUCfg` and data class `IMUData`. Compared to the Isaac Sim implementation of the IMU Sensor, this sensor directly accesses the PhysX view buffers for speed acceleration. This PR also moves and renames a utility used for cameras to a general utility location. Fixes isaac-sim#440 ## Type of change - New feature (non-breaking change which adds functionality) - Breaking change ( ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there
1 parent 7ea2407 commit f0699d3

File tree

18 files changed

+1447
-160
lines changed

18 files changed

+1447
-160
lines changed

docs/source/api/lab/omni.isaac.lab.sensors.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
RayCasterCfg
3232
RayCasterCamera
3333
RayCasterCameraCfg
34+
Imu
35+
ImuCfg
3436

3537
Sensor Base
3638
-----------
@@ -150,3 +152,17 @@ Ray-Cast Camera
150152
:inherited-members:
151153
:show-inheritance:
152154
:exclude-members: __init__, class_type
155+
156+
Inertia Measurement Unit
157+
------------------------
158+
159+
.. autoclass:: Imu
160+
:members:
161+
:inherited-members:
162+
:show-inheritance:
163+
164+
.. autoclass:: ImuCfg
165+
:members:
166+
:inherited-members:
167+
:show-inheritance:
168+
:exclude-members: __init__, class_type

source/extensions/omni.isaac.lab/config/extension.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22

33
# Note: Semantic Versioning is used: https://semver.org/
4-
version = "0.25.2"
4+
version = "0.26.0"
55

66
# Description
77
title = "Isaac Lab framework for Robot Learning"

source/extensions/omni.isaac.lab/docs/CHANGELOG.rst

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
Changelog
22
---------
33

4+
0.26.0 (2024-10-16)
5+
~~~~~~~~~~~~~~~~~~~
6+
7+
Added
8+
^^^^^
9+
10+
* Added Imu sensor implementation that directly accesses the physx view :class:`omni.isaac.lab.sensors.Imu`. The
11+
sensor comes with a configuration class :class:`omni.isaac.lab.sensors.ImuCfg` and data class
12+
:class:`omni.isaac.lab.sensors.ImuData`.
13+
* Moved and renamed :meth:`omni.isaac.lab.sensors.camera.utils.convert_orientation_convention` to :meth:`omni.isaac.lab.utils.math.convert_camera_frame_orientation_convention`
14+
* Moved :meth:`omni.isaac.lab.sensors.camera.utils.create_rotation_matrix_from_view` to :meth:`omni.isaac.lab.utils.math.create_rotation_matrix_from_view`
15+
16+
417
0.25.2 (2024-10-16)
5-
~~~~~~~~~~~~~~~~~~~~
18+
~~~~~~~~~~~~~~~~~~~
619

720
Added
821
^^^^^

source/extensions/omni.isaac.lab/omni/isaac/lab/envs/mdp/observations.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import omni.isaac.lab.utils.math as math_utils
1818
from omni.isaac.lab.assets import Articulation, RigidObject
1919
from omni.isaac.lab.managers import SceneEntityCfg
20-
from omni.isaac.lab.sensors import Camera, RayCaster, RayCasterCamera, TiledCamera
20+
from omni.isaac.lab.sensors import Camera, Imu, RayCaster, RayCasterCamera, TiledCamera
2121

2222
if TYPE_CHECKING:
2323
from omni.isaac.lab.envs import ManagerBasedEnv, ManagerBasedRLEnv
@@ -182,6 +182,48 @@ def body_incoming_wrench(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg) -> tor
182182
return link_incoming_forces.view(env.num_envs, -1)
183183

184184

185+
def imu_orientation(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("imu")) -> torch.Tensor:
186+
"""Imu sensor orientation w.r.t the env.scene.origin.
187+
188+
Args:
189+
env: The environment.
190+
asset_cfg: The SceneEntity associated with an Imu sensor.
191+
192+
Returns:
193+
Orientation quaternion (wxyz), shape of torch.tensor is (num_env,4).
194+
"""
195+
asset: Imu = env.scene[asset_cfg.name]
196+
return asset.data.quat_w
197+
198+
199+
def imu_ang_vel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("imu")) -> torch.Tensor:
200+
"""Imu sensor angular velocity w.r.t. env.scene.origin expressed in the sensor frame.
201+
202+
Args:
203+
env: The environment.
204+
asset_cfg: The SceneEntity associated with an Imu sensor.
205+
206+
Returns:
207+
Angular velocity (rad/s), shape of torch.tensor is (num_env,3).
208+
"""
209+
asset: Imu = env.scene[asset_cfg.name]
210+
return asset.data.ang_vel_b
211+
212+
213+
def imu_lin_acc(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("imu")) -> torch.Tensor:
214+
"""Imu sensor linear acceleration w.r.t. env.scene.origin expressed in sensor frame.
215+
216+
Args:
217+
env: The environment.
218+
asset_cfg: The SceneEntity associated with an Imu sensor.
219+
220+
Returns:
221+
linear acceleration (m/s^2), shape of torch.tensor is (num_env,3).
222+
"""
223+
asset: Imu = env.scene[asset_cfg.name]
224+
return asset.data.lin_acc_b
225+
226+
185227
def image(
186228
env: ManagerBasedEnv,
187229
sensor_cfg: SceneEntityCfg = SceneEntityCfg("tiled_camera"),

source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@
3030
+---------------------+---------------------------+---------------------------------------------------------------+
3131
| Frame Transformer | /World/robot/base | Leaf exists and is a physics body (Articulation / Rigid Body) |
3232
+---------------------+---------------------------+---------------------------------------------------------------+
33+
| Imu | /World/robot/base | Leaf exists and is a physics body (Rigid Body) |
34+
+---------------------+---------------------------+---------------------------------------------------------------+
3335
3436
"""
3537

3638
from .camera import * # noqa: F401, F403
3739
from .contact_sensor import * # noqa: F401, F403
3840
from .frame_transformer import * # noqa: F401
41+
from .imu import * # noqa: F401, F403
3942
from .ray_caster import * # noqa: F401, F403
4043
from .sensor_base import SensorBase # noqa: F401
4144
from .sensor_base_cfg import SensorBaseCfg # noqa: F401

source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from typing import TYPE_CHECKING, Any, Literal
1414

1515
import carb
16+
import omni.isaac.core.utils.stage as stage_utils
1617
import omni.kit.commands
1718
import omni.usd
1819
from omni.isaac.core.prims import XFormPrimView
@@ -21,11 +22,14 @@
2122
import omni.isaac.lab.sim as sim_utils
2223
from omni.isaac.lab.utils import to_camel_case
2324
from omni.isaac.lab.utils.array import convert_to_torch
24-
from omni.isaac.lab.utils.math import quat_from_matrix
25+
from omni.isaac.lab.utils.math import (
26+
convert_camera_frame_orientation_convention,
27+
create_rotation_matrix_from_view,
28+
quat_from_matrix,
29+
)
2530

2631
from ..sensor_base import SensorBase
2732
from .camera_data import CameraData
28-
from .utils import convert_orientation_convention, create_rotation_matrix_from_view
2933

3034
if TYPE_CHECKING:
3135
from .camera_cfg import CameraCfg
@@ -116,7 +120,9 @@ def __init__(self, cfg: CameraCfg):
116120
if self.cfg.spawn is not None:
117121
# compute the rotation offset
118122
rot = torch.tensor(self.cfg.offset.rot, dtype=torch.float32).unsqueeze(0)
119-
rot_offset = convert_orientation_convention(rot, origin=self.cfg.offset.convention, target="opengl")
123+
rot_offset = convert_camera_frame_orientation_convention(
124+
rot, origin=self.cfg.offset.convention, target="opengl"
125+
)
120126
rot_offset = rot_offset.squeeze(0).numpy()
121127
# ensure vertical aperture is set, otherwise replace with default for squared pixels
122128
if self.cfg.spawn.vertical_aperture is None:
@@ -289,7 +295,7 @@ def set_world_poses(
289295
- :obj:`"ros"` - forward axis: +Z - up axis -Y - Offset is applied in the ROS convention
290296
- :obj:`"world"` - forward axis: +X - up axis +Z - Offset is applied in the World Frame convention
291297
292-
See :meth:`omni.isaac.lab.sensors.camera.utils.convert_orientation_convention` for more details
298+
See :meth:`omni.isaac.lab.sensors.camera.utils.convert_camera_frame_orientation_convention` for more details
293299
on the conventions.
294300
295301
Args:
@@ -318,7 +324,7 @@ def set_world_poses(
318324
orientations = torch.from_numpy(orientations).to(device=self._device)
319325
elif not isinstance(orientations, torch.Tensor):
320326
orientations = torch.tensor(orientations, device=self._device)
321-
orientations = convert_orientation_convention(orientations, origin=convention, target="opengl")
327+
orientations = convert_camera_frame_orientation_convention(orientations, origin=convention, target="opengl")
322328
# set the pose
323329
self._view.set_world_poses(positions, orientations, env_ids)
324330

@@ -339,8 +345,10 @@ def set_world_poses_from_view(
339345
# resolve env_ids
340346
if env_ids is None:
341347
env_ids = self._ALL_INDICES
348+
# get up axis of current stage
349+
up_axis = stage_utils.get_stage_up_axis()
342350
# set camera poses using the view
343-
orientations = quat_from_matrix(create_rotation_matrix_from_view(eyes, targets, device=self._device))
351+
orientations = quat_from_matrix(create_rotation_matrix_from_view(eyes, targets, up_axis, device=self._device))
344352
self._view.set_world_poses(eyes, orientations, env_ids)
345353

346354
"""
@@ -596,7 +604,9 @@ def _update_poses(self, env_ids: Sequence[int]):
596604
# get the poses from the view
597605
poses, quat = self._view.get_world_poses(env_ids)
598606
self._data.pos_w[env_ids] = poses
599-
self._data.quat_w_world[env_ids] = convert_orientation_convention(quat, origin="opengl", target="world")
607+
self._data.quat_w_world[env_ids] = convert_camera_frame_orientation_convention(
608+
quat, origin="opengl", target="world"
609+
)
600610

601611
def _create_annotator_data(self):
602612
"""Create the buffers to store the annotator data.

source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/camera_data.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from tensordict import TensorDict
99
from typing import Any
1010

11-
from .utils import convert_orientation_convention
11+
from omni.isaac.lab.utils.math import convert_camera_frame_orientation_convention
1212

1313

1414
@dataclass
@@ -77,7 +77,7 @@ def quat_w_ros(self) -> torch.Tensor:
7777
7878
Shape is (N, 4) where N is the number of sensors.
7979
"""
80-
return convert_orientation_convention(self.quat_w_world, origin="world", target="ros")
80+
return convert_camera_frame_orientation_convention(self.quat_w_world, origin="world", target="ros")
8181

8282
@property
8383
def quat_w_opengl(self) -> torch.Tensor:
@@ -89,4 +89,4 @@ def quat_w_opengl(self) -> torch.Tensor:
8989
9090
Shape is (N, 4) where N is the number of sensors.
9191
"""
92-
return convert_orientation_convention(self.quat_w_world, origin="world", target="opengl")
92+
return convert_camera_frame_orientation_convention(self.quat_w_world, origin="world", target="opengl")

source/extensions/omni.isaac.lab/omni/isaac/lab/sensors/camera/utils.py

Lines changed: 0 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,11 @@
88
# needed to import for allowing type-hinting: torch.device | str | None
99
from __future__ import annotations
1010

11-
import math
1211
import numpy as np
1312
import torch
14-
import torch.nn.functional as F
1513
from collections.abc import Sequence
16-
from typing import Literal
1714

18-
import omni.isaac.core.utils.stage as stage_utils
1915
import warp as wp
20-
from pxr import UsdGeom
2116

2217
import omni.isaac.lab.utils.math as math_utils
2318
from omni.isaac.lab.utils.array import TensorData, convert_to_torch
@@ -262,143 +257,6 @@ def create_pointcloud_from_rgbd(
262257
return points_xyz, points_rgb
263258

264259

265-
def convert_orientation_convention(
266-
orientation: torch.Tensor,
267-
origin: Literal["opengl", "ros", "world"] = "opengl",
268-
target: Literal["opengl", "ros", "world"] = "ros",
269-
) -> torch.Tensor:
270-
r"""Converts a quaternion representing a rotation from one convention to another.
271-
272-
In USD, the camera follows the ``"opengl"`` convention. Thus, it is always in **Y up** convention.
273-
This means that the camera is looking down the -Z axis with the +Y axis pointing up , and +X axis pointing right.
274-
However, in ROS, the camera is looking down the +Z axis with the +Y axis pointing down, and +X axis pointing right.
275-
Thus, the camera needs to be rotated by :math:`180^{\circ}` around the X axis to follow the ROS convention.
276-
277-
.. math::
278-
279-
T_{ROS} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & -1 & 0 & 0 \\ 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} T_{USD}
280-
281-
On the other hand, the typical world coordinate system is with +X pointing forward, +Y pointing left,
282-
and +Z pointing up. The camera can also be set in this convention by rotating the camera by :math:`90^{\circ}`
283-
around the X axis and :math:`-90^{\circ}` around the Y axis.
284-
285-
.. math::
286-
287-
T_{WORLD} = \begin{bmatrix} 0 & 0 & -1 & 0 \\ -1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} T_{USD}
288-
289-
Thus, based on their application, cameras follow different conventions for their orientation. This function
290-
converts a quaternion from one convention to another.
291-
292-
Possible conventions are:
293-
294-
- :obj:`"opengl"` - forward axis: -Z - up axis +Y - Offset is applied in the OpenGL (Usd.Camera) convention
295-
- :obj:`"ros"` - forward axis: +Z - up axis -Y - Offset is applied in the ROS convention
296-
- :obj:`"world"` - forward axis: +X - up axis +Z - Offset is applied in the World Frame convention
297-
298-
Args:
299-
orientation: Quaternion of form `(w, x, y, z)` with shape (..., 4) in source convention
300-
origin: Convention to convert to. Defaults to "ros".
301-
target: Convention to convert from. Defaults to "opengl".
302-
303-
Returns:
304-
Quaternion of form `(w, x, y, z)` with shape (..., 4) in target convention
305-
"""
306-
if target == origin:
307-
return orientation.clone()
308-
309-
# -- unify input type
310-
if origin == "ros":
311-
# convert from ros to opengl convention
312-
rotm = math_utils.matrix_from_quat(orientation)
313-
rotm[:, :, 2] = -rotm[:, :, 2]
314-
rotm[:, :, 1] = -rotm[:, :, 1]
315-
# convert to opengl convention
316-
quat_gl = math_utils.quat_from_matrix(rotm)
317-
elif origin == "world":
318-
# convert from world (x forward and z up) to opengl convention
319-
rotm = math_utils.matrix_from_quat(orientation)
320-
rotm = torch.matmul(
321-
rotm,
322-
math_utils.matrix_from_euler(
323-
torch.tensor([math.pi / 2, -math.pi / 2, 0], device=orientation.device), "XYZ"
324-
),
325-
)
326-
# convert to isaac-sim convention
327-
quat_gl = math_utils.quat_from_matrix(rotm)
328-
else:
329-
quat_gl = orientation
330-
331-
# -- convert to target convention
332-
if target == "ros":
333-
# convert from opengl to ros convention
334-
rotm = math_utils.matrix_from_quat(quat_gl)
335-
rotm[:, :, 2] = -rotm[:, :, 2]
336-
rotm[:, :, 1] = -rotm[:, :, 1]
337-
return math_utils.quat_from_matrix(rotm)
338-
elif target == "world":
339-
# convert from opengl to world (x forward and z up) convention
340-
rotm = math_utils.matrix_from_quat(quat_gl)
341-
rotm = torch.matmul(
342-
rotm,
343-
math_utils.matrix_from_euler(
344-
torch.tensor([math.pi / 2, -math.pi / 2, 0], device=orientation.device), "XYZ"
345-
).T,
346-
)
347-
return math_utils.quat_from_matrix(rotm)
348-
else:
349-
return quat_gl.clone()
350-
351-
352-
# @torch.jit.script
353-
def create_rotation_matrix_from_view(
354-
eyes: torch.Tensor,
355-
targets: torch.Tensor,
356-
device: str = "cpu",
357-
) -> torch.Tensor:
358-
"""
359-
This function takes a vector ''eyes'' which specifies the location
360-
of the camera in world coordinates and the vector ''targets'' which
361-
indicate the position of the object.
362-
The output is a rotation matrix representing the transformation
363-
from world coordinates -> view coordinates.
364-
365-
The inputs camera_position and targets can each be a
366-
- 3 element tuple/list
367-
- torch tensor of shape (1, 3)
368-
- torch tensor of shape (N, 3)
369-
370-
Args:
371-
eyes: position of the camera in world coordinates
372-
targets: position of the object in world coordinates
373-
374-
The vectors are broadcast against each other so they all have shape (N, 3).
375-
376-
Returns:
377-
R: (N, 3, 3) batched rotation matrices
378-
379-
Reference:
380-
Based on PyTorch3D (https://github.com/facebookresearch/pytorch3d/blob/eaf0709d6af0025fe94d1ee7cec454bc3054826a/pytorch3d/renderer/cameras.py#L1635-L1685)
381-
"""
382-
up_axis_token = stage_utils.get_stage_up_axis()
383-
if up_axis_token == UsdGeom.Tokens.y:
384-
up_axis = torch.tensor((0, 1, 0), device=device, dtype=torch.float32).repeat(eyes.shape[0], 1)
385-
elif up_axis_token == UsdGeom.Tokens.z:
386-
up_axis = torch.tensor((0, 0, 1), device=device, dtype=torch.float32).repeat(eyes.shape[0], 1)
387-
else:
388-
raise ValueError(f"Invalid up axis: {up_axis_token}")
389-
390-
# get rotation matrix in opengl format (-Z forward, +Y up)
391-
z_axis = -F.normalize(targets - eyes, eps=1e-5)
392-
x_axis = F.normalize(torch.cross(up_axis, z_axis, dim=1), eps=1e-5)
393-
y_axis = F.normalize(torch.cross(z_axis, x_axis, dim=1), eps=1e-5)
394-
is_close = torch.isclose(x_axis, torch.tensor(0.0), atol=5e-3).all(dim=1, keepdim=True)
395-
if is_close.any():
396-
replacement = F.normalize(torch.cross(y_axis, z_axis, dim=1), eps=1e-5)
397-
x_axis = torch.where(is_close, replacement, x_axis)
398-
R = torch.cat((x_axis[:, None, :], y_axis[:, None, :], z_axis[:, None, :]), dim=1)
399-
return R.transpose(1, 2)
400-
401-
402260
def save_images_to_file(images: torch.Tensor, file_path: str):
403261
"""Save images to file.
404262

0 commit comments

Comments
 (0)