LineViewer / utils /camera.py
lihao57
add utils and support for visualizing Bezier curve
e26dfd8
# -*- 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