Skip to content

Commit fa82cd7

Browse files
Gpu lidar fix (#165)
* Added lidardisplay for ubuntu * organize test files * use cylindrical gpu for test * Match GPU lidar conventions to CPU lidar conventions * GPU shader no longer uses the cam-1 inverse deprojection path for points * GPU lidar readback metadata alignment. This avoids publishing stale point data with the current tick’s pose. * gpu_cylindrical now uses the configured vertical FOV lower/upper bounds instead of assuming symmetry around zero. * GPU lidar: replace the projection-to-pixel step with explicit perspective math. * Fix partial FOV normalization issue in GPU lidar * GPU lidar guard threads beyond the actual point count * GPU lidar order ok, fixed from renderer not from lidar code * Gpu lidar working 100% Co-authored-by: Lucas Scheinkerman <37854249+LucasJSch@users.noreply.github.com>
1 parent f777efe commit fa82cd7

13 files changed

Lines changed: 538 additions & 289 deletions

File tree

client/python/example_user_scripts/depth_lidar.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
import time
55
from pathlib import Path
66

7-
import cv2
87
import numpy as np
98

109
from projectairsim import Drone, ProjectAirSimClient, World
11-
from projectairsim.lidar_utils import LidarDisplay
10+
from projectairsim.image_utils import ImageDisplay
11+
from projectairsim.lidar_utils import LidarTopDownRenderer
1212
from projectairsim.utils import projectairsim_log
1313

1414

@@ -72,13 +72,12 @@ def receive(self, cloud_msg):
7272
async def main():
7373
client = ProjectAirSimClient()
7474
lidar_cloud_stats = PointCloudStats("lidar1.lidar")
75-
lidar_cloud_display = LidarDisplay(
76-
win_name="Lidar PointCloud",
77-
color_mode=LidarDisplay.COLOR_INTENSITY,
75+
image_display = ImageDisplay(num_subwin=1, subwin_width=640, subwin_height=640)
76+
lidar_cloud_window = "Lidar PointCloud"
77+
lidar_cloud_renderer = LidarTopDownRenderer(
7878
width=640,
79-
height=360,
80-
x=40,
81-
y=420,
79+
height=640,
80+
range_m=60.0,
8281
)
8382
lidar_cloud_topic = None
8483
drone = None
@@ -100,14 +99,19 @@ async def main():
10099
f"Subscribing lidar pointcloud stream: {lidar_cloud_topic}"
101100
)
102101

103-
lidar_cloud_display.start()
102+
image_display.add_image(lidar_cloud_window, subwin_idx=0)
103+
image_display.start()
104104

105105
client.subscribe(
106106
lidar_cloud_topic,
107107
safe_topic_callback(
108108
"lidar_cloud",
109109
lambda _, lidar_msg: handle_lidar_cloud(
110-
lidar_msg, lidar_cloud_stats, lidar_cloud_display
110+
lidar_msg,
111+
lidar_cloud_stats,
112+
lidar_cloud_renderer,
113+
image_display,
114+
lidar_cloud_window,
111115
),
112116
),
113117
)
@@ -156,15 +160,23 @@ async def main():
156160
drone.disarm()
157161
drone.disable_api_control()
158162

159-
lidar_cloud_display.stop()
160-
cv2.destroyAllWindows()
163+
image_display.stop()
161164
client.disconnect()
162165

163166

164-
def handle_lidar_cloud(lidar_msg, lidar_cloud_stats, lidar_cloud_display):
167+
def handle_lidar_cloud(
168+
lidar_msg,
169+
lidar_cloud_stats,
170+
lidar_cloud_renderer,
171+
image_display,
172+
lidar_cloud_window,
173+
):
165174
lidar_cloud_stats.receive(lidar_msg)
166-
lidar_cloud_display.receive(lidar_msg)
175+
image_display.receive(
176+
lidar_cloud_renderer.render_image_msg(lidar_msg),
177+
lidar_cloud_window,
178+
)
167179

168180

169181
if __name__ == "__main__":
170-
asyncio.run(main())
182+
asyncio.run(main())

client/python/example_user_scripts/lidar_basic.py

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,60 @@
77
"""
88

99
import asyncio
10+
import math
11+
import time
12+
from pathlib import Path
1013

1114
from projectairsim import ProjectAirSimClient, Drone, World
15+
from projectairsim.lidar_utils import LidarTopDownRenderer
1216
from projectairsim.utils import projectairsim_log
1317
from projectairsim.image_utils import ImageDisplay
14-
from projectairsim.lidar_utils import LidarDisplay
18+
19+
20+
class LidarStats:
21+
def __init__(self):
22+
self.message_count = 0
23+
self.last_report_wall = 0.0
24+
25+
def receive(self, lidar_data):
26+
if lidar_data is None:
27+
return
28+
29+
point_count = int(len(lidar_data.get("point_cloud", [])) / 3)
30+
if point_count == 0:
31+
return
32+
33+
self.message_count += 1
34+
now = time.time()
35+
if self.message_count == 1 or now - self.last_report_wall >= 1.0:
36+
projectairsim_log().info(
37+
f"lidar1: msg={self.message_count} points={point_count}"
38+
)
39+
self.last_report_wall = now
40+
41+
42+
def receive_lidar(lidar_data, lidar_stats, lidar_renderer, image_display, image_name):
43+
lidar_stats.receive(lidar_data)
44+
image_display.receive(lidar_renderer.render_image_msg(lidar_data), image_name)
1545

1646

1747
# Async main function to wrap async drone commands
1848
async def main():
1949
# Create a Project AirSim client
2050
client = ProjectAirSimClient()
51+
lidar_stats = LidarStats()
2152

2253
# Initialize an ImageDisplay object to display camera sub-windows
2354
image_display = ImageDisplay()
2455

2556
# ----------------------------------------------------------------------------------
2657

27-
# Initialize a LidarDisplay object for a point cloud visualization sub-window
28-
lidar_subwin = image_display.get_subwin_info(2)
29-
lidar_display = LidarDisplay(
30-
x=lidar_subwin["x"], y=lidar_subwin["y"] + 30
31-
) # add 30 y-pix for window title bar
58+
# Initialize a top-down LIDAR image renderer.
59+
lidar_renderer = LidarTopDownRenderer(
60+
width=640,
61+
height=640,
62+
range_m=60.0,
63+
)
3264

3365
# ----------------------------------------------------------------------------------
3466

@@ -37,7 +69,13 @@ async def main():
3769
client.connect()
3870

3971
# Create a World object to interact with the sim world and load a scene
40-
world = World(client, "scene_lidar_drone.jsonc", delay_after_load_sec=2)
72+
sim_config_path = str(Path(__file__).resolve().parent / "sim_config")
73+
world = World(
74+
client,
75+
"scene_lidar_drone.jsonc",
76+
delay_after_load_sec=2,
77+
sim_config_path=sim_config_path,
78+
)
4179

4280
# Create a Drone object to interact with a drone in the loaded sim world
4381
drone = Drone(client, world, "Drone1")
@@ -65,22 +103,24 @@ async def main():
65103
lambda _, depth: image_display.receive(depth, depth_name),
66104
)
67105

106+
lidar_name = "LIDAR"
107+
image_display.add_image(lidar_name, resize_x=640, resize_y=640)
108+
68109
image_display.start()
69110

70111
# ------------------------------------------------------------------------------
71112

72113
client.subscribe(
73114
drone.sensors["lidar1"]["lidar"],
74-
lambda _, lidar: lidar_display.receive(lidar),
115+
lambda _, lidar: receive_lidar(
116+
lidar, lidar_stats, lidar_renderer, image_display, lidar_name
117+
),
75118
)
76119

77120
# client.subscribe(
78121
# drone.sensors["lidar1"]["lidar"],
79122
# lambda _, lidar: print(lidar),
80123
# )
81-
82-
lidar_display.start()
83-
84124
# ------------------------------------------------------------------------------
85125

86126
# Set the drone to be ready to fly
@@ -100,6 +140,13 @@ async def main():
100140
)
101141
await move_task
102142

143+
projectairsim_log().info("Yaw 720 degrees")
144+
yaw_task = await drone.rotate_by_yaw_rate_async(
145+
yaw_rate=math.radians(12.0),
146+
duration=20.0,
147+
)
148+
await yaw_task
149+
103150
projectairsim_log().info("Move north-east")
104151
move_task = await drone.move_by_velocity_async(
105152
v_north=4.0, v_east=4.0, v_down=0.0, duration=8.0
@@ -133,10 +180,6 @@ async def main():
133180

134181
# ------------------------------------------------------------------------------
135182

136-
lidar_display.stop()
137-
138-
# ------------------------------------------------------------------------------
139-
140183

141184
if __name__ == "__main__":
142185
asyncio.run(main()) # Runner for async main function

client/python/example_user_scripts/sim_config/robot_quadrotor_fastphysics_lidar.jsonc

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -424,25 +424,27 @@
424424
{
425425
"id": "lidar1",
426426
"type": "lidar",
427+
"lidar-type": "gpu_cylindrical",
427428
"enabled": true,
428429
"parent-link": "Frame",
429-
"number-of-channels": 16,
430+
"number-of-channels": 32,
430431
"range": 100,
431-
"points-per-second": 100000,
432+
"points-per-second": 180000,
432433
"horizontal-rotation-frequency": 10,
433-
"horizontal-fov-start-deg": 0.0,
434+
"horizontal-fov-start-deg": 0,
434435
"horizontal-fov-end-deg": 360.0,
435-
"vertical-fov-upper-deg": 0.0,
436-
"vertical-fov-lower-deg": -90.0,
436+
"vertical-fov-upper-deg": 15.0,
437+
"vertical-fov-lower-deg": -15.0,
437438
"disable-self-hits": true,
438439
"draw-debug-points": false,
439-
"origin": {
440-
"xyz": "0 0 0.2",
441-
"rpy-deg": "0 0 0"
442-
},
440+
"report-frequency": 0,
443441
"report-point-cloud": true,
444442
"report-azimuth-elevation-range": false,
445-
"report-no-return-points": false
443+
"report-no-return-points": false,
444+
"origin": {
445+
"xyz": "0 0 1.0",
446+
"rpy-deg": "0 0 0"
447+
}
446448
},
447449
{
448450
"id": "GPS",
@@ -451,4 +453,4 @@
451453
"parent-link": "Frame"
452454
}
453455
]
454-
}
456+
}

client/python/projectairsim/src/projectairsim/lidar_utils.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import multiprocessing as mp
33
import time
44

5+
import cv2
56
import numpy as np
67
import open3d as o3d
78
from matplotlib import cm
@@ -10,6 +11,74 @@
1011
# from projectairsim.utils import projectairsim_log
1112

1213

14+
class LidarTopDownRenderer:
15+
"""Render lidar point clouds as top-down OpenCV-compatible image messages.
16+
17+
The output uses the sensor frame with forward plotted upward and right plotted
18+
to the right. Point color is based on the down-axis value.
19+
"""
20+
21+
def __init__(self, width=640, height=640, range_m=60.0):
22+
self.width = width
23+
self.height = height
24+
self.range_m = range_m
25+
26+
def make_blank_image(self):
27+
image = np.zeros((self.height, self.width, 3), dtype=np.uint8)
28+
center = (self.width // 2, self.height // 2)
29+
cv2.circle(image, center, 3, (255, 255, 255), -1)
30+
return image
31+
32+
def render_lidar(self, lidar_data):
33+
point_cloud = lidar_data.get("point_cloud")
34+
if point_cloud is None or len(point_cloud) == 0:
35+
return self.make_blank_image()
36+
37+
points = np.asarray(point_cloud, dtype=np.float32).reshape((-1, 3))
38+
image = self.make_blank_image()
39+
40+
scale = min(self.width, self.height) / (2.0 * self.range_m)
41+
px = np.round(self.width / 2.0 + points[:, 1] * scale).astype(np.int32)
42+
py = np.round(self.height / 2.0 - points[:, 0] * scale).astype(np.int32)
43+
44+
in_bounds = (px >= 0) & (px < self.width) & (py >= 0) & (py < self.height)
45+
if not np.any(in_bounds):
46+
return image
47+
48+
z_down = points[in_bounds, 2]
49+
z_norm = np.clip((z_down - z_down.min()) / (np.ptp(z_down) + 1e-6), 0.0, 1.0)
50+
colors = cv2.applyColorMap(
51+
(255.0 * z_norm).astype(np.uint8), cv2.COLORMAP_TURBO
52+
)
53+
image[py[in_bounds], px[in_bounds]] = colors[:, 0, :]
54+
55+
cv2.line(
56+
image,
57+
(self.width // 2, 0),
58+
(self.width // 2, self.height - 1),
59+
(60, 60, 60),
60+
1,
61+
)
62+
cv2.line(
63+
image,
64+
(0, self.height // 2),
65+
(self.width - 1, self.height // 2),
66+
(60, 60, 60),
67+
1,
68+
)
69+
cv2.circle(image, (self.width // 2, self.height // 2), 4, (255, 255, 255), -1)
70+
return image
71+
72+
def render_image_msg(self, lidar_data):
73+
image = self.render_lidar(lidar_data)
74+
return {
75+
"encoding": "BGR",
76+
"height": self.height,
77+
"width": self.width,
78+
"data": image.tobytes(),
79+
}
80+
81+
1382
class LidarDisplay:
1483
"""Class to display LIDAR point cloud data in a window. This runs its visualization
1584
display update loop in a separate process independent of the client's async.io main

0 commit comments

Comments
 (0)