Skip to content

Commit a26cb25

Browse files
krrishj18CopilotJohnYanxinLiu
authored
Scene prep bug fix (#354)
* fixes to scene_prep_utils.py * edited docs * clean launch script * updated version * fixed comments inconsistency and typos * formatting fix Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * bug in gossip if payload is empty * fixed omni_pass.env file creation bug from CICD guest default profile * fixed depth topic naming in foxglove gcs * changed gps topic * removed redundant exntentions --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: airlab <johnliuchs2022@gmail.com>
1 parent ae64f97 commit a26cb25

9 files changed

Lines changed: 290 additions & 76 deletions

File tree

‎.airstack/modules/config.sh‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ function cmd_config_nucleus {
6363
local escaped_api_token="${API_TOKEN//\\/\\\\}"
6464
escaped_api_token="${escaped_api_token//&/\\&}"
6565
escaped_api_token="${escaped_api_token//\//\\/}"
66-
sed "s/PASTE-YOUR-API-TOKEN/$escaped_api_token/g" "$OMNI_PASS_SOURCE" > "$OMNI_PASS_DESTINATION"
66+
sed -e "s/^OMNI_USER=.*$/OMNI_USER='\$omni-api-token'/" \
67+
-e "s/^OMNI_PASS=.*$/OMNI_PASS=$escaped_api_token/" \
68+
"$OMNI_PASS_SOURCE" > "$OMNI_PASS_DESTINATION"
6769
log_info "Nucleus login configuration complete"
6870
else
6971
cp "$OMNI_PASS_SOURCE" "$OMNI_PASS_DESTINATION"

‎.env‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ PROJECT_NAME="airstack"
1212
# If you've run ./airstack.sh setup, then this will auto-generate from the git commit hash every time a change is made
1313
# to a Dockerfile or docker-compose.yaml file. Otherwise this can also be set explicitly to make a release version.
1414
# auto-generated from git commit hash
15-
VERSION="0.18.0-alpha.10"
15+
VERSION="0.18.0-alpha.11"
1616
# Choose "dev" or "prebuilt". "dev" is for mounted code that must be built live. "prebuilt" is for built ros_ws baked into the image
1717
DOCKER_IMAGE_BUILD_MODE="dev"
1818
# Where to push and pull images from. Can replace with your docker hub username if using docker hub.

‎common/ros_packages/coordination/coordination_bringup/coordination_bringup/gossip_node.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def __init__(self):
9494

9595
self._navsat_sub = self.create_subscription(
9696
NavSatFix,
97-
f"/{self._robot_name}/interface/mavros/global_position/global",
97+
f"/{self._robot_name}/interface/mavros/global_position/raw/fix",
9898
self._on_navsat,
9999
SENSOR_QOS,
100100
)
@@ -148,7 +148,7 @@ def _setup_payload_subscriptions(self, config_path: str) -> None:
148148
self.get_logger().warn(f"Could not load payload config '{config_path}': {e}")
149149
return
150150

151-
for entry in cfg.get("payload_topics", []):
151+
for entry in cfg.get("payload_topics") or []:
152152
topic_template = entry.get("topic", "")
153153
type_str = entry.get("type", "")
154154
if not topic_template or not type_str:

‎docs/simulation/isaac_sim/overhead_camera.md‎

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,25 +70,38 @@ The three constants (`OVERHEAD_ALTITUDE_M`, `OVERHEAD_COVERAGE_M`, `OVERHEAD_PX_
7070
| `OVERHEAD_COVERAGE_M` | `200.0` | Side length of the captured square (m). |
7171
| `OVERHEAD_PX_PER_METER` | `4.0` | Texture density. Increase for sharper text/markings; capped at `max_resolution=2048`. |
7272

73-
The camera is positioned at world origin `(0, 0)`. If your scene's points of interest are off-origin, shift the camera's `prim_path` xform after `add_orthographic_camera` returns:
73+
### Re-centering or transforming the camera
74+
75+
By default the camera sits over world origin `(0, 0)`. For an off-origin area of interest, pass `center_x_m` / `center_y_m` to both helpers — they take care of the camera xform and the spec topics the GCS reads:
7476

7577
```python
76-
from pxr import Gf, UsdGeom
78+
CENTER_X_M, CENTER_Y_M = 50.0, -25.0
7779

78-
cam_path = add_orthographic_camera(stage, prim_path="/World/MapCamera", ...)
80+
cam_path = add_orthographic_camera(
81+
stage, prim_path="/World/MapCamera",
82+
altitude_m=OVERHEAD_ALTITUDE_M,
83+
coverage_m=OVERHEAD_COVERAGE_M,
84+
scene_scale_factor=scene_scale_factor,
85+
center_x_m=CENTER_X_M,
86+
center_y_m=CENTER_Y_M,
87+
)
7988

80-
# Re-center the camera over (CENTER_X_M, CENTER_Y_M) instead of world origin.
81-
CENTER_X_M, CENTER_Y_M = 50.0, -25.0
82-
xform = UsdGeom.Xformable(stage.GetPrimAtPath(cam_path))
83-
xform.ClearXformOpOrder()
84-
xform.AddTranslateOp().Set(Gf.Vec3d(
85-
CENTER_X_M * scene_scale_factor,
86-
CENTER_Y_M * scene_scale_factor,
87-
OVERHEAD_ALTITUDE_M * scene_scale_factor,
88-
))
89+
add_overhead_camera_publisher(
90+
parent_graph_path="/World/MapCameraGraph",
91+
camera_prim_path=cam_path,
92+
topic="/sim/overhead/image",
93+
spec_topic="/sim/overhead/spec",
94+
center_x_topic="/sim/overhead/center_x",
95+
center_y_topic="/sim/overhead/center_y",
96+
frame_id="map",
97+
coverage_m=OVERHEAD_COVERAGE_M,
98+
center_x_m=CENTER_X_M,
99+
center_y_m=CENTER_Y_M,
100+
pixels_per_meter=OVERHEAD_PX_PER_METER,
101+
domain_id=0,
102+
)
89103
```
90104

91-
92105
## GCS side
93106

94107
The GCS rendering is handled by `_build_sim_ground_marker` in `gcs/ros_ws/src/gcs_visualizer/gcs_visualizer/foxglove_visualizer_node.py`. It:
@@ -113,8 +126,6 @@ The default downsample (0.8 cells/m, cap 384) is conservative. To raise the rend
113126
<param name="overhead_max_grid_resolution" value="1024" />
114127
```
115128

116-
`1024` over a 200 m scene gives ~5 cells/m. If the 3D panel slows down, drop back toward `768`. Any change here also requires bumping `OVERHEAD_PX_PER_METER` on the sim side (otherwise you're sampling a low-resolution source more densely).
117-
118129
To change other rendering behavior (alpha, lighting), edit `_build_sim_ground_marker` directly. To force a re-render, restart the GCS visualizer.
119130

120131
## See also

‎docs/simulation/isaac_sim/spawning_drones.md‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,98 @@ set_gps_origins(DRONE_CONFIGS, world_origin=(40.4433, -79.9436, 280.0)) # Pitts
5555

5656
The anchor only affects the geographic location reported via GPS; nothing in the scene moves. Pick something close to where you want the drones to "be" — Foxglove's Map panel will center on it, and any GPS-referenced inputs to your stack will be relative to it.
5757

58+
## Scene prep helpers — `scene_prep.py`
59+
60+
`simulation/isaac-sim/utils/scene_prep.py` is the small toolbox of stage preparation helpers `example_multi_drone_scene_import.py` uses inside its post-load callback (after the stage is loaded, before drones spawn). The full file has more — what's documented here is what you'll reach for in 95% of scenes.
61+
62+
```python
63+
from utils.scene_prep import (
64+
get_stage_meters_per_unit, scale_stage_prim, add_colliders,
65+
add_dome_light, save_scene_as_contained_usd,
66+
add_orthographic_camera, add_overhead_camera_publisher,
67+
)
68+
69+
mpu, scene_scale_factor = get_stage_meters_per_unit(stage)
70+
```
71+
72+
### Scaling — `scale_stage_prim`
73+
74+
USD scenes are authored at all sorts of stage units. To apply a uniform scale to the imported stage root once, before drones spawn:
75+
76+
```python
77+
STAGE_SCALE = 0.01 # cm → m
78+
scale_stage_prim(stage, "/World/stage", STAGE_SCALE)
79+
```
80+
81+
### Colliders — `add_colliders`
82+
83+
Recursively applies `UsdPhysics.CollisionAPI` to every `UsdGeom.Mesh` under the given prim. Imported environment USDs are usually visual-only; without this, drones fall through buildings.
84+
85+
```python
86+
stage_prim = stage.GetPrimAtPath("/World/stage")
87+
add_colliders(stage_prim)
88+
```
89+
90+
Skips prims that already have the API applied. Run it on the stage root after `scale_stage_prim` returns.
91+
92+
### Lighting — `add_dome_light`
93+
94+
In case the scene is missing any lights, this adds a dome light that can act like an overhead 'sun'.
95+
96+
```python
97+
add_dome_light(
98+
stage,
99+
prim_path="/World/DomeLight",
100+
intensity=3500.0,
101+
exposure=-5.0, # negative = darker; tune per scene
102+
)
103+
```
104+
105+
### Overhead camera — `add_orthographic_camera` + `add_overhead_camera_publisher`
106+
107+
Used as a pair: one drops an orthographic camera over the scene, the other wires an OmniGraph to publish its frame plus three Float32 spec topics (`coverage_m`, `center_x_m`, `center_y_m`) the GCS uses to texture a ground plane in Foxglove's 3D panel.
108+
109+
```python
110+
cam_path = add_orthographic_camera(
111+
stage,
112+
prim_path="/World/MapCamera",
113+
altitude_m=165.0,
114+
coverage_m=225.0,
115+
scene_scale_factor=scene_scale_factor,
116+
center_x_m=0.0, # set if your area of interest isn't at world origin
117+
center_y_m=0.0,
118+
)
119+
add_overhead_camera_publisher(
120+
parent_graph_path="/World/MapCameraGraph",
121+
camera_prim_path=cam_path,
122+
topic="/sim/overhead/image",
123+
spec_topic="/sim/overhead/spec",
124+
center_x_topic="/sim/overhead/center_x",
125+
center_y_topic="/sim/overhead/center_y",
126+
frame_id="map",
127+
coverage_m=225.0,
128+
center_x_m=0.0,
129+
center_y_m=0.0,
130+
pixels_per_meter=10.0,
131+
domain_id=0,
132+
)
133+
```
134+
135+
Full setup, GCS-side rendering, and tuning knobs are on the **[Overhead Camera](overhead_camera.md)** page.
136+
137+
### Saving a self-contained copy — `save_scene_as_contained_usd`
138+
139+
For scenes you'd like to keep working with offline (no Nucleus connection), or for sharing a scene with collaborators, collect the root USD plus every referenced asset (textures, MDLs, sublayers) into a local directory:
140+
141+
```python
142+
save_scene_as_contained_usd(
143+
source_usd_url=ENV_URL,
144+
output_dir="/tmp/collected_scene",
145+
)
146+
```
147+
148+
The collected folder contains a standalone root USD with relative references — load it directly via `omniverse://localhost/...` or a local file path. Note that this collects the **source USD as-is**: scale, colliders, dome light, and any other stage edits applied in this post-load callback are *not* baked into the saved copy. To capture the live stage with your modifications, first export the in-memory stage to a USD on disk (e.g. via `stage.GetRootLayer().Export(...)`) and pass that exported path as `source_usd_url`.
149+
58150
## Common issues
59151

60152
| Symptom | Likely cause | Fix |

‎gcs/foxglove_extensions/airstack_default.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@
539539
},
540540
"synchronize": false,
541541
"imageMode": {
542-
"imageTopic": "/robot_1/sensors/front_stereo/right/depth_ground_",
542+
"imageTopic": "/robot_1/sensors/front_stereo/right/depth_ground_truth",
543543
"calibrationTopic": "/robot_1/sensors/front_stereo/right/camera_info",
544544
"colorMode": "gradient",
545545
"minValue": 0,

‎simulation/isaac-sim/launch_scripts/barebones_pegasus_launch.py‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
ext_manager = omni.kit.app.get_app().get_extension_manager()
2929
for ext in [
3030
# "airlab.airstack",
31-
"omni.physx.forcefields",
3231
"omni.graph.core", # Core runtime for OmniGraph engine
3332
"omni.graph.action", # Action Graph framework
3433
"omni.graph.action_nodes", # Built-in Action Graph node library
@@ -38,7 +37,6 @@
3837
"omni.graph.window.action", # Action Graph editor window
3938
"omni.graph.window.generic", # Generic graph UI tools
4039
"omni.graph.ui_nodes", # UI node building helpers
41-
"airlab.pegasus", # Airlab extension Pegasus core extension
4240
"pegasus.simulator",
4341
]:
4442
if not ext_manager.is_extension_enabled(ext):

‎simulation/isaac-sim/launch_scripts/example_multi_drone_scene_import.py‎

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from pegasus.simulator.logic.interface.pegasus_interface import PegasusInterface
2828
from pegasus.simulator.ogn.api.spawn_multirotor import spawn_px4_multirotor_node
2929
from pegasus.simulator.ogn.api.spawn_zed_camera import add_zed_stereo_camera_subgraph
30-
from pegasus.simulator.ogn.api.spawn_ouster_lidar import add_ouster_lidar_subgraph
30+
from pegasus.simulator.ogn.api.spawn_rtx_lidar import add_rtx_lidar_subgraph
3131

3232
# gps_utils lives in the same directory as this script
3333
_LAUNCH_SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -39,7 +39,7 @@
3939
import scene_prep
4040
from scene_prep import (
4141
scale_stage_prim, add_colliders, add_dome_light, get_stage_meters_per_unit,
42-
reference_root_prims_under_world,
42+
reference_root_prims_under_world, dedupe_physics_scenes,
4343
add_orthographic_camera, add_overhead_camera_publisher,
4444
)
4545

@@ -49,9 +49,7 @@
4949

5050
#env/stage path and scale
5151
ENV_URL = f"omniverse://{NUCLEUS_SERVER}/Projects/AirStack/scenes/urban/allegheny_county_fire_academy/fire_academy.scene.usd"
52-
#f"omniverse://{NUCLEUS_SERVER}/Library/Assets/FireAcademyFaro/fire_academy_faro.usd"
53-
#f"omniverse://{NUCLEUS_SERVER}/Projects/AirStack/RayFronts-Planner/FireAcademy.scene.usd"
54-
#f"omniverse://{NUCLEUS_SERVER}/Library/Assets/Fire_Academy_Digital_Twin/fire_academy.usd"
52+
5553
STAGE_SCALE = 0.01
5654

5755
DRONE_USD = "~/.local/share/ov/data/documents/Kit/shared/exts/pegasus.simulator/pegasus/simulator/assets/Robots/Iris/iris.usd"
@@ -74,29 +72,34 @@
7472
# spawn location for /Assets/Fire_Academy_Digital_Twin/fire_academy.usd:
7573
# {"domain_id": 1, "x_m": 20.0, "y_m": -7.0, ...}
7674
# {"domain_id": 2, "x_m": 17.0, "y_m": 1.5, ...}
75+
7776
SPAWN_HEIGHT_ABOVE_FLOOR_M = 0.03
7877
DRONE_CONFIGS = [
79-
{"domain_id": 1, "x_m": 27.0, "y_m": 7.6, "z_m": SPAWN_HEIGHT_ABOVE_FLOOR_M, "orient": [0.0, 0.0, -0.937, 0.35], "lidar_min_range": 4.0},
80-
{"domain_id": 2, "x_m": 23.0, "y_m": 9.8, "z_m": SPAWN_HEIGHT_ABOVE_FLOOR_M, "orient": [0.0, 0.0, -0.937, 0.35], "lidar_min_range": 4.0},
81-
{"domain_id": 3, "x_m": 27.0, "y_m": 12.0, "z_m": SPAWN_HEIGHT_ABOVE_FLOOR_M, "orient": [0.0, 0.0, -0.937, 0.35], "lidar_min_range": 4.0},
82-
{"domain_id": 4, "x_m": 23.0, "y_m": 14.0, "z_m": SPAWN_HEIGHT_ABOVE_FLOOR_M, "orient": [0.0, 0.0, -0.937, 0.35], "lidar_min_range": 4.0}
78+
{"domain_id": 1, "x_m": 32.0, "y_m": 12.6, "z_m": SPAWN_HEIGHT_ABOVE_FLOOR_M, "orient": [0.0, 0.0, -0.937, 0.35], "lidar_min_range": 0.75},
79+
{"domain_id": 2, "x_m": 28.0, "y_m": 14.8, "z_m": SPAWN_HEIGHT_ABOVE_FLOOR_M, "orient": [0.0, 0.0, -0.937, 0.35], "lidar_min_range": 0.75},
80+
{"domain_id": 3, "x_m": 32.0, "y_m": 19.8, "z_m": SPAWN_HEIGHT_ABOVE_FLOOR_M, "orient": [0.0, 0.0, -0.937, 0.35], "lidar_min_range": 0.75}
8381
]
8482

85-
# Top-down "map" camera over (0, 0). Captures one aerial of the static scene
86-
# that the GCS visualizer turns into a textured ground in Foxglove's 3D panel.
87-
OVERHEAD_ALTITUDE_M = 150.0
88-
OVERHEAD_COVERAGE_M = 200.0 # per-map knob: world meters per side.
89-
OVERHEAD_PX_PER_METER = 12.0 # Source-image density. Bump for sharper texture.
83+
# Top-down "map" camera. Captures one aerial of the static scene that the
84+
# GCS visualizer turns into a textured ground in Foxglove's 3D panel. The
85+
# camera centers on (OVERHEAD_CENTER_X_M, OVERHEAD_CENTER_Y_M) in world
86+
# meters — leave both 0.0 for the legacy origin-centered behavior.
87+
OVERHEAD_ALTITUDE_M = 165.0
88+
OVERHEAD_COVERAGE_M = 225 # per-map knob: world meters per side.
89+
OVERHEAD_CENTER_X_M = 0.0 #-152 # world-X of camera center / texture center.
90+
OVERHEAD_CENTER_Y_M = 0.0 #-80 # world-Y of camera center / texture center.
91+
OVERHEAD_PX_PER_METER = 10.0 # Source-image density. Bump for sharper texture.
9092
OVERHEAD_TOPIC = "/sim/overhead/image"
9193
OVERHEAD_SPEC_TOPIC = "/sim/overhead/spec"
94+
OVERHEAD_CENTER_X_TOPIC = "/sim/overhead/center_x"
95+
OVERHEAD_CENTER_Y_TOPIC = "/sim/overhead/center_y"
9296
OVERHEAD_FRAME_ID = "map"
9397
OVERHEAD_DOMAIN_ID = 0
9498
# ---------------------------------------------------------
9599

96100

97101
ext_manager = omni.kit.app.get_app().get_extension_manager()
98102
for ext in [
99-
"omni.physx.forcefields",
100103
"omni.graph.core", # Core runtime for OmniGraph engine
101104
"omni.graph.action", # Action Graph framework
102105
"omni.graph.action_nodes", # Built-in Action Graph node library
@@ -106,7 +109,6 @@
106109
"omni.graph.window.action", # Action Graph editor window
107110
"omni.graph.window.generic", # Generic graph UI tools
108111
"omni.graph.ui_nodes", # UI node building helpers
109-
"airlab.pegasus", # Airlab extension Pegasus core extension
110112
"pegasus.simulator",
111113
]:
112114
if not ext_manager.is_extension_enabled(ext):
@@ -165,8 +167,13 @@ def __init__(self):
165167
if not wait_for_stage(stage):
166168
carb.log_warn("Stage load timed out — continuing anyway.")
167169

170+
dedupe_physics_scenes(stage)
171+
168172
# ----- Scene preparation -----
169-
# Bring in sky/sun/environment prims that sit outside /World in the source USD
173+
# Bring in sky/sun/environment prims that sit at root level in the
174+
# source USD next to the defaultPrim that pg.load_environment already
175+
# loaded into /World/stage. reference_root_prims_under_world skips
176+
# the defaultPrim, so this can't duplicate geometry.
170177
reference_root_prims_under_world(stage, ENV_URL)
171178

172179
stage_prim = stage.GetPrimAtPath("/World/stage")
@@ -193,14 +200,20 @@ def __init__(self):
193200
altitude_m=OVERHEAD_ALTITUDE_M,
194201
coverage_m=OVERHEAD_COVERAGE_M,
195202
scene_scale_factor=s,
203+
center_x_m=OVERHEAD_CENTER_X_M,
204+
center_y_m=OVERHEAD_CENTER_Y_M,
196205
)
197206
add_overhead_camera_publisher(
198207
parent_graph_path="/World/MapCameraGraph",
199208
camera_prim_path=cam_path,
200209
topic=OVERHEAD_TOPIC,
201210
spec_topic=OVERHEAD_SPEC_TOPIC,
211+
center_x_topic=OVERHEAD_CENTER_X_TOPIC,
212+
center_y_topic=OVERHEAD_CENTER_Y_TOPIC,
202213
frame_id=OVERHEAD_FRAME_ID,
203214
coverage_m=OVERHEAD_COVERAGE_M,
215+
center_x_m=OVERHEAD_CENTER_X_M,
216+
center_y_m=OVERHEAD_CENTER_Y_M,
204217
pixels_per_meter=OVERHEAD_PX_PER_METER,
205218
domain_id=OVERHEAD_DOMAIN_ID,
206219
)
@@ -230,14 +243,15 @@ def __init__(self):
230243
camera_rotation_offset=[0.0, 0.0, 0.0],
231244
)
232245

233-
add_ouster_lidar_subgraph(
246+
add_rtx_lidar_subgraph(
234247
parent_graph_handle=graph_handle,
235248
drone_prim=f"/World/drone{i}/base_link",
236249
robot_name=f"robot_{i}",
237-
lidar_name="OS1_REV6_128_10hz___512_resolution",
250+
lidar_config="ouster_os1",
251+
lidar_topic_name="point_cloud_raw",
238252
lidar_offset=[0.0, 0.0, 0.025],
239253
lidar_rotation_offset=[0.0, 0.0, 0.0],
240-
lidar_min_range=cfg["lidar_min_range"],
254+
min_range=cfg["lidar_min_range"],
241255
)
242256

243257
self.play_on_start = os.environ.get("PLAY_SIM_ON_START", "true").lower() == "true"

0 commit comments

Comments
 (0)