Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ src/vtr3_pyposegraph.egg-info
src/vtr_gps_plotting
*.db3
build
point_clouds/
55 changes: 55 additions & 0 deletions helpers/pix4d2radar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import open3d as o3d
import numpy as np

# Input and output file paths
input_ply = "point_clouds/pin_4_crop_sub.ply"
output_ply = "point_clouds/pin_4_filtered.ply"
Comment on lines +3 to +6

Copilot AI Jul 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The script uses hardcoded file paths and parameters (e.g., input_ply, output_ply, base heights). Consider adding argparse support to make these values configurable and remove magic numbers for better flexibility.

Suggested change
# Input and output file paths
input_ply = "point_clouds/pin_4_crop_sub.ply"
output_ply = "point_clouds/pin_4_filtered.ply"
import argparse
# Parse command-line arguments
parser = argparse.ArgumentParser(description="Filter point cloud based on radar parameters.")
parser.add_argument("--input_ply", type=str, default="point_clouds/pin_4_crop_sub.ply", help="Path to input PLY file.")
parser.add_argument("--output_ply", type=str, default="point_clouds/pin_4_filtered.ply", help="Path to output PLY file.")
parser.add_argument("--pix4d_pc_base_height", type=float, default=-12, help="Base height of the Pix4D point cloud.")
parser.add_argument("--radar_height", type=float, default=1.5, help="Height of the radar above the base.")
parser.add_argument("--radar_angle", type=float, default=2, help="Radar angle in degrees.")
parser.add_argument("--radar_range", type=float, default=20, help="Radar range in meters.")
args = parser.parse_args()
# Input and output file paths
input_ply = args.input_ply
output_ply = args.output_ply

Copilot uses AI. Check for mistakes.

# Read the point cloud
pcd = o3d.io.read_point_cloud(input_ply)

# Convert to numpy array
points = np.asarray(pcd.points)

pix4d_pc_base_height = -12
radar_height = 1.5
radar_angle = 2 # degrees
radar_range = 20 # meters

# Calculate centroid of x and y coordinates
centroid_x = np.mean(points[:, 0])
centroid_y = np.mean(points[:, 1])

# Radar position in the point cloud coordinate system
# Using centroid of points for x and y coordinates
radar_position = np.array([centroid_x, centroid_y, pix4d_pc_base_height + radar_height])

# Calculate horizontal distance of each point from radar in x-y plane
distances = np.sqrt((points[:, 0] - radar_position[0])**2 +
(points[:, 1] - radar_position[1])**2)

# Calculate allowed z-range at each distance based on radar_angle
allowed_deviation = distances * np.tan(np.deg2rad(radar_angle))

# Calculate upper and lower bounds for each point
lower_bounds = radar_position[2] - allowed_deviation
upper_bounds = radar_position[2] + allowed_deviation

# Filter points that fall within the conic field of view
mask = (points[:, 2] >= lower_bounds) & (points[:, 2] <= upper_bounds) & (distances <= radar_range)
filtered_points = points[mask]

# Set z coordinates to 0
filtered_points[:, 2] = 0

# Create new point cloud
filtered_pcd = o3d.geometry.PointCloud()
filtered_pcd.points = o3d.utility.Vector3dVector(filtered_points)

# Optionally, copy colors if present
if pcd.has_colors():
colors = np.asarray(pcd.colors)
filtered_pcd.colors = o3d.utility.Vector3dVector(colors[mask])

# Save the filtered point cloud
o3d.io.write_point_cloud(output_ply, filtered_pcd)
41 changes: 38 additions & 3 deletions samples/plot_teach_submaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,17 @@

parser = argparse.ArgumentParser(prog = 'Plot Point Clouds Path',
description = 'Plots point clouds')
parser.add_argument('-g', '--graph', default=os.getenv("VTRDATA"), help="The filepath to the pose graph folder. (Usually /a/path/graph)") # option that takes a value
parser.add_argument('-g', '--graph', default=os.getenv("VTRDATA"), help="The filepath to the pose graph folder. (Usually /a/path/graph)")
parser.add_argument('--save_pc', type=lambda x: (str(x).lower() == 'true'), default=False, help="Whether to save point clouds (True/False).")
parser.add_argument('--save_dir', type=str, default=None, help="Directory to save point clouds (required if --save_pc is True).")
args = parser.parse_args()

if args.save_pc and not args.save_dir:
parser.error("--save_dir must be specified if --save_pc is True.")

if args.save_pc and not os.path.exists(args.save_dir):
os.makedirs(args.save_dir)

factory = Rosbag2GraphFactory(args.graph)

test_graph = factory.buildGraph()
Expand All @@ -33,7 +41,6 @@
live_2_map = []
map_2_live = []


first = True
paused = False
def toggle(vis):
Expand All @@ -49,13 +56,18 @@ def toggle(vis):
vis.poll_events()
vis.update_renderer()

cloud_count = 0

for i in range(test_graph.major_id + 1):
v_start = test_graph.get_vertex((i, 0))
paused = True
vertices = list(TemporalIterator(v_start))
vertices_to_plot = vertices[:-10] if len(vertices) > 10 else vertices

for vertex, e in vertices_to_plot:
# Accumulate all points for this major_id
accumulated_points = []

for idx, (vertex, e) in enumerate(vertices_to_plot):

new_points, map_ptr = extract_map_from_vertex(test_graph, vertex)

Expand All @@ -76,6 +88,18 @@ def toggle(vis):
else:
pcd.paint_uniform_color((0.1*vertex.run, 0.25*vertex.run, 0.45))

# Save every 20th point cloud if requested
if args.save_pc and (cloud_count % 20 == 0):
filename = os.path.join(args.save_dir, f"cloud_{cloud_count:05d}.ply")
o3d.io.write_point_cloud(filename, pcd)
print(f"Saved point cloud to {filename}")

# Accumulate all points for this major_id
if args.save_pc:
accumulated_points.append(new_points.T)

Copilot AI Jul 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appending 'new_points.T' transposes each point set to shape (3, N), which will misalign when vstacking later. Use 'new_points' (shape N×3) instead of its transpose.

Suggested change
accumulated_points.append(new_points.T)
accumulated_points.append(new_points)

Copilot uses AI. Check for mistakes.

cloud_count += 1

# Create coordinate frame for the vertex
frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=1.0, origin=robot_position)

Expand All @@ -92,5 +116,16 @@ def toggle(vis):
vis.poll_events()
vis.update_renderer()

# Save accumulated point cloud for this major_id
if args.save_pc and accumulated_points:
all_points = np.vstack(accumulated_points)
acc_pcd = o3d.geometry.PointCloud()
acc_pcd.points = o3d.utility.Vector3dVector(all_points)
acc_filename = os.path.join(args.save_dir, f"accumulated_major_{i:03d}.ply")
o3d.io.write_point_cloud(acc_filename, acc_pcd)
print(f"Saved accumulated point cloud for major_id {i} to {acc_filename}")

print("Finished processing all point clouds.")

vis.run()
vis.destroy_window()
32 changes: 23 additions & 9 deletions src/vtr_odom_extractor/relative_export_odometry_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@
from scipy.spatial.transform import Rotation as R
import matplotlib.pyplot as plt

def plot_odometry_path(positions):
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot(positions[:, 0], positions[:, 1], positions[:, 2], label='Odometry Path')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')

max_range = np.array([positions[:,0].max()-positions[:,0].min(),
positions[:,1].max()-positions[:,1].min(),
positions[:,2].max()-positions[:,2].min()]).max()
mid_x = (positions[:,0].max()+positions[:,0].min()) * 0.5
mid_y = (positions[:,1].max()+positions[:,1].min()) * 0.5
mid_z = (positions[:,2].max()+positions[:,2].min()) * 0.5
ax.set_xlim(mid_x - max_range/2, mid_x + max_range/2)
ax.set_ylim(mid_y - max_range/2, mid_y + max_range/2)
ax.set_zlim(mid_z - max_range/2, mid_z + max_range/2)

ax.legend()
plt.title('Odometry Path')
plt.show()

def export_relative_transforms(graph_path, output_path):
if os.path.isdir(output_path):
output_path_mat = os.path.join(output_path, "relative_transforms_mat_test.csv")
Expand Down Expand Up @@ -122,15 +144,7 @@ def export_relative_transforms(graph_path, output_path):
actual_positions = np.array(actual_positions)

# Plot the path
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot(actual_positions[:, 0], actual_positions[:, 1], actual_positions[:, 2], label='Odometry Path')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.legend()
plt.title('Odometry Path')
plt.show()
plot_odometry_path(actual_positions)

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Export odometry path from pose graph.")
Expand Down