diff --git a/camtools/camera.py b/camtools/camera.py index e772154e..784cefc6 100644 --- a/camtools/camera.py +++ b/camtools/camera.py @@ -13,7 +13,9 @@ def create_camera_frustums( image_whs: Optional[List[List[int]]] = None, size: float = 0.1, color: Tuple[float, float, float] = (0, 0, 1), - highlight_color_map: Optional[Dict[int, Tuple[float, float, float]]] = None, + highlight_color_map: Optional[ + Dict[int, Tuple[float, float, float]] + ] = None, center_line: bool = True, center_line_color: Tuple[float, float, float] = (1, 0, 0), up_triangle: bool = True, @@ -72,7 +74,9 @@ def create_camera_frustums( if not isinstance(w, (int, np.integer)) or not isinstance( h, (int, np.integer) ): - raise ValueError(f"image_wh must be integer, but got {image_wh}.") + raise ValueError( + f"image_wh must be integer, but got {image_wh}." + ) # Wrap the highlight_color_map dimensions. if highlight_color_map is not None: @@ -115,7 +119,9 @@ def create_camera_frustum_with_Ts( image_whs: Optional[List[List[int]]] = None, size: float = 0.1, color: Tuple[float, float, float] = (0, 0, 1), - highlight_color_map: Optional[Dict[int, Tuple[float, float, float]]] = None, + highlight_color_map: Optional[ + Dict[int, Tuple[float, float, float]] + ] = None, center_line: bool = True, center_line_color: Tuple[float, float, float] = (1, 0, 0), up_triangle: bool = True, @@ -209,7 +215,9 @@ def _create_camera_frustum( sanity.assert_shape_3(color, "color") w, h = image_wh - if not isinstance(w, (int, np.integer)) or not isinstance(h, (int, np.integer)): + if not isinstance(w, (int, np.integer)) or not isinstance( + h, (int, np.integer) + ): raise ValueError(f"image_wh must be integer, but got {image_wh}.") R, _ = convert.T_to_R_t(T) @@ -223,7 +231,9 @@ def _create_camera_frustum( [0, h - 1, 1], ] ) - camera_plane_points_3d = (np.linalg.inv(K) @ camera_plane_points_2d_homo.T).T + camera_plane_points_3d = ( + np.linalg.inv(K) @ camera_plane_points_2d_homo.T + ).T camera_plane_dist = solver.point_plane_distance_three_points( [0, 0, 0], camera_plane_points_3d ) diff --git a/camtools/colmap.py b/camtools/colmap.py index ff7ecc33..5af2bc04 100644 --- a/camtools/colmap.py +++ b/camtools/colmap.py @@ -109,7 +109,9 @@ def qvec2rotmat(self): ) -def read_next_bytes(fid, num_bytes, format_char_sequence, endian_character="<"): +def read_next_bytes( + fid, num_bytes, format_char_sequence, endian_character="<" +): """Read and unpack the next bytes from a binary file. :param fid: :param num_bytes: Sum of combination of {2, 4, 8}, e.g. 2, 6, 16, 30, etc. @@ -158,7 +160,11 @@ def read_cameras_text(path): height = int(elems[3]) params = np.array(tuple(map(float, elems[4:]))) cameras[camera_id] = Camera( - id=camera_id, model=model, width=width, height=height, params=params + id=camera_id, + model=model, + width=width, + height=height, + params=params, ) return cameras @@ -183,7 +189,9 @@ def read_cameras_binary(path_to_model_file): height = camera_properties[3] num_params = CAMERA_MODEL_IDS[model_id].num_params params = read_next_bytes( - fid, num_bytes=8 * num_params, format_char_sequence="d" * num_params + fid, + num_bytes=8 * num_params, + format_char_sequence="d" * num_params, ) cameras[camera_id] = Camera( id=camera_id, @@ -254,7 +262,10 @@ def read_images_text(path): image_name = elems[9] elems = fid.readline().split() xys = np.column_stack( - [tuple(map(float, elems[0::3])), tuple(map(float, elems[1::3]))] + [ + tuple(map(float, elems[0::3])), + tuple(map(float, elems[1::3])), + ] ) point3D_ids = np.array(tuple(map(int, elems[2::3]))) images[image_id] = Image( @@ -291,16 +302,19 @@ def read_images_binary(path_to_model_file): while current_char != b"\x00": # look for the ASCII 0 entry image_name += current_char.decode("utf-8") current_char = read_next_bytes(fid, 1, "c")[0] - num_points2D = read_next_bytes(fid, num_bytes=8, format_char_sequence="Q")[ - 0 - ] + num_points2D = read_next_bytes( + fid, num_bytes=8, format_char_sequence="Q" + )[0] x_y_id_s = read_next_bytes( fid, num_bytes=24 * num_points2D, format_char_sequence="ddq" * num_points2D, ) xys = np.column_stack( - [tuple(map(float, x_y_id_s[0::3])), tuple(map(float, x_y_id_s[1::3]))] + [ + tuple(map(float, x_y_id_s[0::3])), + tuple(map(float, x_y_id_s[1::3])), + ] ) point3D_ids = np.array(tuple(map(int, x_y_id_s[2::3]))) images[image_id] = Image( @@ -339,7 +353,13 @@ def write_images_text(images, path): with open(path, "w") as fid: fid.write(HEADER) for _, img in images.items(): - image_header = [img.id, *img.qvec, *img.tvec, img.camera_id, img.name] + image_header = [ + img.id, + *img.qvec, + *img.tvec, + img.camera_id, + img.name, + ] first_line = " ".join(map(str, image_header)) fid.write(first_line + "\n") @@ -419,9 +439,9 @@ def read_points3D_binary(path_to_model_file): xyz = np.array(binary_point_line_properties[1:4]) rgb = np.array(binary_point_line_properties[4:7]) error = np.array(binary_point_line_properties[7]) - track_length = read_next_bytes(fid, num_bytes=8, format_char_sequence="Q")[ - 0 - ] + track_length = read_next_bytes( + fid, num_bytes=8, format_char_sequence="Q" + )[0] track_elems = read_next_bytes( fid, num_bytes=8 * track_length, @@ -643,7 +663,8 @@ def quat_from_rotm(R): q3[:, 3] = z q = q0 * (w[:, None] > 0) + (w[:, None] == 0) * ( q1 * (x[:, None] > 0) - + (x[:, None] == 0) * (q2 * (y[:, None] > 0) + (y[:, None] == 0) * (q3)) + + (x[:, None] == 0) + * (q2 * (y[:, None] > 0) + (y[:, None] == 0) * (q3)) ) q /= np.linalg.norm(q, axis=1, keepdims=True) return q.squeeze() @@ -819,7 +840,9 @@ def main(): ) args = parser.parse_args() - cameras, images, points3D = read_model(path=args.input_model, ext=args.input_format) + cameras, images, points3D = read_model( + path=args.input_model, ext=args.input_format + ) print("num_cameras:", len(cameras)) print("num_images:", len(images)) @@ -827,7 +850,11 @@ def main(): if args.output_model is not None: write_model( - cameras, images, points3D, path=args.output_model, ext=args.output_format + cameras, + images, + points3D, + path=args.output_model, + ext=args.output_format, ) diff --git a/camtools/convert.py b/camtools/convert.py index 3334c5fd..4f38c0b2 100644 --- a/camtools/convert.py +++ b/camtools/convert.py @@ -20,7 +20,9 @@ def pad_0001(array): """ if array.ndim == 2: if not array.shape == (3, 4): - raise ValueError(f"Expected array of shape (3, 4), but got {array.shape}.") + raise ValueError( + f"Expected array of shape (3, 4), but got {array.shape}." + ) elif array.ndim == 3: if not array.shape[-2:] == (3, 4): raise ValueError( @@ -56,7 +58,9 @@ def rm_pad_0001(array, check_vals=False): # Check shapes. if array.ndim == 2: if not array.shape == (4, 4): - raise ValueError(f"Expected array of shape (4, 4), but got {array.shape}.") + raise ValueError( + f"Expected array of shape (4, 4), but got {array.shape}." + ) elif array.ndim == 3: if not array.shape[-2:] == (4, 4): raise ValueError( @@ -77,7 +81,9 @@ def rm_pad_0001(array, check_vals=False): ) elif array.ndim == 3: bottom = array[:, 3:4, :] - expected_bottom = np.broadcast_to([0, 0, 0, 1], (array.shape[0], 1, 4)) + expected_bottom = np.broadcast_to( + [0, 0, 0, 1], (array.shape[0], 1, 4) + ) if not np.allclose(bottom, expected_bottom): raise ValueError( f"Expected bottom row to be {expected_bottom}, but got {bottom}." @@ -99,7 +105,9 @@ def to_homo(array): A numpy array of shape (N, M+1) with a column of ones appended. """ if not isinstance(array, np.ndarray) or array.ndim != 2: - raise ValueError(f"Input must be a 2D numpy array, but got {array.shape}.") + raise ValueError( + f"Input must be a 2D numpy array, but got {array.shape}." + ) ones = np.ones((array.shape[0], 1), dtype=array.dtype) return np.hstack((array, ones)) @@ -117,7 +125,9 @@ def from_homo(array): A numpy array of shape (N, M-1) in Cartesian coordinates. """ if not isinstance(array, np.ndarray) or array.ndim != 2: - raise ValueError(f"Input must be a 2D numpy array, but got {array.shape}.") + raise ValueError( + f"Input must be a 2D numpy array, but got {array.shape}." + ) if array.shape[1] < 2: raise ValueError( f"Input array must have at least two columns for removing " @@ -211,7 +221,9 @@ def pose_to_T(pose): return np.linalg.inv(pose) -def T_opengl_to_opencv(T: Float[np.ndarray, "4 4"]) -> Float[np.ndarray, "4 4"]: +def T_opengl_to_opencv( + T: Float[np.ndarray, "4 4"] +) -> Float[np.ndarray, "4 4"]: """ Convert T from OpenGL convention to OpenCV convention. @@ -239,7 +251,9 @@ def T_opengl_to_opencv(T: Float[np.ndarray, "4 4"]) -> Float[np.ndarray, "4 4"]: return T -def T_opencv_to_opengl(T: Float[np.ndarray, "4 4"]) -> Float[np.ndarray, "4 4"]: +def T_opencv_to_opengl( + T: Float[np.ndarray, "4 4"] +) -> Float[np.ndarray, "4 4"]: """ Convert T from OpenCV convention to OpenGL convention. @@ -267,7 +281,9 @@ def T_opencv_to_opengl(T: Float[np.ndarray, "4 4"]) -> Float[np.ndarray, "4 4"]: return T -def pose_opengl_to_opencv(pose: Float[np.ndarray, "4 4"]) -> Float[np.ndarray, "4 4"]: +def pose_opengl_to_opencv( + pose: Float[np.ndarray, "4 4"] +) -> Float[np.ndarray, "4 4"]: """ Convert pose from OpenGL convention to OpenCV convention. @@ -292,7 +308,9 @@ def pose_opengl_to_opencv(pose: Float[np.ndarray, "4 4"]) -> Float[np.ndarray, " return pose -def pose_opencv_to_opengl(pose: Float[np.ndarray, "4 4"]) -> Float[np.ndarray, "4 4"]: +def pose_opencv_to_opengl( + pose: Float[np.ndarray, "4 4"] +) -> Float[np.ndarray, "4 4"]: """ Convert pose from OpenCV convention to OpenGL convention. @@ -446,7 +464,9 @@ def T_to_R_t( def P_to_K_R_t( P: Float[np.ndarray, "3 4"], -) -> Tuple[Float[np.ndarray, "3 3"], Float[np.ndarray, "3 3"], Float[np.ndarray, "3"]]: +) -> Tuple[ + Float[np.ndarray, "3 3"], Float[np.ndarray, "3 3"], Float[np.ndarray, "3"] +]: """ Decompose projection matrix P into intrinsic matrix K, rotation matrix R, and translation vector t. @@ -785,7 +805,9 @@ def mesh_to_lineset( if color is not None: if len(color) != 3: - raise ValueError(f"Expected color of shape (3,), but got {color.shape}.") + raise ValueError( + f"Expected color of shape (3,), but got {color.shape}." + ) lineset.paint_uniform_color(color) return lineset diff --git a/camtools/geometry.py b/camtools/geometry.py index b972ef78..8a2f59cc 100644 --- a/camtools/geometry.py +++ b/camtools/geometry.py @@ -62,7 +62,9 @@ def mesh_to_lineset( """ # Downsample mesh if downsample_ratio < 1.0: - target_number_of_triangles = int(len(mesh.triangles) * downsample_ratio) + target_number_of_triangles = int( + len(mesh.triangles) * downsample_ratio + ) mesh = mesh.simplify_quadric_decimation(target_number_of_triangles) elif downsample_ratio > 1.0: raise ValueError("Subsample must be less than or equal to 1.0") diff --git a/camtools/image.py b/camtools/image.py index 01929075..c193ab0f 100644 --- a/camtools/image.py +++ b/camtools/image.py @@ -7,7 +7,8 @@ def crop_white_boarders( - im: Float[np.ndarray, "h w 3"], padding: Tuple[int, int, int, int] = (0, 0, 0, 0) + im: Float[np.ndarray, "h w 3"], + padding: Tuple[int, int, int, int] = (0, 0, 0, 0), ) -> Float[np.ndarray, "h_cropped w_cropped 3"]: """ Crop white borders from an image and apply optional padding. @@ -27,7 +28,9 @@ def crop_white_boarders( return im_dst -def compute_cropping_v1(im: Float[np.ndarray, "h w n"]) -> Tuple[int, int, int, int]: +def compute_cropping_v1( + im: Float[np.ndarray, "h w n"] +) -> Tuple[int, int, int, int]: """ Compute white border sizes in pixels for multi-channel images. @@ -115,9 +118,13 @@ def compute_cropping( ValueError: If input image has invalid dtype, dimensions, or fails v1 check. """ if not im.dtype == np.float32: - raise ValueError(f"Expected im.dtype to be np.float32, but got {im.dtype}") + raise ValueError( + f"Expected im.dtype to be np.float32, but got {im.dtype}" + ) if im.ndim != 3 or im.shape[2] != 3: - raise ValueError(f"Expected im to be of shape (H, W, 3), but got {im.shape}") + raise ValueError( + f"Expected im to be of shape (H, W, 3), but got {im.shape}" + ) # Create a mask where white pixels are marked as True white_mask = np.all(im == 1.0, axis=-1) @@ -128,9 +135,13 @@ def compute_cropping( # Determine the crop values based on the positions of non-white pixels crop_t = rows_with_color[0] if len(rows_with_color) else 0 - crop_b = im.shape[0] - rows_with_color[-1] - 1 if len(rows_with_color) else 0 + crop_b = ( + im.shape[0] - rows_with_color[-1] - 1 if len(rows_with_color) else 0 + ) crop_l = cols_with_color[0] if len(cols_with_color) else 0 - crop_r = im.shape[1] - cols_with_color[-1] - 1 if len(cols_with_color) else 0 + crop_r = ( + im.shape[1] - cols_with_color[-1] - 1 if len(cols_with_color) else 0 + ) # Check the results against compute_cropping_v1 if requested if check_with_v1: @@ -316,7 +327,9 @@ def overlay_mask_on_rgb( assert overlay_color.max() <= 1.0 and overlay_color.min() >= 0.0 im_mask_stacked = np.dstack([im_mask, im_mask, im_mask]) - im_hard = im_rgb * (1.0 - im_mask_stacked) + overlay_color * im_mask_stacked + im_hard = ( + im_rgb * (1.0 - im_mask_stacked) + overlay_color * im_mask_stacked + ) im_soft = im_rgb * (1.0 - overlay_alpha) + im_hard * overlay_alpha return im_soft @@ -368,7 +381,9 @@ def ndc_coords_to_pixels( dst_tl = np.array([-0.5, -0.5], dtype=dtype) dst_br = np.array([w - 0.5, h - 0.5], dtype=dtype) - dst_pixels = (ndc_coords - src_tl) / (src_br - src_tl) * (dst_br - dst_tl) + dst_tl + dst_pixels = (ndc_coords - src_tl) / (src_br - src_tl) * ( + dst_br - dst_tl + ) + dst_tl return dst_pixels @@ -467,7 +482,9 @@ def recover_rotated_pixels(dst_pixels, src_wh, ccw_degrees): dst_pixels_recovered = np.stack([h - 1 - src_r, src_c], axis=1) else: raise ValueError(f"Invalid rotation angle: {ccw_degrees}.") - np.testing.assert_allclose(dst_pixels, dst_pixels_recovered, rtol=1e-5, atol=1e-5) + np.testing.assert_allclose( + dst_pixels, dst_pixels_recovered, rtol=1e-5, atol=1e-5 + ) return src_pixels @@ -595,7 +612,9 @@ def resize( if tmp_w == dst_w and tmp_h == dst_h: im_resize = im_tmp else: - im_resize = np.full(dst_numpy_shape, fill_value=aspect_ratio_fill, dtype=dtype) + im_resize = np.full( + dst_numpy_shape, fill_value=aspect_ratio_fill, dtype=dtype + ) im_resize[:tmp_h, :tmp_w] = im_tmp # Final sanity checks for the reshaped image. @@ -672,7 +691,9 @@ def recover_resized_pixels( src_br = np.array([src_w - 0.5, src_h - 0.5]) dst_tl = np.array([-0.5, -0.5]) dst_br = np.array([tmp_w - 0.5, tmp_h - 0.5]) - src_pixels = (dst_pixels - dst_tl) / (dst_br - dst_tl) * (src_br - src_tl) + src_tl + src_pixels = (dst_pixels - dst_tl) / (dst_br - dst_tl) * ( + src_br - src_tl + ) + src_tl return src_pixels @@ -738,7 +759,9 @@ def make_corres_image( if confidences is not None: assert len(confidences) == len(src_pixels) - assert confidences.dtype == np.float32 or confidences.dtype == np.float64 + assert ( + confidences.dtype == np.float32 or confidences.dtype == np.float64 + ) if confidences.size > 0: assert confidences.min() >= 0.0 and confidences.max() <= 1.0 assert confidences.ndim == 1 @@ -783,7 +806,9 @@ def make_corres_image( assert sample_ratio > 0.0 and sample_ratio <= 1.0 num_points = len(src_pixels) num_samples = int(round(num_points * sample_ratio)) - sample_indices = np.random.choice(num_points, num_samples, replace=False) + sample_indices = np.random.choice( + num_points, num_samples, replace=False + ) src_pixels = src_pixels[sample_indices] dst_pixels = dst_pixels[sample_indices] confidences = confidences[sample_indices] @@ -795,8 +820,12 @@ def make_corres_image( if confidences is None: # Draw white points as mask. - im_point_mask = np.zeros(im_corres.shape[:2], dtype=im_corres.dtype) - for (src_c, src_r), (dst_c, dst_r) in zip(src_pixels, dst_pixels): + im_point_mask = np.zeros( + im_corres.shape[:2], dtype=im_corres.dtype + ) + for (src_c, src_r), (dst_c, dst_r) in zip( + src_pixels, dst_pixels + ): cv2.circle( im_point_mask, (src_c, src_r), @@ -851,7 +880,11 @@ def make_corres_image( im_line_mask = np.zeros(im_corres.shape[:2], dtype=im_corres.dtype) for (src_c, src_r), (dst_c, dst_r) in zip(src_pixels, dst_pixels): cv2.line( - im_line_mask, (src_c, src_r), (dst_c + w, dst_r), (1,), line_width + im_line_mask, + (src_c, src_r), + (dst_c + w, dst_r), + (1,), + line_width, ) line_alpha = line_color[3] if len(line_color) == 4 else 1.0 @@ -963,9 +996,9 @@ def vstack_images( if alignment == "center" else max_width - im.shape[1] if alignment == "right" else 0 ) - im_stacked[curr_row : curr_row + im.shape[0], offset : offset + im.shape[1]] = ( - im - ) + im_stacked[ + curr_row : curr_row + im.shape[0], offset : offset + im.shape[1] + ] = im curr_row += im.shape[0] return im_stacked diff --git a/camtools/io.py b/camtools/io.py index 93fc68f9..6f60c603 100644 --- a/camtools/io.py +++ b/camtools/io.py @@ -259,7 +259,8 @@ def imread( if im.shape[2] == 4: if alpha_mode is None: raise ValueError( - f"{im_path} has an alpha channel, alpha_mode " f"must be specified." + f"{im_path} has an alpha channel, alpha_mode " + f"must be specified." ) elif alpha_mode == "keep": pass @@ -278,7 +279,8 @@ def imread( im = im[..., :3] * im[..., 3:] else: raise ValueError( - f"Unexpected alpha_mode: {alpha_mode} for a " "4-channel image." + f"Unexpected alpha_mode: {alpha_mode} for a " + "4-channel image." ) elif im.shape[2] == 3: pass diff --git a/camtools/metric.py b/camtools/metric.py index d0166280..a1d7fd17 100644 --- a/camtools/metric.py +++ b/camtools/metric.py @@ -113,7 +113,12 @@ def image_lpips( loss_fn = lpips.LPIPS(net="alex") image_lpips.static_vars["loss_fn"] = loss_fn - ans = loss_fn.forward(torch.tensor(pr), torch.tensor(gt)).cpu().detach().numpy() + ans = ( + loss_fn.forward(torch.tensor(pr), torch.tensor(gt)) + .cpu() + .detach() + .numpy() + ) return float(ans) @@ -198,7 +203,9 @@ def load_im_pd_im_gt_im_mask_for_eval( im_mask_path: Optional[Union[str, Path]] = None, alpha_mode: str = "white", ) -> Tuple[ - Float[np.ndarray, "h w 3"], Float[np.ndarray, "h w 3"], Float[np.ndarray, "h w"] + Float[np.ndarray, "h w 3"], + Float[np.ndarray, "h w 3"], + Float[np.ndarray, "h w"], ]: """ Load predicted image, ground truth image, and mask for evaluation. @@ -240,7 +247,9 @@ def load_im_pd_im_gt_im_mask_for_eval( ... 'pred.png', 'gt.png', 'mask.png', 1.0) """ if alpha_mode != "white": - raise NotImplementedError('Currently only alpha_mode="white" is supported.') + raise NotImplementedError( + 'Currently only alpha_mode="white" is supported.' + ) # Prepare im_gt. # (h, w, 3) or (h, w, 4), float32. diff --git a/camtools/normalize.py b/camtools/normalize.py index a4093cc7..d09e4bea 100644 --- a/camtools/normalize.py +++ b/camtools/normalize.py @@ -2,7 +2,9 @@ from jaxtyping import Float -def compute_normalize_mat(points: Float[np.ndarray, "n 3"]) -> Float[np.ndarray, "4 4"]: +def compute_normalize_mat( + points: Float[np.ndarray, "n 3"] +) -> Float[np.ndarray, "4 4"]: """ Args: points: (N, 3) numpy array. diff --git a/camtools/render.py b/camtools/render.py index 347e0691..27804454 100644 --- a/camtools/render.py +++ b/camtools/render.py @@ -417,7 +417,9 @@ def align_vector_to_another( axis = np.cross(a, b) axis /= np.linalg.norm(axis) angle = np.arccos( - np.clip(np.dot(a / np.linalg.norm(a), b / np.linalg.norm(b)), -1.0, 1.0) + np.clip( + np.dot(a / np.linalg.norm(a), b / np.linalg.norm(b)), -1.0, 1.0 + ) ) return axis, angle @@ -441,9 +443,13 @@ def normalized(a: np.ndarray) -> Tuple[np.ndarray, float]: start_point, end_point = points[line[0]], points[line[1]] line_segment = end_point - start_point line_segment_unit, line_length = normalized(line_segment) - axis, angle = align_vector_to_another(np.array([0, 0, 1]), line_segment_unit) + axis, angle = align_vector_to_another( + np.array([0, 0, 1]), line_segment_unit + ) translation = start_point + line_segment * 0.5 - cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius, line_length) + cylinder = o3d.geometry.TriangleMesh.create_cylinder( + radius, line_length + ) cylinder.translate(translation, relative=False) if not np.isclose(angle, 0): axis_angle = axis * angle @@ -699,7 +705,9 @@ def render_texts( (0, 0), ( (max_width - im.shape[1]) // 2, - max_width - im.shape[1] - (max_width - im.shape[1]) // 2, + max_width + - im.shape[1] + - (max_width - im.shape[1]) // 2, ), (0, 0), ), diff --git a/camtools/sanity.py b/camtools/sanity.py index 5a19af8e..f78800f9 100644 --- a/camtools/sanity.py +++ b/camtools/sanity.py @@ -16,7 +16,9 @@ def assert_numpy(x, name=None): """ if not isinstance(x, np.ndarray): maybe_name = f" {name}" if name is not None else "" - raise ValueError(f"Expected{maybe_name} to be numpy array, but got {type(x)}.") + raise ValueError( + f"Expected{maybe_name} to be numpy array, but got {type(x)}." + ) def assert_K(K: Float[np.ndarray, "3 3"]): @@ -39,7 +41,9 @@ def assert_K(K: Float[np.ndarray, "3 3"]): ValueError: If K is not a 3x3 matrix """ if K.shape != (3, 3): - raise ValueError(f"K must has shape (3, 3), but got {K} of shape {K.shape}.") + raise ValueError( + f"K must has shape (3, 3), but got {K} of shape {K.shape}." + ) def assert_T(T: Float[np.ndarray, "4 4"]): @@ -63,10 +67,14 @@ def assert_T(T: Float[np.ndarray, "4 4"]): ValueError: If T is not a 4x4 matrix or bottom row is not [0, 0, 0, 1] """ if T.shape != (4, 4): - raise ValueError(f"T must has shape (4, 4), but got {T} of shape {T.shape}.") + raise ValueError( + f"T must has shape (4, 4), but got {T} of shape {T.shape}." + ) is_valid = np.allclose(T[3, :], np.array([0, 0, 0, 1])) if not is_valid: - raise ValueError(f"T must has [0, 0, 0, 1] the bottom row, but got {T}.") + raise ValueError( + f"T must has [0, 0, 0, 1] the bottom row, but got {T}." + ) def assert_pose(pose: Float[np.ndarray, "4 4"]): @@ -97,7 +105,9 @@ def assert_pose(pose: Float[np.ndarray, "4 4"]): ) is_valid = np.allclose(pose[3, :], np.array([0, 0, 0, 1])) if not is_valid: - raise ValueError(f"pose must has [0, 0, 0, 1] the bottom row, but got {pose}.") + raise ValueError( + f"pose must has [0, 0, 0, 1] the bottom row, but got {pose}." + ) def assert_shape(x: np.ndarray, shape: tuple, name: Optional[str] = None): @@ -144,7 +154,9 @@ def assert_shape(x: np.ndarray, shape: tuple, name: Optional[str] = None): if not shape_valid: name_must = f"{name} must" if name is not None else "Must" - raise ValueError(f"{name_must} has shape {shape}, but got shape {x.shape}.") + raise ValueError( + f"{name_must} has shape {shape}, but got shape {x.shape}." + ) def assert_shape_ndim(x: np.ndarray, ndim: int, name: Optional[str] = None): @@ -161,7 +173,9 @@ def assert_shape_ndim(x: np.ndarray, ndim: int, name: Optional[str] = None): """ if x.ndim != ndim: name_must = f"{name} must" if name is not None else "Must" - raise ValueError(f"{name_must} have {ndim} dimensions, but got {x.ndim}.") + raise ValueError( + f"{name_must} have {ndim} dimensions, but got {x.ndim}." + ) def assert_shape_nx3(x: np.ndarray, name: Optional[str] = None): diff --git a/camtools/solver.py b/camtools/solver.py index bd7996d5..363354f9 100644 --- a/camtools/solver.py +++ b/camtools/solver.py @@ -23,9 +23,13 @@ def line_intersection_3d( https://math.stackexchange.com/a/1762491/209055 """ if src_points.ndim != 2 or src_points.shape[1] != 3: - raise ValueError(f"src_points must be (N, 3), but got {src_points.shape}.") + raise ValueError( + f"src_points must be (N, 3), but got {src_points.shape}." + ) if dst_points.ndim != 2 or dst_points.shape[1] != 3: - raise ValueError(f"dst_points must be (N, 3), but got {dst_points.shape}.") + raise ValueError( + f"dst_points must be (N, 3), but got {dst_points.shape}." + ) dirs = dst_points - src_points dirs = dirs / np.linalg.norm(dirs, axis=1).reshape((-1, 1)) @@ -208,7 +212,9 @@ def points_to_mesh_distances( np.ndarray: Array of distances with shape (N,). """ if not points.ndim == 2 or points.shape[1] != 3: - raise ValueError(f"Expected points of shape (N, 3), but got {points.shape}.") + raise ValueError( + f"Expected points of shape (N, 3), but got {points.shape}." + ) mesh_t = o3d.t.geometry.TriangleMesh.from_legacy(mesh) scene = o3d.t.geometry.RaycastingScene() _ = scene.add_triangles(mesh_t) diff --git a/camtools/tools/cli.py b/camtools/tools/cli.py index a2f2d5e1..87a76b6d 100644 --- a/camtools/tools/cli.py +++ b/camtools/tools/cli.py @@ -4,7 +4,9 @@ def _print_greetings(): - greeting_str = f"* CamTools: Camera Tools for Computer Vision (v{ct.__version__}) *" + greeting_str = ( + f"* CamTools: Camera Tools for Computer Vision (v{ct.__version__}) *" + ) header = "*" * len(greeting_str) print(header) print(greeting_str) diff --git a/camtools/tools/compress_images.py b/camtools/tools/compress_images.py index 751f7838..236053b6 100644 --- a/camtools/tools/compress_images.py +++ b/camtools/tools/compress_images.py @@ -82,7 +82,9 @@ def entry_point(parser, args): # Handle PNG file's alpha channel. src_paths_with_alpha = [] - png_paths = [src_path for src_path in src_paths if ct.io.is_png_path(src_path)] + png_paths = [ + src_path for src_path in src_paths if ct.io.is_png_path(src_path) + ] for src_path in png_paths: im = ct.io.imread(src_path, alpha_mode="keep") if im.shape[2] == 4: @@ -183,8 +185,12 @@ def entry_point(parser, args): print(f" - compression_ratio: {compression_ratio:.2f}") # Update text files. - src_paths = [stat["src_path"] for stat in stats if not stat["is_direct_copy"]] - dst_paths = [stat["dst_path"] for stat in stats if not stat["is_direct_copy"]] + src_paths = [ + stat["src_path"] for stat in stats if not stat["is_direct_copy"] + ] + dst_paths = [ + stat["dst_path"] for stat in stats if not stat["is_direct_copy"] + ] if num_compressed > 0 and update_texts_in_dir is not None: do_update_texts_in_dir( src_paths=src_paths, @@ -343,7 +349,9 @@ def is_text_file(path): root_dir = Path(root_dir) text_paths = list(root_dir.glob("**/*")) - text_paths = [text_path for text_path in text_paths if is_text_file(text_path)] + text_paths = [ + text_path for text_path in text_paths if is_text_file(text_path) + ] return text_paths diff --git a/camtools/tools/crop_boarders.py b/camtools/tools/crop_boarders.py index 67f31ead..cf9bc6e4 100644 --- a/camtools/tools/crop_boarders.py +++ b/camtools/tools/crop_boarders.py @@ -74,9 +74,13 @@ def entry_point(parser, args): The parser argument is not used. """ if args.pad_pixel < 0: - raise ValueError(f"pad_pixel must be non-negative, but got {args.pad_pixel}") + raise ValueError( + f"pad_pixel must be non-negative, but got {args.pad_pixel}" + ) if args.pad_ratio < 0: - raise ValueError(f"pad_ratio must be non-negative, but got {args.pad_ratio}") + raise ValueError( + f"pad_ratio must be non-negative, but got {args.pad_ratio}" + ) # Determine src and dst paths. if isinstance(args.input, list): @@ -95,7 +99,8 @@ def entry_point(parser, args): else: if args.skip_cropped: dst_paths = [ - src_path.parent / f"cropped_{src_path.name}" for src_path in src_paths + src_path.parent / f"cropped_{src_path.name}" + for src_path in src_paths ] skipped_src_paths = [p for p in src_paths if p in dst_paths] src_paths = [p for p in src_paths if p not in dst_paths] @@ -104,7 +109,8 @@ def entry_point(parser, args): for src_path in skipped_src_paths: print(f" - {src_path}") dst_paths = [ - src_path.parent / f"cropped_{src_path.name}" for src_path in src_paths + src_path.parent / f"cropped_{src_path.name}" + for src_path in src_paths ] # Read. @@ -112,9 +118,13 @@ def entry_point(parser, args): for src_im in src_ims: if not src_im.dtype == np.float32: - raise ValueError(f"Input image {src_path} must be of dtype float32.") + raise ValueError( + f"Input image {src_path} must be of dtype float32." + ) if not src_im.ndim == 3 or not src_im.shape[2] == 3: - raise ValueError(f"Input image {src_path} must be of shape (H, W, 3).") + raise ValueError( + f"Input image {src_path} must be of shape (H, W, 3)." + ) num_ims = len(src_ims) # Compute. @@ -123,19 +133,26 @@ def entry_point(parser, args): shapes = [im.shape for im in src_ims] if not all([s == shapes[0] for s in shapes]): raise ValueError( - "All images must be of the same shape when --same_crop is " "specified." + "All images must be of the same shape when --same_crop is " + "specified." ) - individual_croppings = ct.util.mt_loop(ct.image.compute_cropping, src_ims) + individual_croppings = ct.util.mt_loop( + ct.image.compute_cropping, src_ims + ) # Compute the minimum cropping boarders. - min_crop_u, min_crop_d, min_crop_l, min_crop_r = individual_croppings[0] + min_crop_u, min_crop_d, min_crop_l, min_crop_r = individual_croppings[ + 0 + ] for crop_u, crop_d, crop_l, crop_r in individual_croppings[1:]: min_crop_u = min(min_crop_u, crop_u) min_crop_d = min(min_crop_d, crop_d) min_crop_l = min(min_crop_l, crop_l) min_crop_r = min(min_crop_r, crop_r) - croppings = [(min_crop_u, min_crop_d, min_crop_l, min_crop_r)] * len(src_ims) + croppings = [(min_crop_u, min_crop_d, min_crop_l, min_crop_r)] * len( + src_ims + ) # Compute padding (remains unchanged) if args.pad_pixel != 0: @@ -184,7 +201,9 @@ def entry_point(parser, args): ) ) for i in range(num_ims): - paddings[i] = tuple(np.array(paddings[i]) + np.array(extra_paddings[i])) + paddings[i] = tuple( + np.array(paddings[i]) + np.array(extra_paddings[i]) + ) # Apply. dst_ims = ct.image.apply_croppings_paddings( diff --git a/camtools/tools/draw_bboxes.py b/camtools/tools/draw_bboxes.py index 9fd3e92b..ae72f305 100644 --- a/camtools/tools/draw_bboxes.py +++ b/camtools/tools/draw_bboxes.py @@ -100,7 +100,9 @@ def _bbox_str(bbox: matplotlib.transforms.Bbox) -> str: """ A better matplotlib.transforms.Bbox.__str__()` """ - return f"Bbox({bbox.x0:.2f}, {bbox.y0:.2f}, {bbox.x1:.2f}, {bbox.y1:.2f})" + return ( + f"Bbox({bbox.x0:.2f}, {bbox.y0:.2f}, {bbox.x1:.2f}, {bbox.y1:.2f})" + ) @staticmethod def _copy_rectangle( @@ -116,9 +118,21 @@ def _copy_rectangle( xy=(rectangle.xy[0], rectangle.xy[1]), width=rectangle.get_width(), height=rectangle.get_height(), - linestyle=linestyle if linestyle is not None else rectangle.get_linestyle(), - linewidth=linewidth if linewidth is not None else rectangle.get_linewidth(), - edgecolor=edgecolor if edgecolor is not None else rectangle.get_edgecolor(), + linestyle=( + linestyle + if linestyle is not None + else rectangle.get_linestyle() + ), + linewidth=( + linewidth + if linewidth is not None + else rectangle.get_linewidth() + ), + edgecolor=( + edgecolor + if edgecolor is not None + else rectangle.get_edgecolor() + ), facecolor=rectangle.get_facecolor(), ) return new_rectangle @@ -221,7 +235,9 @@ def fill_connected_component(mat, x, y): (br_bound[0], br_bound[1]), # Bottom-right ] for corner in corners: - im_mask = fill_connected_component(im_mask, corner[0], corner[1]) + im_mask = fill_connected_component( + im_mask, corner[0], corner[1] + ) # 4. Undo mask invalid pixels. im_mask[im_mask == -1.0] = 0.0 @@ -288,7 +304,9 @@ def _save(self) -> None: im_height = im_shape[0] axis = self.axes[0] - bbox = axis.get_window_extent().transformed(self.fig.dpi_scale_trans.inverted()) + bbox = axis.get_window_extent().transformed( + self.fig.dpi_scale_trans.inverted() + ) axis_height = bbox.height * self.fig.dpi # Get the linewidth in pixels. @@ -296,7 +314,9 @@ def _save(self) -> None: linewidth_px = linewidth_px / axis_height * im_height linewidth_px = int(round(linewidth_px)) - dst_paths = [p.parent / f"bbox_{p.stem}{p.suffix}" for p in self.src_paths] + dst_paths = [ + p.parent / f"bbox_{p.stem}{p.suffix}" for p in self.src_paths + ] for src_path, dst_path in zip(self.src_paths, dst_paths): im_dst = ct.io.imread(src_path) for rectangle in self.confirmed_rectangles: @@ -351,7 +371,9 @@ def print_msg(*args, **kwargs): self.confirmed_rectangles.append( BBoxer._copy_rectangle(self.current_rectangle) ) - bbox_str = BBoxer._bbox_str(self.current_rectangle.get_bbox()) + bbox_str = BBoxer._bbox_str( + self.current_rectangle.get_bbox() + ) print_msg(f"Bounding box saved: {bbox_str}.") # Clear current. self.current_rectangle = None diff --git a/camtools/util.py b/camtools/util.py index 7e6ca5e8..b8e51a47 100644 --- a/camtools/util.py +++ b/camtools/util.py @@ -1,4 +1,8 @@ -from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed +from concurrent.futures import ( + ProcessPoolExecutor, + ThreadPoolExecutor, + as_completed, +) from typing import Any, Callable, Iterable, Optional from functools import lru_cache @@ -26,10 +30,13 @@ def mt_loop( desc = f"[mt] {func.__name__}" with ThreadPoolExecutor() as executor: future_to_index = { - executor.submit(func, item, **kwargs): i for i, item in enumerate(inputs) + executor.submit(func, item, **kwargs): i + for i, item in enumerate(inputs) } results = [None] * len(inputs) - for future in tqdm(as_completed(future_to_index), total=len(inputs), desc=desc): + for future in tqdm( + as_completed(future_to_index), total=len(inputs), desc=desc + ): results[future_to_index[future]] = future.result() return results @@ -55,10 +62,13 @@ def mp_loop( desc = f"[mp] {func.__name__}" with ProcessPoolExecutor() as executor: future_to_index = { - executor.submit(func, item, **kwargs): i for i, item in enumerate(inputs) + executor.submit(func, item, **kwargs): i + for i, item in enumerate(inputs) } results = [None] * len(inputs) - for future in tqdm(as_completed(future_to_index), total=len(inputs), desc=desc): + for future in tqdm( + as_completed(future_to_index), total=len(inputs), desc=desc + ): results[future_to_index[future]] = future.result() return results diff --git a/test/conftest.py b/test/conftest.py index b3a16186..144d8024 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -4,7 +4,8 @@ def pytest_configure(config): config.addinivalue_line( - "markers", "skip_no_o3d_display: skip test when no display is available" + "markers", + "skip_no_o3d_display: skip test when no display is available", ) diff --git a/test/test_convert.py b/test/test_convert.py index b5c96de9..4583d775 100644 --- a/test/test_convert.py +++ b/test/test_convert.py @@ -121,7 +121,11 @@ def gen_random_pose(): [-axis[1], axis[0], 0], ] ) - RT = np.eye(3) + np.sin(angle) * ss + (1 - np.cos(angle)) * np.dot(ss, ss) + RT = ( + np.eye(3) + + np.sin(angle) * ss + + (1 - np.cos(angle)) * np.dot(ss, ss) + ) c = np.random.uniform(-10, 10, size=(3,)) pose = np.eye(4) pose[:3, :3] = RT @@ -138,8 +142,12 @@ def gen_random_pose(): pose_gl = ct.convert.pose_opencv_to_opengl(pose_cv) pose_cv_recovered = ct.convert.pose_opengl_to_opencv(pose_gl) pose_gl_recovered = ct.convert.pose_opencv_to_opengl(pose_cv_recovered) - np.testing.assert_allclose(pose_cv, pose_cv_recovered, rtol=1e-5, atol=1e-5) - np.testing.assert_allclose(pose_gl, pose_gl_recovered, rtol=1e-5, atol=1e-5) + np.testing.assert_allclose( + pose_cv, pose_cv_recovered, rtol=1e-5, atol=1e-5 + ) + np.testing.assert_allclose( + pose_gl, pose_gl_recovered, rtol=1e-5, atol=1e-5 + ) # Test convert T bidirectionally T_cv = np.copy(T) @@ -208,8 +216,12 @@ def gen_random_T(): pose_gl = ct.convert.pose_opencv_to_opengl(pose_cv) pose_cv_recovered = ct.convert.pose_opengl_to_opencv(pose_gl) pose_gl_recovered = ct.convert.pose_opencv_to_opengl(pose_cv_recovered) - np.testing.assert_allclose(pose_cv, pose_cv_recovered, rtol=1e-5, atol=1e-5) - np.testing.assert_allclose(pose_gl, pose_gl_recovered, rtol=1e-5, atol=1e-5) + np.testing.assert_allclose( + pose_cv, pose_cv_recovered, rtol=1e-5, atol=1e-5 + ) + np.testing.assert_allclose( + pose_gl, pose_gl_recovered, rtol=1e-5, atol=1e-5 + ) # Test T and pose are consistent across conversions np.testing.assert_allclose( @@ -292,7 +304,9 @@ def test_im_depth_im_distance_convert(): # Geometries sphere = o3d.geometry.TriangleMesh.create_sphere(radius=1.0) sphere = sphere.translate([0, 0, 4]) - box = o3d.geometry.TriangleMesh.create_box(width=1.5, height=1.5, depth=1.5) + box = o3d.geometry.TriangleMesh.create_box( + width=1.5, height=1.5, depth=1.5 + ) box = box.translate([0, 0, 4]) mesh = sphere + box @@ -313,4 +327,6 @@ def test_im_depth_im_distance_convert(): im_depth_reconstructed = ct.convert.im_distance_to_im_depth(im_distance, K) # Assert that the reconstructed depth is close to the original - np.testing.assert_allclose(im_depth, im_depth_reconstructed, rtol=1e-5, atol=1e-5) + np.testing.assert_allclose( + im_depth, im_depth_reconstructed, rtol=1e-5, atol=1e-5 + ) diff --git a/test/test_raycast.py b/test/test_raycast.py index 3ec71966..d3333487 100644 --- a/test/test_raycast.py +++ b/test/test_raycast.py @@ -17,7 +17,9 @@ def test_mesh_to_depth(visualize: bool): # Geometries sphere = o3d.geometry.TriangleMesh.create_sphere(radius=1.0) sphere = sphere.translate([0, 0, 4]) - box = o3d.geometry.TriangleMesh.create_box(width=1.5, height=1.5, depth=1.5) + box = o3d.geometry.TriangleMesh.create_box( + width=1.5, height=1.5, depth=1.5 + ) box = box.translate([0, 0, 4]) mesh = sphere + box lineset = ct.convert.mesh_to_lineset(mesh) diff --git a/test/test_render.py b/test/test_render.py index de667b08..7c322d50 100644 --- a/test/test_render.py +++ b/test/test_render.py @@ -18,11 +18,15 @@ def test_render_geometries(visualize: bool): See conftest.py for more information on the visualize fixture. """ # Setup geometries: sphere (red), box (blue) - sphere = o3d.geometry.TriangleMesh.create_sphere(radius=1.0, resolution=100) + sphere = o3d.geometry.TriangleMesh.create_sphere( + radius=1.0, resolution=100 + ) sphere = sphere.translate([0, 0, 4]) sphere = sphere.paint_uniform_color([0.2, 0.4, 0.8]) sphere.compute_vertex_normals() - box = o3d.geometry.TriangleMesh.create_box(width=1.5, height=1.5, depth=1.5) + box = o3d.geometry.TriangleMesh.create_box( + width=1.5, height=1.5, depth=1.5 + ) box = box.translate([0, 0, 4]) box = box.paint_uniform_color([0.8, 0.2, 0.2]) box.compute_vertex_normals() @@ -65,7 +69,11 @@ def test_render_geometries(visualize: bool): im_raycast_depth[im_raycast_depth == np.inf] = 0 # Heuristic checks of RGB rendering - assert im_render_rgb.shape == (height, width, 3), "Image has incorrect dimensions" + assert im_render_rgb.shape == ( + height, + width, + 3, + ), "Image has incorrect dimensions" num_white_pixels = np.sum( (im_render_rgb[:, :, 0] > 0.9) & (im_render_rgb[:, :, 1] > 0.9) @@ -81,7 +89,9 @@ def test_render_geometries(visualize: bool): & (im_render_rgb[:, :, 1] < 0.3) & (im_render_rgb[:, :, 2] < 0.5) ) - assert num_white_pixels > (height * width * 0.5), "Expected mostly white background" + assert num_white_pixels > ( + height * width * 0.5 + ), "Expected mostly white background" assert num_blue_pixels > 100, "Expected blue pixels (sphere) not found" assert num_red_pixels > 100, "Expected red pixels (box) not found" @@ -98,7 +108,8 @@ def test_render_geometries(visualize: bool): im_render_rgb_mask.astype(float) - im_render_depth_mask.astype(float) ) im_mask_diff_raycast_vs_render = np.abs( - im_raycast_depth_mask.astype(float) - im_render_depth_mask.astype(float) + im_raycast_depth_mask.astype(float) + - im_render_depth_mask.astype(float) ) assert ( np.mean(im_mask_diff_rgb_vs_raycast) < 0.01