Spaces:
Running
Running
File size: 15,015 Bytes
6d737eb |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 |
import os
from copy import deepcopy
import imageio
import open3d as o3d
import numpy as np
from PIL import Image, ImageChops
POINT_COLOR = [1, 0, 0] # red for demonstration
ARROW_COLOR = [0, 1, 0] # green
IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg")
def generate_rotation_visualization(
pcd: o3d.geometry.PointCloud,
axis_arrow: o3d.geometry.TriangleMesh,
mask: np.ndarray,
axis_vector: np.ndarray,
origin: np.ndarray,
range_min: float,
range_max: float,
num_samples: int,
output_dir: str,
) -> None:
"""
Generate visualization files for a rotation motion of a part.
:param pcd: point cloud object representing 2D image input (RGBD) as a point cloud
:param axis_arrow: mesh object representing axis arrow of rotation to be rendered in visualization
:param mask: mask np.array of dimensions (height, width) representing the part to be rotated in the image
:param axis_vector: np.array of dimensions (3, ) representing the vector of the axis of rotation
:param origin: np.array of dimensions (3, ) representing the origin point of the axis of rotation
:param range_min: float representing the minimum range of motion in radians
:param range_max: float representing the maximum range of motion in radians
:param num_samples: number of sample states to visualize in between range_min and range_max of motion
:param output_dir: string path to directory in which to save visualization output
"""
angle_in_radians = np.linspace(range_min, range_max, num_samples)
angles_in_degrees = angle_in_radians * 180 / np.pi
for idx, angle_in_degrees in enumerate(angles_in_degrees):
# Make a copy of your original point cloud and arrow for each rotation
rotated_pcd = deepcopy(pcd)
rotated_arrow = deepcopy(axis_arrow)
angle_rad = np.radians(angle_in_degrees)
rotated_pcd = rotate_part(rotated_pcd, mask, axis_vector, origin, angle_rad)
# Create a Visualizer object for each rotation
vis = o3d.visualization.Visualizer()
vis.create_window(visible=False)
# Add the rotated geometries
vis.add_geometry(rotated_pcd)
vis.add_geometry(rotated_arrow)
# Apply the additional rotation around x-axis if desired
angle_x = np.pi * 5.5 / 5 # 198 degrees
rotation_matrix = o3d.geometry.get_rotation_matrix_from_axis_angle(np.asarray([1, 0, 0]) * angle_x)
rotated_pcd.rotate(rotation_matrix, center=rotated_pcd.get_center())
rotated_arrow.rotate(rotation_matrix, center=rotated_pcd.get_center())
# Capture and save the image
output_filename = f"{output_dir}/{idx}.png"
vis.capture_screen_image(output_filename, do_render=True)
vis.destroy_window()
def generate_translation_visualization(
pcd: o3d.geometry.PointCloud,
axis_arrow: o3d.geometry.TriangleMesh,
mask: np.ndarray,
end: np.ndarray,
range_min: float,
range_max: float,
num_samples: int,
output_dir: str,
) -> None:
"""
Generate visualization files for a translation motion of a part.
:param pcd: point cloud object representing 2D image input (RGBD) as a point cloud
:param axis_arrow: mesh object representing axis arrow of translation to be rendered in visualization
:param mask: mask np.array of dimensions (height, width) representing the part to be translated in the image
:param axis_vector: np.array of dimensions (3, ) representing the vector of the axis of translation
:param origin: np.array of dimensions (3, ) representing the origin point of the axis of translation
:param range_min: float representing the minimum range of motion
:param range_max: float representing the maximum range of motion
:param num_samples: number of sample states to visualize in between range_min and range_max of motion
:param output_dir: string path to directory in which to save visualization output
"""
translate_distances = np.linspace(range_min, range_max, num_samples)
for idx, translate_distance in enumerate(translate_distances):
translated_pcd = deepcopy(pcd)
translated_arrow = deepcopy(axis_arrow)
translated_pcd = translate_part(translated_pcd, mask, end, translate_distance.item())
# Create a Visualizer object for each rotation
vis = o3d.visualization.Visualizer()
vis.create_window(visible=False)
# Add the translated geometries
vis.add_geometry(translated_pcd)
vis.add_geometry(translated_arrow)
# Apply the additional rotation around x-axis if desired
# TODO: not sure why we need this rotation for the translation, and when it would be desired
angle_x = np.pi * 5.5 / 5 # 198 degrees
R = o3d.geometry.get_rotation_matrix_from_axis_angle(np.asarray([1, 0, 0]) * angle_x)
translated_pcd.rotate(R, center=translated_pcd.get_center())
translated_arrow.rotate(R, center=translated_pcd.get_center())
# Capture and save the image
output_filename = f"{output_dir}/{idx}.png"
vis.capture_screen_image(output_filename, do_render=True)
vis.destroy_window()
def get_rotation_matrix_from_vectors(vec1: np.ndarray, vec2: np.ndarray) -> np.ndarray:
"""
Find the rotation matrix that aligns vec1 to vec2
:param vec1: A 3d "source" vector
:param vec2: A 3d "destination" vector
:return: A transform matrix (3x3) which when applied to vec1, aligns it with vec2.
"""
a, b = (vec1 / np.linalg.norm(vec1)).reshape(3), (vec2 / np.linalg.norm(vec2)).reshape(3)
v = np.cross(a, b)
c = np.dot(a, b)
s = np.linalg.norm(v)
kmat = np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]])
rotation_matrix = np.eye(3) + kmat + kmat.dot(kmat) * ((1 - c) / (s**2))
return rotation_matrix
def draw_line(start_point: np.ndarray, end_point: np.ndarray) -> o3d.geometry.TriangleMesh:
"""
Generate 3D mesh representing axis from start_point to end_point.
:param start_point: np.ndarray of dimensions (3, ) representing the start point of the axis
:param end_point: np.ndarray of dimensions (3, ) representing the end point of the axis
:return: mesh object representing axis from start to end
"""
# Compute direction vector and normalize it
direction_vector = end_point - start_point
normalized_vector = direction_vector / np.linalg.norm(direction_vector)
# Compute the rotation matrix to align the Z-axis with the desired direction
target_vector = np.array([0, 0, 1])
rot_mat = get_rotation_matrix_from_vectors(target_vector, normalized_vector)
# Create the cylinder (shaft of the arrow)
cylinder_length = 0.9 # 90% of the total arrow length, you can adjust as needed
cylinder_radius = 0.01 # Adjust the thickness of the arrow shaft
cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=cylinder_radius, height=cylinder_length)
# Move base of cylinder to origin, rotate, then translate to start_point
cylinder.translate([0, 0, 0])
cylinder.rotate(rot_mat, center=[0, 0, 0])
cylinder.translate(start_point)
# Create the cone (head of the arrow)
cone_height = 0.1 # 10% of the total arrow length, adjust as needed
cone_radius = 0.03 # Adjust the size of the arrowhead
cone = o3d.geometry.TriangleMesh.create_cone(radius=cone_radius, height=cone_height)
# Move base of cone to origin, rotate, then translate to end of cylinder
cone.translate([-0, 0, 0])
cone.rotate(rot_mat, center=[0, 0, 0])
cone.translate(start_point + normalized_vector * 0.4)
arrow = cylinder + cone
return arrow
def rotate_part(
pcd: o3d.geometry.PointCloud, mask: np.ndarray, axis_vector: np.ndarray, origin: np.ndarray, angle_rad: float
) -> o3d.geometry.PointCloud:
"""
Generate rotated point cloud of mask based on provided angle around axis.
:param pcd: point cloud object representing points of image
:param mask: mask np.array of dimensions (height, width) representing the part to be rotated in the image
:param axis_vector: np.array of dimensions (3, ) representing the vector of the axis of rotation
:param origin: np.array of dimensions (3, ) representing the origin point of the axis of rotation
:param angle_rad: angle in radians to rotate mask part
:return: point cloud object after rotation of masked part
"""
# Get the coordinates of the point cloud as a numpy array
points_np = np.asarray(pcd.points)
# Convert point cloud colors to numpy array for easier manipulation
colors_np = np.asarray(pcd.colors)
# Create skew-symmetric matrix from end
K = np.array(
[
[0, -axis_vector[2], axis_vector[1]],
[axis_vector[2], 0, -axis_vector[0]],
[-axis_vector[1], axis_vector[0], 0],
]
)
# Compute rotation matrix using Rodrigues' formula
R = np.eye(3) + np.sin(angle_rad) * K + (1 - np.cos(angle_rad)) * np.dot(K, K)
# Iterate over the mask and rotate the points corresponding to the object pixels
for i in range(mask.shape[0]):
for j in range(mask.shape[1]):
if mask[i, j] > 0: # This condition checks if the pixel belongs to the object
point_index = i * mask.shape[1] + j
# Translate the point such that the rotation origin is at the world origin
translated_point = points_np[point_index] - origin
# Rotate the translated point
rotated_point = np.dot(R, translated_point)
# Translate the point back
points_np[point_index] = rotated_point + origin
colors_np[point_index] = POINT_COLOR
# Update the point cloud's coordinates
pcd.points = o3d.utility.Vector3dVector(points_np)
# Update point cloud colors
pcd.colors = o3d.utility.Vector3dVector(colors_np)
return pcd
def translate_part(pcd, mask, axis_vector, distance):
"""
Generate translated point cloud of mask based on provided angle around axis.
:param pcd: point cloud object representing points of image
:param mask: mask np.array of dimensions (height, width) representing the part to be translated in the image
:param axis_vector: np.array of dimensions (3, ) representing the vector of the axis of translation
:param distance: distance within coordinate system to translate mask part
:return: point cloud object after translation of masked part
"""
normalized_vector = axis_vector / np.linalg.norm(axis_vector)
translation_vector = normalized_vector * distance
# Convert point cloud colors to numpy array for easier manipulation
colors_np = np.asarray(pcd.colors)
# Get the coordinates of the point cloud as a numpy array
points_np = np.asarray(pcd.points)
# Iterate over the mask and assign the color to the points corresponding to the object pixels
for i in range(mask.shape[0]):
for j in range(mask.shape[1]):
if mask[i, j] > 0: # This condition checks if the pixel belongs to the object
point_index = i * mask.shape[1] + j
colors_np[point_index] = POINT_COLOR
points_np[point_index] += translation_vector
# Update point cloud colors
pcd.colors = o3d.utility.Vector3dVector(colors_np)
# Update the point cloud's coordinates
pcd.points = o3d.utility.Vector3dVector(points_np)
return pcd
def batch_trim(images_path: str, save_path: str, identical: bool = False) -> None:
"""
Trim white spaces from all images in the given path and save new images to folder.
:param images_path: local path to folder containing all images. Images must have the extension ".png", ".jpg", or
".jpeg".
:param save_path: local path to folder in which to save trimmed images
:param identical: if True, will apply same crop to all images, else each image will have its whitespace trimmed
independently. Note that in the latter case, each image may have a slightly different size.
"""
def get_trim(im):
"""Trim whitespace from an image and return the cropped image."""
bg = Image.new(im.mode, im.size, im.getpixel((0, 0)))
diff = ImageChops.difference(im, bg)
diff = ImageChops.add(diff, diff, 2.0, -100)
bbox = diff.getbbox()
return bbox
if identical: #
images = []
optimal_box = None
# load all images
for image_file in sorted(os.listdir(images_path)):
if image_file.endswith(IMAGE_EXTENSIONS):
image_path = os.path.join(images_path, image_file)
images.append(Image.open(image_path))
# find optimal box size
for im in images:
bbox = get_trim(im)
if bbox is None:
bbox = (0, 0, im.size[0], im.size[1]) # bound entire image
if optimal_box is None:
optimal_box = bbox
else:
optimal_box = (
min(optimal_box[0], bbox[0]),
min(optimal_box[1], bbox[1]),
max(optimal_box[2], bbox[2]),
max(optimal_box[3], bbox[3]),
)
# apply cropping, if optimal box was found
for idx, im in enumerate(images):
im.crop(optimal_box)
im.save(os.path.join(save_path, f"{idx}.png"))
im.close()
else: # trim each image separately
for image_file in os.listdir(images_path):
if image_file.endswith(IMAGE_EXTENSIONS):
image_path = os.path.join(images_path, image_file)
with Image.open(image_path) as im:
bbox = get_trim(im)
trimmed = im.crop(bbox) if bbox else im
trimmed.save(os.path.join(save_path, image_file))
def create_gif(image_folder_path: str, num_samples: int, gif_filename: str = "output.gif") -> None:
"""
Create gif out of folder of images and save to file.
:param image_folder_path: path to folder containing images (non-recursive). Assumes images are named as {i}.png for
each of i from 0 to num_samples.
:param num_samples: number of sampled images to compile into gif.
:param gif_filename: filename for gif, defaults to "output.gif"
"""
# Generate a list of image filenames (assuming the images are saved as 0.png, 1.png, etc.)
image_files = [f"{image_folder_path}/{i}.png" for i in range(num_samples)]
# Read the images using imageio
images = [imageio.imread(image_file) for image_file in image_files]
assert all(
images[0].shape == im.shape for im in images
), f"Found some images with a different shape: {[im.shape for im in images]}"
# Save images as a gif
gif_output_path = f"{image_folder_path}/{gif_filename}"
imageio.mimsave(gif_output_path, images, duration=0.1)
return
|