Skip to content

Commit e11e5a1

Browse files
authored
Merge pull request #574 from bitcraze/Aris/LHvisualization
Lighthouse Visualization Script
2 parents 11d9995 + ce4c1b8 commit e11e5a1

File tree

1 file changed

+155
-0
lines changed

1 file changed

+155
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# || ____ _ __
4+
# +------+ / __ )(_) /_______________ _____ ___
5+
# | 0xBC | / __ / / __/ ___/ ___/ __ `/_ / / _ \
6+
# +------+ / /_/ / / /_/ /__/ / / /_/ / / /_/ __/
7+
# || || /_____/_/\__/\___/_/ \__,_/ /___/\___/
8+
#
9+
# Copyright (C) 2025 Bitcraze AB
10+
#
11+
# This program is free software; you can redistribute it and/or
12+
# modify it under the terms of the GNU General Public License
13+
# as published by the Free Software Foundation; either version 2
14+
# of the License, or (at your option) any later version.
15+
#
16+
# This program is distributed in the hope that it will be useful,
17+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
# GNU General Public License for more details.
20+
# You should have received a copy of the GNU General Public License
21+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
"""
23+
Simple script for visualizing the Ligithouse positioning system's configuration
24+
using matplotlib. Each base station is represented by a local coordinate frame, while
25+
each one's coverage is represented by 2 circular sectors; a horizontal and a vertical one.
26+
Notice that the base station coordinate frame is defined as:
27+
- X-axis pointing forward through the glass
28+
- Y-axis pointing right, when the base station is seen from the front.
29+
- Z-axis pointing up
30+
31+
To run the script, just change the path to your .yaml file.
32+
"""
33+
import matplotlib.pyplot as plt
34+
import numpy as np
35+
import yaml
36+
37+
config_file = 'lighthouse.yaml' # Add the path to your .yaml file
38+
39+
Range = 5 # Range of each base station in meters
40+
FoV_h = 150 # Horizontal Field of View in degrees
41+
FoV_v = 110 # Vertical Field of View in degrees
42+
43+
44+
def draw_coordinate_frame(ax, P, R, label='', length=0.5, is_bs=False):
45+
"""Draw a coordinate frame at position t with orientation R."""
46+
x_axis = R @ np.array([length, 0, 0])
47+
y_axis = R @ np.array([0, length, 0])
48+
z_axis = R @ np.array([0, 0, length])
49+
50+
ax.quiver(P[0], P[1], P[2], x_axis[0], x_axis[1], x_axis[2], color='r', linewidth=2)
51+
ax.quiver(P[0], P[1], P[2], y_axis[0], y_axis[1], y_axis[2], color='g', linewidth=2)
52+
ax.quiver(P[0], P[1], P[2], z_axis[0], z_axis[1], z_axis[2], color='b', linewidth=2)
53+
if is_bs:
54+
ax.scatter(P[0], P[1], P[2], s=50, color='black')
55+
ax.text(P[0], P[1], P[2], label, fontsize=10, color='black')
56+
57+
58+
def draw_horizontal_sector(ax, P, R, radius=Range, angle_deg=FoV_h, color='r', alpha=0.3, n_points=50):
59+
"""
60+
Draw a circular sector centered at the origin of the local coordinate frame,lying in
61+
the local XY-plane, so that its central axis is aligned with the positive X-axis.
62+
"""
63+
# Angle range (centered on X-axis)
64+
half_angle = np.deg2rad(angle_deg / 2)
65+
thetas = np.linspace(-half_angle, half_angle, n_points)
66+
67+
# Circle points in local XY-plane
68+
x_local = radius * np.cos(thetas)
69+
y_local = radius * np.sin(thetas)
70+
z_local = np.zeros_like(thetas)
71+
72+
# Stack the coordinates into a 3xN matix
73+
pts_local = np.vstack([x_local, y_local, z_local])
74+
75+
# Transfer the points to the global frame, creating a 3xN matrix
76+
pts_global = R @ pts_local + P.reshape(3, 1)
77+
78+
# Close the sector by adding the center point at the start and end
79+
X = np.concatenate(([P[0]], pts_global[0, :], [P[0]]))
80+
Y = np.concatenate(([P[1]], pts_global[1, :], [P[1]]))
81+
Z = np.concatenate(([P[2]], pts_global[2, :], [P[2]]))
82+
83+
# Plot filled sector
84+
ax.plot_trisurf(X, Y, Z, color=color, alpha=alpha, linewidth=0)
85+
86+
87+
def draw_vertical_sector(ax, P, R, radius=Range, angle_deg=FoV_v, color='r', alpha=0.3, n_points=50):
88+
"""
89+
Draw a circular sector centered at the origin of the local coordinate frame,lying in
90+
the local XZ-plane, so that its central axis is aligned with the positive X-axis.
91+
"""
92+
# Angle range (centered on X-axis)
93+
half_angle = np.deg2rad(angle_deg / 2)
94+
thetas = np.linspace(-half_angle, half_angle, n_points)
95+
96+
# Circle points in local XZ-plane
97+
x_local = radius * np.cos(thetas)
98+
y_local = np.zeros_like(thetas)
99+
z_local = radius * np.sin(thetas)
100+
101+
# Stack the coordinates into a 3xN matix
102+
pts_local = np.vstack([x_local, y_local, z_local])
103+
104+
# Transfer the points to the global frame, creating a 3xN matrix
105+
pts_global = R @ pts_local + P.reshape(3, 1)
106+
107+
# Close the sector by adding the center point at the start and end
108+
X = np.concatenate(([P[0]], pts_global[0, :], [P[0]]))
109+
Y = np.concatenate(([P[1]], pts_global[1, :], [P[1]]))
110+
Z = np.concatenate(([P[2]], pts_global[2, :], [P[2]]))
111+
112+
# Plot filled sector
113+
ax.plot_trisurf(X, Y, Z, color=color, alpha=alpha, linewidth=0)
114+
115+
116+
if __name__ == '__main__':
117+
# Load the .yamnl file
118+
with open(config_file, 'r') as f:
119+
data = yaml.safe_load(f)
120+
geos = data['geos']
121+
122+
fig = plt.figure()
123+
ax = fig.add_subplot(111, projection='3d')
124+
125+
# Draw global frame
126+
draw_coordinate_frame(ax, np.zeros(3), np.eye(3), label='Global', length=1.0)
127+
128+
# Draw local frames + sectors
129+
for key, geo in geos.items():
130+
origin = np.array(geo['origin'])
131+
rotation = np.array(geo['rotation'])
132+
draw_coordinate_frame(ax, origin, rotation, label=f'BS {key+1}', length=0.5, is_bs=True)
133+
134+
# Local XY-plane sector
135+
draw_horizontal_sector(ax, origin, rotation, radius=Range, angle_deg=FoV_h, color='red', alpha=0.15)
136+
137+
# Local YZ-plane sector
138+
draw_vertical_sector(ax, origin, rotation, radius=Range, angle_deg=FoV_v, color='red', alpha=0.15)
139+
140+
ax.set_xlabel('X')
141+
ax.set_ylabel('Y')
142+
ax.set_zlabel('Z')
143+
ax.set_title('Lighthouse Visualization')
144+
145+
# Set equal aspect ratio
146+
all_points = [np.array(geo['origin']) for geo in geos.values()]
147+
all_points.append(np.zeros(3))
148+
all_points = np.array(all_points)
149+
max_range = np.ptp(all_points, axis=0).max()
150+
mid = all_points.mean(axis=0)
151+
ax.set_xlim(mid[0] - max_range/2, mid[0] + max_range/2)
152+
ax.set_ylim(mid[1] - max_range/2, mid[1] + max_range/2)
153+
ax.set_zlim(mid[2] - max_range/2, mid[2] + max_range/2)
154+
155+
plt.show()

0 commit comments

Comments
 (0)