Spaces:
Running
Running
| # -*- encoding: utf-8 -*- | |
| """ | |
| @File : camera.py | |
| @Time : 2025/9/3 15:25:00 | |
| @Author : lh9171338 | |
| @Version : 1.0 | |
| @Contact : 2909171338@qq.com | |
| """ | |
| import numpy as np | |
| import cv2 | |
| class Camera: | |
| """ | |
| Base Camera | |
| Args: | |
| coeff (dict | None): camera coefficients | |
| **kwargs: keyword arguments | |
| """ | |
| def __init__(self, coeff=None, **kwargs): | |
| self.coeff = coeff | |
| self.format_coeff() | |
| def format_coeff(self): | |
| """ | |
| Format coeff | |
| Args: | |
| None | |
| Returns: | |
| None | |
| """ | |
| if self.coeff: | |
| self.coeff = {k: np.array(v) for k, v in self.coeff.items()} | |
| def load_coeff(self, filename): | |
| """ | |
| Load coeff | |
| Args: | |
| filename (str): filename | |
| Returns: | |
| None | |
| """ | |
| fs = cv2.FileStorage(filename, cv2.FileStorage_READ) | |
| K = fs.getNode("K").mat() | |
| D = fs.getNode("D").mat() | |
| fs.release() | |
| self.coeff = {"K": K, "D": D} | |
| def save_coeff(self, filename): | |
| """ | |
| Save coeff | |
| Args: | |
| filename (str): filename | |
| Returns: | |
| None | |
| """ | |
| fs = cv2.FileStorage(filename, cv2.FileStorage_WRITE) | |
| fs.write("K", self.coeff["K"]) | |
| fs.write("D", self.coeff["D"]) | |
| fs.release() | |
| def distort_point(self, undistorted): | |
| """ | |
| Distort point | |
| Args: | |
| undistorted (np.ndarray): undistorted points, shape [N, 2] | |
| Returns: | |
| distorted (np.ndarray): distorted points, shape [N, 2] | |
| """ | |
| raise NotImplementedError | |
| def undistort_point(self, distorted): | |
| """ | |
| Undistort point | |
| Args: | |
| distorted (np.ndarray): distorted points, shape [N, 2] | |
| Returns: | |
| undistorted (np.ndarray): undistorted points, shape [N, 2] | |
| """ | |
| raise NotImplementedError | |
| def distort_image(self, image, transform=None): | |
| """ | |
| Distort image | |
| Args: | |
| image (np.ndarray): image | |
| transform (list): transform, [tx, ty, sx, sy] | |
| Returns: | |
| image (np.ndarray): distorted image | |
| """ | |
| if transform is None: | |
| transform = [0.0, 0.0, 1.0, 1.0] | |
| tx, ty, sx, sy = transform[0], transform[1], transform[2], transform[3] | |
| height, width = image.shape[0], image.shape[1] | |
| distorted = np.mgrid[0:width, 0:height].T.reshape(-1, 2).astype(np.float64) | |
| undistorted = self.undistort_point(distorted) | |
| undistorted = undistorted.reshape(height, width, 2) | |
| map1 = (undistorted[:, :, 0].astype(np.float32) - tx) / sx | |
| map2 = (undistorted[:, :, 1].astype(np.float32) - ty) / sy | |
| image = cv2.remap(image, map1, map2, cv2.INTER_CUBIC) | |
| return image | |
| def undistort_image(self, image, transform=None): | |
| """ | |
| Undistort image | |
| Args: | |
| image (np.ndarray): image | |
| transform (list): transform, [tx, ty, sx, sy] | |
| Returns: | |
| image (np.ndarray): undistorted image | |
| """ | |
| if transform is None: | |
| transform = [0.0, 0.0, 1.0, 1.0] | |
| tx, ty, sx, sy = transform[0], transform[1], transform[2], transform[3] | |
| height, width = image.shape[0], image.shape[1] | |
| undistorted = np.mgrid[0:width, 0:height].T.reshape(-1, 2).astype(np.float64) | |
| undistorted[:, 0] = undistorted[:, 0] * sx + tx | |
| undistorted[:, 1] = undistorted[:, 1] * sy + ty | |
| distorted = self.distort_point(undistorted) | |
| distorted = distorted.reshape(height, width, 2) | |
| map1 = distorted[:, :, 0].astype(np.float32) | |
| map2 = distorted[:, :, 1].astype(np.float32) | |
| image = cv2.remap(image, map1, map2, cv2.INTER_CUBIC) | |
| return image | |
| def interp_line(self, lines, num=None, resolution=1.0): | |
| """ | |
| Interpolate line | |
| Args: | |
| lines (np.ndarray): lines, shape [N, 2, 2] | |
| num (int | None): number of interpolated points per line | |
| resolution (float): resolution of interpolation | |
| Returns: | |
| pts_list (list): list of interpolated points | |
| """ | |
| raise NotImplementedError | |
| def interp_arc(self, arcs, num=None, resolution=0.01): | |
| """ | |
| Interpolate arc | |
| Args: | |
| arcs (np.ndarray): arcs, shape [N, 2, 2] | |
| num (int | None): number of interpolated points per line | |
| resolution (float): resolution of interpolation | |
| Returns: | |
| pts_list (list): list of interpolated points | |
| """ | |
| resolution *= np.pi / 180.0 | |
| pts_list = [] | |
| for arc in arcs: | |
| pt1, pt2 = arc[0], arc[1] | |
| normal = np.cross(pt1, pt2) | |
| normal /= np.linalg.norm(normal) | |
| angle = np.arccos(normal[2]) | |
| axes = np.array([-normal[1], normal[0], 0]) | |
| axes /= max(np.linalg.norm(axes), np.finfo(np.float64).eps) | |
| rotation_vector = angle * axes | |
| rotation_matrix, _ = cv2.Rodrigues(rotation_vector) | |
| pt1 = np.matmul(rotation_matrix.T, pt1[:, None]).flatten() | |
| pt2 = np.matmul(rotation_matrix.T, pt2[:, None]).flatten() | |
| min_angle = np.arctan2(pt1[1], pt1[0]) | |
| max_angle = np.arctan2(pt2[1], pt2[0]) | |
| if max_angle < min_angle: | |
| max_angle += 2 * np.pi | |
| K = int(round((max_angle - min_angle) / resolution) + 1) if num is None else num | |
| angles = np.linspace(min_angle, max_angle, K) | |
| pts = np.hstack((np.cos(angles)[:, None], np.sin(angles)[:, None], np.zeros((K, 1)))) | |
| pts = np.matmul(rotation_matrix, pts.T).T | |
| pts_list.append(pts) | |
| return pts_list | |
| def insert_line(self, image, pts_list, color, thickness=1): | |
| """ | |
| Insert line | |
| Args: | |
| image (np.ndarray): image | |
| pts_list (list): list of points | |
| color (tuple): color | |
| thickness (int): thickness | |
| Returns: | |
| image (np.ndarray): image | |
| """ | |
| for pts in pts_list: | |
| pts = np.round(pts).astype(np.int32) | |
| cv2.polylines(image, [pts], isClosed=False, color=color, thickness=thickness) | |
| return image | |
| def truncate_line(self, lines): | |
| """ | |
| Truncate line | |
| Args: | |
| lines (np.ndarray): lines, shape [N, 2, 2] | |
| image_size (tuple): image size, [width, height] | |
| Returns: | |
| lines (np.ndarray): truncated lines, shape [M, 2, 2] | |
| """ | |
| return lines | |
| class Pinhole(Camera): | |
| """ | |
| Pinhole camera | |
| """ | |
| def distort_point(self, undistorted): | |
| """ | |
| Distort point | |
| Args: | |
| undistorted (np.ndarray): undistorted points, shape [N, 2] | |
| Returns: | |
| distorted (np.ndarray): distorted points, shape [N, 2] | |
| """ | |
| if self.coeff is not None: | |
| K, D = self.coeff["K"], self.coeff["D"] | |
| fx, fy = K[0, 0], K[1, 1] | |
| cx, cy = K[0, 2], K[1, 2] | |
| undistorted = undistorted.copy().astype(np.float64) | |
| undistorted[:, 0] = (undistorted[:, 0] - cx) / fx | |
| undistorted[:, 1] = (undistorted[:, 1] - cy) / fy | |
| undistorted = np.hstack((undistorted, np.ones((undistorted.shape[0], 1), np.float64))) | |
| distorted = cv2.projectPoints(undistorted.reshape(1, -1, 3), (0, 0, 0), (0, 0, 0), K, D)[0].reshape(-1, 2) | |
| else: | |
| distorted = undistorted | |
| return distorted | |
| def undistort_point(self, distorted): | |
| """ | |
| Undistort point | |
| Args: | |
| distorted (np.ndarray): distorted points, shape [N, 2] | |
| Returns: | |
| undistorted (np.ndarray): undistorted points, shape [N, 2] | |
| """ | |
| if self.coeff is not None: | |
| K, D = self.coeff["K"], self.coeff["D"] | |
| distorted = distorted.copy().astype(np.float64) | |
| undistorted = cv2.undistortPoints(distorted.reshape(1, -1, 2), K, D, R=None, P=K).reshape(-1, 2) | |
| else: | |
| undistorted = distorted | |
| return undistorted | |
| def interp_line(self, lines, num=None, resolution=0.1): | |
| """ | |
| Interpolate line | |
| Args: | |
| lines (np.ndarray): lines, shape [N, 2, 2] | |
| num (int | None): number of interpolated points per line | |
| resolution (float): resolution of interpolation | |
| Returns: | |
| pts_list (list): list of interpolated points | |
| """ | |
| distorted = lines.reshape(-1, 2) | |
| undistorted = self.undistort_point(distorted) | |
| lines = undistorted.reshape(-1, 2, 2) | |
| pts_list = [] | |
| for line in lines: | |
| K = num or int(round(max(abs(line[1] - line[0])) / resolution)) + 1 | |
| lambda_ = np.linspace(0, 1, K)[:, None] | |
| pts = line[1] * lambda_ + line[0] * (1 - lambda_) | |
| pts = self.distort_point(pts) | |
| pts_list.append(pts) | |
| return pts_list | |
| def insert_line(self, image, lines, color, thickness=1): | |
| """ | |
| Insert line | |
| Args: | |
| image (np.ndarray): image | |
| lines (np.ndarray): lines, shape [N, 2, 2] | |
| color (tuple): color | |
| thickness (int): thickness | |
| Returns: | |
| image (np.ndarray): image | |
| """ | |
| pts_list = self.interp_line(lines) | |
| super().insert_line(image, pts_list, color, thickness) | |
| return image | |
| class Fisheye(Camera): | |
| """ | |
| Fisheye camera | |
| """ | |
| def distort_point(self, undistorted): | |
| """ | |
| Distort point | |
| Args: | |
| undistorted (np.ndarray): undistorted points, shape [N, 2] | |
| Returns: | |
| distorted (np.ndarray): distorted points, shape [N, 2] | |
| """ | |
| undistorted = undistorted.copy().astype(np.float64) | |
| K, D = self.coeff["K"], self.coeff["D"] | |
| fx, fy = K[0, 0], K[1, 1] | |
| cx, cy = K[0, 2], K[1, 2] | |
| undistorted[:, 0] = (undistorted[:, 0] - cx) / fx | |
| undistorted[:, 1] = (undistorted[:, 1] - cy) / fy | |
| distorted = cv2.fisheye.distortPoints(undistorted.reshape(1, -1, 2), K, D).reshape(-1, 2) | |
| return distorted | |
| def undistort_point(self, distorted): | |
| """ | |
| Undistort point | |
| Args: | |
| distorted (np.ndarray): distorted points, shape [N, 2] | |
| Returns: | |
| undistorted (np.ndarray): undistorted points, shape [N, 2] | |
| """ | |
| distorted = distorted.copy().astype(np.float64) | |
| K, D = self.coeff["K"], self.coeff["D"] | |
| undistorted = cv2.fisheye.undistortPoints(distorted.reshape(1, -1, 2), K, D, P=K).reshape(-1, 2) | |
| return undistorted | |
| def interp_line(self, lines, num=None, resolution=0.1): | |
| """ | |
| Interpolate line | |
| Args: | |
| lines (np.ndarray): lines, shape [N, 2, 2] | |
| num (int | None): number of interpolated points per line | |
| resolution (float): resolution of interpolation | |
| Returns: | |
| pts_list (list): list of interpolated points | |
| """ | |
| distorted = lines.reshape(-1, 2) | |
| undistorted = self.undistort_point(distorted) | |
| undistorted = np.hstack((undistorted, np.ones((undistorted.shape[0], 1), np.float64))) | |
| undistorted = undistorted / np.linalg.norm(undistorted, axis=1, keepdims=True) | |
| arcs = undistorted.reshape(-1, 2, 3) | |
| undistorted_list = self.interp_arc(arcs, num, resolution) | |
| distorted_list = [] | |
| for undistorted in undistorted_list: | |
| undistorted = undistorted / (undistorted[:, 2:] + np.finfo(np.float64).eps) | |
| undistorted = undistorted[:, :2] | |
| distorted = self.distort_point(undistorted) | |
| distorted_list.append(distorted) | |
| return distorted_list | |
| def insert_line(self, image, lines, color, thickness=1): | |
| """ | |
| Insert line | |
| Args: | |
| image (np.ndarray): image | |
| lines (np.ndarray): lines, shape [N, 2, 2] | |
| color (tuple): color | |
| thickness (int): thickness | |
| Returns: | |
| image (np.ndarray): image | |
| """ | |
| pts_list = self.interp_line(lines) | |
| super().insert_line(image, pts_list, color, thickness) | |
| return image | |
| class Spherical(Camera): | |
| """ | |
| Spherical camera | |
| Args: | |
| image_size (tuple): image size, [width, height] | |
| **kwargs: keyword arguments | |
| """ | |
| def __init__(self, image_size, **kwargs): | |
| super().__init__(**kwargs) | |
| self.image_size = image_size | |
| def distort_point(self, undistorted): | |
| """ | |
| Distort point | |
| Args: | |
| undistorted (np.ndarray): undistorted points, shape [N, 3] | |
| Returns: | |
| distorted (np.ndarray): distorted points, shape [N, 2] | |
| """ | |
| undistorted = undistorted.copy().astype(np.float64) | |
| width, height = self.image_size | |
| if self.coeff is not None: | |
| K, D = self.coeff["K"], self.coeff["D"] | |
| cx = cy = (height - 1.0) / 2.0 | |
| mask = undistorted[:, 2] < 0 | |
| undistorted[mask, 0] = -undistorted[mask, 0] | |
| undistorted[mask, 2] = -undistorted[mask, 2] | |
| undistorted = undistorted / (undistorted[:, 2:] + np.finfo(np.float64).eps) | |
| undistorted = undistorted[:, :2] | |
| distorted = cv2.fisheye.distortPoints(undistorted.reshape(1, -1, 2), K, D).reshape(-1, 2) | |
| x = (distorted[:, 0] - cx) / cx | |
| y = (distorted[:, 1] - cy) / cy | |
| theta = np.arctan2(y, x) | |
| phi = np.sqrt(x**2 + y**2) * np.pi / 2.0 | |
| x = np.sin(phi) * np.cos(theta) | |
| y = np.sin(phi) * np.sin(theta) | |
| z = np.cos(phi) | |
| undistorted = np.hstack((x[:, None], y[:, None], z[:, None])) | |
| undistorted[mask, 0] = -undistorted[mask, 0] | |
| undistorted[mask, 2] = -undistorted[mask, 2] | |
| x, y, z = undistorted[:, 0], undistorted[:, 1], undistorted[:, 2] | |
| lat = np.pi - np.arccos(y) | |
| lon = np.pi - np.arctan2(z, x) | |
| u = width * lon / (2 * np.pi) | |
| v = height * lat / np.pi | |
| u = np.mod(u, width) | |
| v = np.mod(v, height) | |
| distorted = np.stack([u, v], axis=-1) | |
| return distorted | |
| def undistort_point(self, distorted): | |
| """ | |
| Undistort point | |
| Args: | |
| distorted (np.ndarray): distorted points, shape [N, 2] | |
| Returns: | |
| undistorted (np.ndarray): undistorted points, shape [N, 3] | |
| """ | |
| distorted = distorted.copy().astype(np.float64) | |
| width, height = self.image_size | |
| u, v = distorted[:, 0], distorted[:, 1] | |
| lon = np.pi - u / width * 2 * np.pi | |
| lat = np.pi - v / height * np.pi | |
| y = np.cos(lat) | |
| x = np.sin(lat) * np.cos(lon) | |
| z = np.sin(lat) * np.sin(lon) | |
| undistorted = np.stack([x, y, z], axis=-1) | |
| if self.coeff is not None: | |
| K, D = self.coeff["K"], self.coeff["D"] | |
| cx = cy = (height - 1.0) / 2.0 | |
| mask = undistorted[:, 2] < 0 | |
| undistorted[mask, 0] = -undistorted[mask, 0] | |
| undistorted[mask, 2] = -undistorted[mask, 2] | |
| x, y, z = undistorted[:, 0], undistorted[:, 1], undistorted[:, 2] | |
| theta = np.arctan2(y, x) | |
| phi = np.arccos(z) | |
| r = phi * 2.0 / np.pi | |
| x = r * np.cos(theta) * cx + cx | |
| y = r * np.sin(theta) * cy + cy | |
| distorted = np.hstack((x[:, None], y[:, None])) | |
| undistorted = cv2.fisheye.undistortPoints(distorted.reshape(1, -1, 2), K, D).reshape(-1, 2) | |
| undistorted = np.hstack((undistorted, np.ones((undistorted.shape[0], 1), np.float64))) | |
| undistorted = undistorted / np.linalg.norm(undistorted, axis=1, keepdims=True) | |
| undistorted[mask, 0] = -undistorted[mask, 0] | |
| undistorted[mask, 2] = -undistorted[mask, 2] | |
| return undistorted | |
| def interp_line(self, lines, num=None, resolution=0.01): | |
| """ | |
| Interpolate line | |
| Args: | |
| lines (np.ndarray): lines, shape [N, 2, 2] | |
| num (int | None): number of interpolated points per line | |
| resolution (float): resolution of interpolation | |
| Returns: | |
| pts_list (list): list of interpolated points | |
| """ | |
| distorted = lines.reshape(-1, 2) | |
| undistorted = self.undistort_point(distorted) | |
| arcs = undistorted.reshape(-1, 2, 3) | |
| undistorted_list = self.interp_arc(arcs, num, resolution) | |
| distorted_list = [] | |
| for undistorted in undistorted_list: | |
| distorted = self.distort_point(undistorted) | |
| distorted_list.append(distorted) | |
| return distorted_list | |
| def insert_line(self, image, lines, color, thickness=1): | |
| """ | |
| Insert line | |
| Args: | |
| image (np.ndarray): image | |
| lines (np.ndarray): lines, shape [N, 2, 2] | |
| color (tuple): color | |
| thickness (int): thickness | |
| Returns: | |
| image (np.ndarray): image | |
| """ | |
| pts_list = self.interp_line(lines) | |
| super().insert_line(image, pts_list, color, thickness) | |
| return image | |
| def truncate_line(self, lines): | |
| """ | |
| Truncate line | |
| Args: | |
| lines (np.ndarray): lines, shape [N, 2, 2] | |
| image_size (tuple): image size, [width, height] | |
| Returns: | |
| lines (np.ndarray): truncated lines, shape [M, 2, 2] | |
| """ | |
| width = self.image_size[0] | |
| pts_list = self.interp_line(lines) | |
| lines = [] | |
| for pts in pts_list: | |
| dx = abs(pts[:-1, 0] - pts[1:, 0]) | |
| mask = dx > width / 2.0 | |
| s = sum(mask) | |
| assert s <= 1 | |
| if s == 0: | |
| lines.append([pts[0], pts[-1]]) | |
| else: | |
| ind = np.where(mask)[0][0] | |
| lines.append([pts[0], pts[ind]]) | |
| lines.append([pts[ind + 1], pts[-1]]) | |
| lines = np.asarray(lines) | |
| return lines | |