Last active
September 6, 2024 16:00
-
-
Save glvov-bdai/834b188ee91f4fed91049a423f540b5d to your computer and use it in GitHub Desktop.
Some Scraps for RGB to Point Cloud Registration
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# I never got to finish this fully, but it's most of the way there for full non-obstructed charuco board views | |
# Maybe one day will be useful... | |
# Although retrospectively, I kind of wish I just projected the RGB points to 3D space and just | |
# did ICP with the point cloud. Not sure if I'd need to segment out non-plane points in the cloud for that still though... | |
import open3d as o3d | |
import numpy as np | |
import matplotlib.pyplot as plt | |
import cv2 | |
from typing import List, Dict, Optional | |
from copy import deepcopy | |
from scipy.optimize import minimize | |
from scipy.spatial import ConvexHull | |
import spatialmath.base as smb | |
import spatialmath as sm | |
from spatialmath import SO3, SE3 | |
def add_random_color_to_pcd(pcd): | |
"""Add random color to a point cloud. | |
Args: | |
pcd (o3d.geometry.PointCloud): The input point cloud. | |
Returns: | |
o3d.geometry.PointCloud: The colored point cloud. | |
""" | |
color = np.random.rand(3) | |
colors = np.tile(color, (np.asarray(pcd.points).shape[0], 1)) | |
pcd.colors = o3d.utility.Vector3dVector(colors) | |
return pcd | |
def create_polyline(points, close_line=True): | |
if len(points) < 2: | |
raise ValueError("Polyline must have at least two points.") | |
# Ensure points are in numpy array format | |
points = np.array(points, dtype=np.float64) | |
if points.shape[1] != 3: | |
raise ValueError("Each point must have 3 coordinates (x, y, z).") | |
# Create the lines connecting each point to the next | |
lines = [[i, i+1] for i in range(len(points) - 1)] | |
# If close_line is True, add a line connecting the last point to the first | |
if close_line: | |
lines.append([len(points) - 1, 0]) | |
colors = [[1, 0, 0] for _ in range(len(lines))] # Red color for the lines | |
line_set = o3d.geometry.LineSet() | |
line_set.points = o3d.utility.Vector3dVector(points) | |
line_set.lines = o3d.utility.Vector2iVector(lines) | |
line_set.colors = o3d.utility.Vector3dVector(colors) | |
return line_set | |
def draw_frame(transform, size=0.1): | |
"""Create an Open3D geometry representing an SE(3) frame.""" | |
mesh_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=size) | |
mesh_frame.transform(transform) | |
return mesh_frame | |
def visualize_multiple_point_clouds(pcds, polyline_points=None, frames=None): | |
"""Visualize multiple point clouds with random colors, an optional polyline, and optional SE(3) frames. | |
Args: | |
pcds (list of o3d.geometry.PointCloud): List of point clouds to visualize. | |
polyline_points (list of list or np.array, optional): List of points [x, y, z] that make up the optional polyline. | |
frames (list of np.array, optional): List of SE(3) frames (4x4 transformation matrices) to draw. | |
""" | |
vis = o3d.visualization.Visualizer() | |
vis.create_window() | |
for pcd in pcds: | |
colored_pcd = add_random_color_to_pcd(pcd) | |
vis.add_geometry(colored_pcd) | |
if polyline_points is not None: | |
polyline_shape = np.array(polyline_points).shape | |
if polyline_shape[0] == 1: | |
line_set = create_polyline(polyline_points[0]) | |
vis.add_geometry(line_set) | |
elif polyline_shape[0] > 1: | |
for line in polyline_points: | |
line_set = create_polyline(line) | |
vis.add_geometry(line_set) | |
if frames is not None: | |
for frame in frames: | |
frame_mesh = draw_frame(frame) | |
vis.add_geometry(frame_mesh) | |
vis.run() | |
vis.destroy_window() | |
def rotation_matrix_to_align_vectors(vec1, vec2): | |
v1 = np.array(vec1) / np.linalg.norm(vec1) | |
v2 = np.array(vec2) / np.linalg.norm(vec2) | |
cross_product = np.cross(v1, v2) | |
dot_product = np.dot(v1, v2) | |
cross_product_matrix = np.array([ | |
[0, -cross_product[2], cross_product[1]], | |
[cross_product[2], 0, -cross_product[0]], | |
[-cross_product[1], cross_product[0], 0] | |
]) | |
rotation_matrix = np.eye(3) + cross_product_matrix + \ | |
np.dot(cross_product_matrix, cross_product_matrix) * \ | |
(1 / (1 + dot_product)) | |
return rotation_matrix | |
def numpy_to_pcd(xyz): | |
pcd = o3d.geometry.PointCloud() | |
pcd.points = o3d.utility.Vector3dVector(xyz) | |
return pcd | |
def pcd_to_numpy(pcd): | |
return np.asarray(pcd.points) | |
def remove_noise_radius(pcd: o3d.geometry.PointCloud, | |
nb_points: int = 16, | |
radius: float = 0.05) -> o3d.geometry.PointCloud: | |
"""Remove point clouds noise using radius outlier removal method.""" | |
cl, ind = pcd.remove_radius_outlier(nb_points=nb_points, radius=radius) | |
return cl | |
def cautiously_clean_pointcloud(pcd: o3d.geometry.PointCloud, | |
number_neighbors_for_radius_removal: int = 16, | |
radius: float = .05): | |
def remove_nan(pcd: o3d.geometry.PointCloud) -> o3d.geometry.PointCloud: | |
"""Remove nan values from point clouds.""" | |
points = np.asarray(pcd.points) | |
pcd.points = o3d.utility.Vector3dVector(points[~np.isnan(points[:, 0])]) | |
return pcd | |
no_nan_pcd = remove_nan(pcd) | |
no_isolated_points_pcd = remove_noise_radius(no_nan_pcd) | |
return no_isolated_points_pcd | |
def remove_noise_statistical(pcd: o3d.geometry.PointCloud, nb_neighbors: int = 20, std_ratio: float = 2.0) -> o3d.geometry.PointCloud: | |
cl, ind = pcd.remove_statistical_outlier(nb_neighbors=nb_neighbors, std_ratio=std_ratio) | |
return cl | |
def voxel_down_sample(pcd: o3d.geometry.PointCloud, voxel_size=0.003) -> o3d.geometry.PointCloud: | |
return pcd.voxel_down_sample(voxel_size=voxel_size) | |
def random_down_sample(pcd: o3d.geometry.PointCloud, preservation_ratio=0.5) -> o3d.geometry.PointCloud: | |
return pcd.random_down_sample(sampling_ratio=preservation_ratio) | |
def compute_plane_equation_from_obox(obox): | |
# Get the normal vector (z-axis of the bounding box rotation matrix) | |
normal = obox.R[:, 2] | |
# Get a point on the plane (center of the bounding box) | |
point_on_plane = obox.center | |
# Plane equation: ax + by + cz + d = 0 | |
d = -np.dot(normal, point_on_plane) | |
plane_equation = np.append(normal, d) | |
return plane_equation | |
def get_bbox_vertices_above_plane(bbx, plane_eq=[0, 0, 1, 0]): | |
vertices = bbx.get_box_points() | |
def check_if_point_above_plane(point, plane=plane_eq): | |
A, B, C, D = plane | |
x, y, z = point | |
result = A * x + B * y + C * z + D | |
return result >= 0 | |
above_plane_vertices = list(filter(lambda x: check_if_point_above_plane(x), vertices)) | |
above_plane_vertices = sorted(above_plane_vertices, | |
key=lambda x: np.linalg.norm(x - above_plane_vertices[0])) | |
bbx = numpy_to_pcd(above_plane_vertices) | |
return pcd_to_numpy(bbx) | |
def detect_multi_planes(pcd: o3d.geometry.PointCloud, min_ratio: float = 0.05, | |
normal_variance_threshold_deg: float = 30, | |
coplanarity_deg: float = 60, | |
outlier_ratio: float = 0.75, | |
min_plane_edge_length: float = 0,#min(rectangle_height, rectangle_width), | |
min_num_points: int = 0,#MIN_PLANE_PTS, | |
knn: int = 30): | |
""" | |
Detect multiple planes from given point clouds using Open3D's detect_planar_patches. | |
""" | |
# Ensure normals are estimated | |
if not pcd.has_normals(): | |
pcd.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamKNN(knn=knn)) | |
# Detect planar patches | |
oboxes = pcd.detect_planar_patches( | |
normal_variance_threshold_deg=normal_variance_threshold_deg, | |
coplanarity_deg=coplanarity_deg, | |
outlier_ratio=outlier_ratio, | |
min_plane_edge_length=min_plane_edge_length, | |
min_num_points=min_num_points, | |
search_param=o3d.geometry.KDTreeSearchParamKNN(knn=knn) | |
) | |
plane_list = [] | |
for obox in oboxes: | |
inlier_cloud = pcd.crop(obox) | |
plane_equation = compute_plane_equation_from_obox(obox) | |
plane_list.append((plane_equation, inlier_cloud, obox)) | |
return plane_list | |
def project_point_to_plane(point, plane): | |
A, B, C, D = plane | |
point = np.array(point) | |
normal = np.array([A, B, C]) | |
distance = (np.dot(normal, point) + D) / np.linalg.norm(normal) | |
projected_point = point - distance * (normal / np.linalg.norm(normal)) | |
return projected_point | |
def select_checkerboard_from_candidates(plane_eqs_and_inliers_and_bboxs, | |
rectangle_height: float = rectangle_height, | |
rectangle_width: float = rectangle_width, | |
fit_tolerance: float = RECT_FIT_TOL, | |
min_num_points: int = MIN_PLANE_PTS, | |
unit_vec = [0, 0, 1], | |
dim_tol = .02): | |
results = [] | |
for (plane_eq, plane_inliers, bbox) in plane_eqs_and_inliers_and_bboxs: | |
plane_vec = plane_eq[:3] | |
align_plane_normal_to_z_axis_rot_mat = rotation_matrix_to_align_vectors(plane_vec, unit_vec) | |
bbox_vertices_above_plane = get_bbox_vertices_above_plane(bbox, plane_eq) | |
bbox_vertices_projected_to_plane = [project_point_to_plane(p, plane_eq) for p in bbox_vertices_above_plane] | |
projected_corners = numpy_to_pcd(bbox_vertices_above_plane) | |
projected_corners = projected_corners.rotate(align_plane_normal_to_z_axis_rot_mat) | |
projected_corners = pcd_to_numpy(projected_corners)[:, :2] | |
projected_corners_without_opp = projected_corners[:-1] | |
dim = sorted([np.linalg.norm(projected_corners_without_opp[-1] - \ | |
projected_corners_without_opp[0]), | |
np.linalg.norm(projected_corners_without_opp[-2] - \ | |
projected_corners_without_opp[0])]) | |
results.append({"corners_3d": bbox_vertices_projected_to_plane, | |
"center_3d": np.asarray(bbox.center), | |
"dim": dim, | |
"align_plane_normal_to_z_axis_rot_mat": align_plane_normal_to_z_axis_rot_mat, | |
"plane_eq": plane_eq, | |
"inliers": plane_inliers, | |
"corners_2d": projected_corners, | |
"center_2d": np.mean(projected_corners, axis=0)}) | |
ref_dim = sorted([rectangle_height, rectangle_width]) | |
compare_dim = lambda x: ((ref_dim[0] - x[0])**2 + (ref_dim[1] - x[1])**2) ** .5 | |
dim_tol = (dim_tol ** 2 + dim_tol ** 2) ** .5 | |
results = sorted(results, key = lambda x: compare_dim(x['dim'])) | |
if len(results) == 0 or compare_dim(results[0]['dim']) > dim_tol: | |
return None | |
else: | |
return results[0] | |
# cloud_calib = {} | |
# for idx, image_data in enumerate(image_data_list[play_slice]): | |
# tof_pcd = o3d.geometry.PointCloud.create_from_depth_image(depth=image_data.depth, | |
# depth_scale=1000, | |
# intrinsic=image_data.unregistered_depth_intrinsic.to_o3d()) | |
# cleaned_tof_pcd = cautiously_clean_pointcloud(tof_pcd, | |
# number_neighbors_for_radius_removal=4, | |
# radius=.03) | |
# plane_eqs_and_inliers_and_bboxs = detect_multi_planes(cleaned_tof_pcd) | |
# result = select_checkerboard_from_candidates(plane_eqs_and_inliers_and_bboxs) | |
# if result is not None: | |
# result["cloud"] = cleaned_tof_pcd | |
# cloud_calib[idx] = result | |
def assign_pose_to_cloud(cloud_calib, | |
rgb_pose_est, | |
alignment_heuristic = RPY_HEURISTIC, | |
t_guess = T_HEURISTIC): | |
rgb_t_depth = SE3.Rt(R=SO3.RPY(*alignment_heuristic), | |
t=t_guess) | |
rgb_t_depth_rot = SO3.RPY(*alignment_heuristic) | |
rgb_t_board = SE3(rgb_pose_est['pose']) | |
rgb_t_board_rot = SO3(rgb_pose_est['pose'][:3,:3]) | |
depth_t_board_est_from_rgb = rgb_t_depth.inv() * rgb_t_board | |
cloud_plane_normal = cloud_calib['plane_eq'][:-1] | |
cloud_plane_normal /= np.linalg.norm(cloud_plane_normal) # normalize jic | |
rgb_plane_normal = rgb_pose_est['pose'][:3,2] | |
if np.dot(cloud_plane_normal, rgb_plane_normal) < 0: | |
cloud_plane_normal *= -1 # reverse the direction to line things up | |
random_corner = cloud_calib['corners_3d'][0] | |
distances = [np.linalg.norm(random_corner - c) for c in cloud_calib['corners_3d']] | |
hypot_idx = np.argmax(np.array(distances)) | |
bordering_corners = [] | |
for idx in range(1, 4): | |
if idx != hypot_idx: | |
bordering_corners.append(cloud_calib['corners_3d'][idx]) | |
base_vectors = [c - random_corner for c in bordering_corners] | |
base_vectors = [np.array(vec) / np.linalg.norm(vec) for vec in base_vectors] # normalize jic | |
possible_basis_vectors = [base_vectors, # all possible basis to combine with normal for SO3 | |
[base_vectors[0] * -1, base_vectors[1]], | |
[base_vectors[0], base_vectors[1] * -1], | |
[base_vectors[0] * -1, base_vectors[1] * -1]] | |
rotations = [] | |
for vector_set in possible_basis_vectors: | |
for order in [vector_set, vector_set[::-1]]: | |
possible_rot_mat = np.zeros((3, 3)) | |
possible_rot_mat[:, 0] = order[0] | |
possible_rot_mat[:, 1] = order[1] | |
possible_rot_mat[:, 2] = cloud_plane_normal | |
orthagonalized, upper_triangular = np.linalg.qr(possible_rot_mat) | |
for idx in range(3): | |
if np.dot(orthagonalized[:, idx], possible_rot_mat[:, idx]) < 0: | |
orthagonalized[:, idx] *= -1.0 | |
if np.linalg.det(orthagonalized) > 0: | |
try: | |
rotations.append(SO3(orthagonalized)) | |
except ValueError as e: | |
print(f"Rotation matrix is not properly formed: {e} ") | |
depth_t_board_est_from_rgb_rot = SO3(depth_t_board_est_from_rgb.A[:3,:3]) # operator overload goes brr | |
relative_rots = [(depth_t_board_est_from_rgb_rot.inv() * depth_t_board_rot).angvec()[0] for \ | |
depth_t_board_rot in rotations] | |
most_likely_rot = rotations[np.argmin(relative_rots)] # relative rotation here | |
depth_camera_origin_t_charuco_center = SE3.Rt(R=most_likely_rot, t=cloud_calib["center_3d"].reshape((3,))) | |
return { | |
"pose": depth_camera_origin_t_charuco_center.A, | |
"rgb_t_board_in_depth_frame_est": depth_t_board_est_from_rgb, | |
"visu_rotation_matrix": cv2.Rodrigues(depth_camera_origin_t_charuco_center.A[:-1, :-1])[0], # visu | |
"visu_translation_vector": depth_camera_origin_t_charuco_center.A[:-1, -1].reshape((3, 1)) | |
} | |
# poses = {} | |
# for idx in calib_ids: | |
# cloud_data = cloud_calib[idx] | |
# rgb_pose = rgb_calib['poses'][idx] | |
# cloud_pose = assign_pose_to_cloud(cloud_data, rgb_pose, alignment_heuristic = RPY_HEURISTIC, | |
# t_guess = T_HEURISTIC) | |
# poses[idx] = [rgb_pose['pose'], cloud_pose['pose'], cloud_pose["rgb_t_board_in_depth_frame_est"]] | |
# for jdx in calib_ids: | |
# visualize_multiple_point_clouds([cloud_calib[jdx]['cloud']], | |
# polyline_points=[[*cloud_calib[jdx]['corners_3d'], | |
# cloud_calib[jdx]['center_3d']]], | |
# frames=[poses[jdx][1], poses[jdx][2]]) | |
# x = input("press c to continue") | |
# if x != "c": | |
# break | |
def remove_outliers_based_on_transform(poses, threshold=3): | |
""" | |
Remove outliers from a list of 4x4 transformation matrices based on the last column values ([:-1, -1]). | |
Parameters: | |
poses (list of np.ndarray): List of 4x4 transformation matrices. | |
threshold (float): Z-score threshold to determine outliers. | |
Returns: | |
np.ndarray: Cleaned list of transformation matrices with outliers removed. | |
""" | |
# Extract the relevant values to determine outliers ([:-1, -1]) | |
extracted_values = np.array([pose[:-1, -1] for pose in poses], dtype=np.float32) | |
# Calculate the mean and standard deviation for each column | |
mean = np.mean(extracted_values, axis=0) | |
std = np.std(extracted_values, axis=0) | |
# Identify outliers | |
z_scores = (extracted_values - mean) / std | |
outliers = np.any(np.abs(z_scores) > threshold, axis=1) | |
# Remove outliers | |
cleaned_poses = np.array(poses)[~outliers] | |
cleaned_values = extracted_values[~outliers] | |
# Calculate mean and standard deviation of the cleaned data | |
cleaned_mean = np.mean(cleaned_values, axis=0) | |
cleaned_std = np.std(cleaned_values, axis=0) | |
# Print results | |
print("Mean of Extracted Values:\n", mean) | |
print("Standard Deviation of Extracted Values:\n", std) | |
print("Mean of Cleaned Extracted Values:\n", cleaned_mean) | |
print("Standard Deviation of Cleaned Extracted Values:\n", cleaned_std) | |
return cleaned_poses |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment