|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from datasets import load_dataset |
|
|
from hoho2025.vis import plot_all_modalities |
|
|
from hoho2025.viz3d import * |
|
|
import pycolmap |
|
|
import tempfile,zipfile |
|
|
import io |
|
|
import open3d as o3d |
|
|
from PIL import Image, ImageDraw |
|
|
|
|
|
def _plotly_rgb_to_normalized_o3d_color(color_val) -> list[float]: |
|
|
""" |
|
|
Converts Plotly-style color (str 'rgb(r,g,b)' or tuple (r,g,b)) |
|
|
to normalized [r/255, g/255, b/255] for Open3D. |
|
|
""" |
|
|
if isinstance(color_val, str): |
|
|
if color_val.startswith('rgba'): |
|
|
parts = color_val[5:-1].split(',') |
|
|
return [int(p.strip())/255.0 for p in parts[:3]] |
|
|
elif color_val.startswith('rgb'): |
|
|
parts = color_val[4:-1].split(',') |
|
|
return [int(p.strip())/255.0 for p in parts] |
|
|
else: |
|
|
|
|
|
|
|
|
|
|
|
raise ValueError(f"Unsupported color string format for Open3D conversion: {color_val}. Expected 'rgb(...)' or 'rgba(...)'.") |
|
|
elif isinstance(color_val, (list, tuple)) and len(color_val) == 3: |
|
|
|
|
|
return [c/255.0 for c in color_val] |
|
|
raise ValueError(f"Unsupported color type for Open3D conversion: {type(color_val)}. Expected string or 3-element tuple/list.") |
|
|
|
|
|
def draw_crosses_on_image(image_pil, vertices_data, output_file_path, size=5, color=(0, 0, 0)): |
|
|
""" |
|
|
Draws crosses on a PIL Image at specified vertex locations and saves it. |
|
|
Args: |
|
|
image_pil (PIL.Image.Image): The image to draw on. |
|
|
vertices_data (list): List of dictionaries, each with an 'xy' key |
|
|
holding [x, y] coordinates. |
|
|
output_file_path (str): Path to save the modified image. |
|
|
size (int): Size of the cross arms. |
|
|
color (tuple): RGB color for the cross. |
|
|
""" |
|
|
|
|
|
img_to_draw_on = image_pil.copy() |
|
|
drawer = ImageDraw.Draw(img_to_draw_on) |
|
|
|
|
|
for vert_info in vertices_data: |
|
|
if 'xy' in vert_info: |
|
|
x, y = vert_info['xy'] |
|
|
|
|
|
x_int, y_int = int(round(x)), int(round(y)) |
|
|
|
|
|
|
|
|
drawer.line([(x_int - size, y_int), (x_int + size, y_int)], fill=color, width=1) |
|
|
|
|
|
drawer.line([(x_int, y_int - size), (x_int, y_int + size)], fill=color, width=1) |
|
|
|
|
|
img_to_draw_on.save(output_file_path) |
|
|
|
|
|
def save_gestalt_with_proj(gest_seg_np, gt_verts, img_id): |
|
|
|
|
|
|
|
|
if gest_seg_np.ndim == 2: |
|
|
img_to_draw_on = Image.fromarray(gest_seg_np, mode='L') |
|
|
elif gest_seg_np.ndim == 3 and gest_seg_np.shape[2] == 3: |
|
|
img_to_draw_on = Image.fromarray(gest_seg_np, mode='RGB') |
|
|
else: |
|
|
|
|
|
|
|
|
img_to_draw_on = Image.fromarray(gest_seg_np.astype(np.uint8)) |
|
|
|
|
|
|
|
|
if img_to_draw_on.mode == 'L': |
|
|
img_to_draw_on = img_to_draw_on.convert('RGB') |
|
|
|
|
|
draw = ImageDraw.Draw(img_to_draw_on) |
|
|
cross_size = 5 |
|
|
cross_color = (0, 0, 0) |
|
|
|
|
|
for vert_dict in gt_verts: |
|
|
x, y = vert_dict['xy'] |
|
|
|
|
|
draw.line([(x - cross_size, y), (x + cross_size, y)], fill=cross_color, width=1) |
|
|
|
|
|
draw.line([(x, y - cross_size), (x, y + cross_size)], fill=cross_color, width=1) |
|
|
|
|
|
|
|
|
|
|
|
img_to_draw_on.save(f'gestalt_cross/{img_id}.png') |
|
|
|
|
|
def plot_reconstruction_local( |
|
|
fig: go.Figure, |
|
|
rec: pycolmap.Reconstruction, |
|
|
color: str = 'rgb(0, 0, 255)', |
|
|
name: Optional[str] = None, |
|
|
points: bool = True, |
|
|
cameras: bool = True, |
|
|
cs: float = 1.0, |
|
|
single_color_points=False, |
|
|
camera_color='rgba(0, 255, 0, 0.5)', |
|
|
crop_outliers: bool = False): |
|
|
|
|
|
|
|
|
xyzs = [] |
|
|
rgbs = [] |
|
|
|
|
|
for k, p3D in rec.points3D.items(): |
|
|
|
|
|
xyzs.append(p3D.xyz) |
|
|
rgbs.append(p3D.color) |
|
|
|
|
|
xyzs = np.array(xyzs) |
|
|
rgbs = np.array(rgbs) |
|
|
|
|
|
|
|
|
if crop_outliers and len(xyzs) > 0: |
|
|
|
|
|
distances = np.linalg.norm(xyzs, axis=1) |
|
|
|
|
|
threshold = np.percentile(distances, 98) |
|
|
|
|
|
mask = distances <= threshold |
|
|
xyzs = xyzs[mask] |
|
|
rgbs = rgbs[mask] |
|
|
print(f"Cropped outliers: removed {np.sum(~mask)} out of {len(mask)} points ({np.sum(~mask)/len(mask)*100:.2f}%)") |
|
|
|
|
|
if points and len(xyzs) > 0: |
|
|
pcd = o3d.geometry.PointCloud() |
|
|
pcd.points = o3d.utility.Vector3dVector(xyzs) |
|
|
|
|
|
|
|
|
|
|
|
if rgbs.size > 0: |
|
|
normalized_rgbs = rgbs / 255.0 |
|
|
pcd.colors = o3d.utility.Vector3dVector(normalized_rgbs) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if cameras: |
|
|
try: |
|
|
|
|
|
cam_color_normalized = _plotly_rgb_to_normalized_o3d_color(camera_color) |
|
|
except ValueError as e: |
|
|
print(f"Warning: Invalid camera_color '{camera_color}'. Using default green. Error: {e}") |
|
|
cam_color_normalized = [0.0, 1.0, 0.0] |
|
|
|
|
|
geometries = [] |
|
|
|
|
|
for image_id, image_data in rec.images.items(): |
|
|
|
|
|
cam = rec.cameras[image_data.camera_id] |
|
|
K = cam.calibration_matrix() |
|
|
|
|
|
|
|
|
if K[0, 0] > 5000 or K[1, 1] > 5000: |
|
|
print(f"Skipping camera for image {image_id} due to large focal length (fx={K[0,0]}, fy={K[1,1]}).") |
|
|
continue |
|
|
|
|
|
|
|
|
|
|
|
T_world_cam = image_data.cam_from_world.inverse() |
|
|
R_wc = T_world_cam.rotation.matrix() |
|
|
t_wc = T_world_cam.translation |
|
|
|
|
|
W, H = float(cam.width), float(cam.height) |
|
|
|
|
|
|
|
|
if W <= 0 or H <= 0: |
|
|
print(f"Skipping camera for image {image_id} due to invalid dimensions (W={W}, H={H}).") |
|
|
continue |
|
|
|
|
|
|
|
|
img_corners_px = np.array([ |
|
|
[0, 0], [W, 0], [W, H], [0, H] |
|
|
], dtype=float) |
|
|
|
|
|
|
|
|
img_corners_h = np.hstack([img_corners_px, np.ones((4, 1))]) |
|
|
|
|
|
try: |
|
|
K_inv = np.linalg.inv(K) |
|
|
except np.linalg.LinAlgError: |
|
|
print(f"Skipping camera for image {image_id} due to non-invertible K matrix.") |
|
|
continue |
|
|
|
|
|
|
|
|
cam_coords_norm = (K_inv @ img_corners_h.T).T |
|
|
|
|
|
|
|
|
|
|
|
cam_coords_scaled = cam_coords_norm * cs |
|
|
|
|
|
|
|
|
world_coords_base = (R_wc @ cam_coords_scaled.T).T + t_wc |
|
|
|
|
|
|
|
|
|
|
|
pyramid_vertices = np.vstack((t_wc, world_coords_base)) |
|
|
if not pyramid_vertices.flags['C_CONTIGUOUS']: |
|
|
pyramid_vertices = np.ascontiguousarray(pyramid_vertices, dtype=np.float64) |
|
|
elif pyramid_vertices.dtype != np.float64: |
|
|
pyramid_vertices = np.asarray(pyramid_vertices, dtype=np.float64) |
|
|
|
|
|
|
|
|
lines = np.array([ |
|
|
[0, 1], [0, 2], [0, 3], [0, 4], |
|
|
[1, 2], [2, 3], [3, 4], [4, 1] |
|
|
]) |
|
|
|
|
|
if not lines.flags['C_CONTIGUOUS']: |
|
|
lines = np.ascontiguousarray(lines, dtype=np.int32) |
|
|
elif lines.dtype != np.int32: |
|
|
lines = np.asarray(lines, dtype=np.int32) |
|
|
|
|
|
|
|
|
camera_pyramid_lineset = o3d.geometry.LineSet() |
|
|
camera_pyramid_lineset.points = o3d.utility.Vector3dVector(pyramid_vertices) |
|
|
camera_pyramid_lineset.lines = o3d.utility.Vector2iVector(lines) |
|
|
|
|
|
|
|
|
geometries.append(camera_pyramid_lineset) |
|
|
|
|
|
else: |
|
|
geometries = [] |
|
|
|
|
|
return pcd, geometries |
|
|
|
|
|
def plot_wireframe_local( |
|
|
fig: go.Figure, |
|
|
vertices: np.ndarray, |
|
|
edges: np.ndarray, |
|
|
classifications: np.ndarray = None, |
|
|
color: str = 'rgb(0, 0, 255)', |
|
|
name: Optional[str] = None, |
|
|
**kwargs) -> list: |
|
|
""" |
|
|
Generates Open3D geometries for a wireframe: a PointCloud for vertices |
|
|
and a LineSet for edges. |
|
|
|
|
|
Args: |
|
|
fig: Plotly figure object (no longer used for plotting by this function). |
|
|
vertices: Numpy array of vertex coordinates (N, 3). |
|
|
edges: Numpy array of edge connections (M, 2), indices into vertices. |
|
|
classifications: Optional numpy array of classifications for edges. |
|
|
color: Default color string (e.g., 'rgb(0,0,255)') for vertices and |
|
|
for edges if classifications are not provided or don't match. |
|
|
name: Optional name (unused in Open3D context here). |
|
|
**kwargs: Additional keyword arguments (unused). |
|
|
|
|
|
Returns: |
|
|
A list of Open3D geometry objects. Empty if no vertices. |
|
|
Note: Plotly-specific 'ps' (point size) and line width are not |
|
|
directly translated. These are typically visualizer render options in Open3D. |
|
|
""" |
|
|
open3d_geometries = [] |
|
|
|
|
|
|
|
|
|
|
|
gt_vertices = np.asarray(vertices) |
|
|
if not gt_vertices.flags['C_CONTIGUOUS'] or gt_vertices.dtype != np.float64: |
|
|
gt_vertices = np.ascontiguousarray(gt_vertices, dtype=np.float64) |
|
|
|
|
|
|
|
|
gt_connections = np.asarray(edges) |
|
|
if not gt_connections.flags['C_CONTIGUOUS'] or gt_connections.dtype != np.int32: |
|
|
gt_connections = np.ascontiguousarray(gt_connections, dtype=np.int32) |
|
|
|
|
|
if gt_vertices.size == 0: |
|
|
return open3d_geometries |
|
|
|
|
|
|
|
|
try: |
|
|
vertex_rgb_normalized = _plotly_rgb_to_normalized_o3d_color(color) |
|
|
except ValueError as e: |
|
|
print(f"Warning: Could not parse vertex color '{color}'. Using default black. Error: {e}") |
|
|
vertex_rgb_normalized = [0.0, 0.0, 0.0] |
|
|
|
|
|
vertex_pcd = o3d.geometry.PointCloud() |
|
|
|
|
|
vertex_pcd.points = o3d.utility.Vector3dVector(gt_vertices) |
|
|
|
|
|
num_vertices = len(gt_vertices) |
|
|
if num_vertices > 0: |
|
|
|
|
|
vertex_colors_np = np.array([vertex_rgb_normalized] * num_vertices, dtype=np.float64) |
|
|
|
|
|
|
|
|
if not vertex_colors_np.flags['C_CONTIGUOUS']: |
|
|
vertex_colors_np = np.ascontiguousarray(vertex_colors_np) |
|
|
vertex_pcd.colors = o3d.utility.Vector3dVector(vertex_colors_np) |
|
|
open3d_geometries.append(vertex_pcd) |
|
|
|
|
|
|
|
|
if gt_connections.size > 0 and num_vertices > 0: |
|
|
line_set = o3d.geometry.LineSet() |
|
|
|
|
|
line_set.points = o3d.utility.Vector3dVector(gt_vertices) |
|
|
|
|
|
line_set.lines = o3d.utility.Vector2iVector(gt_connections) |
|
|
|
|
|
line_colors_list_normalized = [] |
|
|
if classifications is not None and len(classifications) == len(gt_connections): |
|
|
|
|
|
|
|
|
for c_idx in classifications: |
|
|
try: |
|
|
color_tuple_255 = edge_color_mapping[EDGE_CLASSES_BY_ID[c_idx]] |
|
|
line_colors_list_normalized.append(_plotly_rgb_to_normalized_o3d_color(color_tuple_255)) |
|
|
except KeyError: |
|
|
print(f"Warning: Classification ID {c_idx} or its mapping not found. Using default color.") |
|
|
line_colors_list_normalized.append(vertex_rgb_normalized) |
|
|
except Exception as e: |
|
|
print(f"Warning: Error processing classification color for index {c_idx}. Using default. Error: {e}") |
|
|
line_colors_list_normalized.append(vertex_rgb_normalized) |
|
|
else: |
|
|
|
|
|
default_line_rgb_normalized = vertex_rgb_normalized |
|
|
for _ in range(len(gt_connections)): |
|
|
line_colors_list_normalized.append(default_line_rgb_normalized) |
|
|
|
|
|
if line_colors_list_normalized: |
|
|
|
|
|
line_colors_np = np.array(line_colors_list_normalized, dtype=np.float64) |
|
|
|
|
|
|
|
|
if not line_colors_np.flags['C_CONTIGUOUS']: |
|
|
line_colors_np = np.ascontiguousarray(line_colors_np) |
|
|
line_set.colors = o3d.utility.Vector3dVector(line_colors_np) |
|
|
|
|
|
open3d_geometries.append(line_set) |
|
|
|
|
|
return open3d_geometries |
|
|
|
|
|
def plot_bpo_cameras_from_entry_local(fig: go.Figure, entry: dict, idx = None, camera_scale_factor: float = 1.0): |
|
|
def cam2world_to_world2cam(R, t): |
|
|
rt = np.eye(4) |
|
|
rt[:3,:3] = R |
|
|
rt[:3,3] = t.reshape(-1) |
|
|
rt = np.linalg.inv(rt) |
|
|
return rt[:3,:3], rt[:3,3] |
|
|
geometries = [] |
|
|
for i in range(len(entry['R'])): |
|
|
if idx is not None and i != idx: |
|
|
continue |
|
|
|
|
|
|
|
|
|
|
|
current_cam_color_str = 'rgb(0, 0, 255)' |
|
|
|
|
|
|
|
|
K_matrix = np.array(entry['K'][i]) |
|
|
R_orig = np.array(entry['R'][i]) |
|
|
t_orig = np.array(entry['t'][i]) |
|
|
|
|
|
|
|
|
|
|
|
R_transformed, t_transformed = cam2world_to_world2cam(R_orig, t_orig) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
W_img, H_img = K_matrix[0, 2] * 2, K_matrix[1, 2] * 2 |
|
|
if W_img <= 0 or H_img <= 0: |
|
|
|
|
|
|
|
|
print(f"Warning: Camera {i} has invalid dimensions (W={W_img}, H={H_img}) based on K. Skipping.") |
|
|
continue |
|
|
|
|
|
|
|
|
corners_px = np.array([[0, 0], [W_img, 0], [W_img, H_img], [0, H_img]], dtype=float) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
K_inv = np.linalg.inv(K_matrix) |
|
|
except np.linalg.LinAlgError: |
|
|
print(f"Warning: K matrix for camera {i} is singular. Skipping this camera.") |
|
|
continue |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
corners_cam_homog = to_homogeneous(corners_px) @ K_inv.T |
|
|
|
|
|
|
|
|
|
|
|
scaled_cam_points = corners_cam_homog * camera_scale_factor |
|
|
|
|
|
|
|
|
world_coords_base = scaled_cam_points @ R_transformed.T + t_transformed |
|
|
|
|
|
|
|
|
apex_world = t_transformed.reshape(1, 3) |
|
|
|
|
|
|
|
|
pyramid_vertices_np = np.vstack((apex_world, world_coords_base)) |
|
|
if not pyramid_vertices_np.flags['C_CONTIGUOUS'] or pyramid_vertices_np.dtype != np.float64: |
|
|
pyramid_vertices_np = np.ascontiguousarray(pyramid_vertices_np, dtype=np.float64) |
|
|
|
|
|
|
|
|
lines_np = np.array([ |
|
|
[0, 1], [0, 2], [0, 3], [0, 4], |
|
|
[1, 2], [2, 3], [3, 4], [4, 1] |
|
|
], dtype=np.int32) |
|
|
|
|
|
|
|
|
camera_lineset = o3d.geometry.LineSet() |
|
|
camera_lineset.points = o3d.utility.Vector3dVector(pyramid_vertices_np) |
|
|
lines_np = np.ascontiguousarray(lines_np, dtype=np.int32) |
|
|
camera_lineset.lines = o3d.utility.Vector2iVector(lines_np) |
|
|
|
|
|
|
|
|
try: |
|
|
o3d_color = _plotly_rgb_to_normalized_o3d_color(current_cam_color_str) |
|
|
except ValueError as e: |
|
|
print(f"Warning: Invalid camera color string '{current_cam_color_str}' for camera {i}. Using default blue. Error: {e}") |
|
|
o3d_color = [0.0, 0.0, 1.0] |
|
|
camera_lineset.colors = o3d.utility.Vector3dVector(np.array([o3d_color] * len(lines_np), dtype=np.float64)) |
|
|
|
|
|
geometries.append(camera_lineset) |
|
|
|
|
|
return geometries |