"""
This module provides functionality for adding markers to fisheye images.
"""
from typing import Tuple
import cv2
import numpy as np
import json
[docs]
class MoilFisheyeMarker:
"""
A class for marking points on fisheye images.
"""
@classmethod
def __auto_thickness(cls, image: np.ndarray) -> int:
"""
Automatically determines the thickness of the marker based on the size of the image.
Args:
image (np.ndarray): The input image.
Returns:
int: The determined thickness of the marker.
"""
h, w = image.shape[:2]
if w >= h:
return h // 350
if h > w:
return w // 350
@classmethod
def __auto_point_size(cls, image: np.ndarray) -> int:
"""
Automatically determines the size of the marker based on the size of the image.
Args:
image (np.ndarray): The input image.
Returns:
int: The determined size of the marker.
"""
h, w = image.shape[:2]
if w >= h:
return h // 150
if h > w:
return w // 150
[docs]
@staticmethod
# circle size flexible
def point(image: np.ndarray,
pixel_coordinate: Tuple[int, int],
radius: int = None,
color: tuple = (0, 0, 255),
fill: bool = False) -> np.ndarray:
"""
Marks a point on the image at the specified pixel coordinates.
Args:
image (np.ndarray): Input image.
pixel_coordinate (Tuple[int, int]): Pixel coordinates (x, y) of the point.
radius (int, optional): Radius of the circle. If not provided, it will be automatically determined based on image size.
color (tuple, optional): Color of the point marker in BGR format. Default is (0, 0, 255) for red.
fill (bool, optional): If True, fills the circle. Default is False.
Returns:
np.ndarray: Image with the marked point.
"""
x, y = pixel_coordinate
if not radius:
radius = MoilFisheyeMarker.__auto_point_size(image)
thickness = MoilFisheyeMarker.__auto_thickness(image)
if thickness == 0:
thickness = 2
if fill:
cv2.circle(image, (x, y), radius=radius, color=color, thickness=-1)
else:
cv2.circle(image, (x, y), radius=radius, color=color, thickness=thickness)
return image
[docs]
@staticmethod
# circle position flexible
def crosshair(image: np.ndarray,
pixel_coordinate: Tuple[int, int],
color: Tuple[int, int, int] = (0, 0, 255)) -> np.ndarray:
"""
Draws a crosshair at the specified pixel coordinates on the image.
Args:
image (np.ndarray): Input image.
pixel_coordinate (Tuple[int, int]): Pixel coordinates (x, y) of the center of the crosshair.
color (Tuple[int, int, int], optional): Color of the crosshair in BGR format. Default is (0, 0, 255) for red.
Returns:
np.ndarray: Image with the drawn crosshair.
"""
x, y = pixel_coordinate
size = MoilFisheyeMarker.__auto_point_size(image) * 3
thickness = int(MoilFisheyeMarker.__auto_thickness(image) // 1.5)
if thickness == 0:
thickness = 2
cv2.line(image, (x + size, y), (x - size, y), color, thickness)
cv2.line(image, (x, y + size), (x, y - size), color, thickness)
return image
[docs]
@staticmethod
def cross(image: np.ndarray,
pixel_coordinate: Tuple[int, int],
color: Tuple[int, int, int] = (0, 0, 255)) -> np.ndarray:
"""
Draws a cross at the specified pixel coordinates on the image.
Args:
image (np.ndarray): Input image.
pixel_coordinate (Tuple[int, int]): Pixel coordinates (x, y) of the center of the cross.
color (Tuple[int, int, int], optional): Color of the cross in BGR format. Default is (0, 0, 255) for red.
Returns:
np.ndarray: Image with the drawn cross.
"""
x, y = pixel_coordinate
size = MoilFisheyeMarker.__auto_point_size(image)
thickness = MoilFisheyeMarker.__auto_thickness(image)
if thickness == 0:
thickness = 2
cv2.line(image, (x + size, y + size), (x - size, y - size), color, thickness)
cv2.line(image, (x + size, y - size), (x - size, y + size), color, thickness)
return image
[docs]
@staticmethod
def square(image: np.ndarray,
pixel_coordinate: Tuple[int, int],
color: Tuple[int, int, int] = (0, 0, 255)) -> np.ndarray:
"""
Draws a square around the specified pixel coordinates on the image.
Args:
image (np.ndarray): Input image.
pixel_coordinate (Tuple[int, int]): Pixel coordinates (x, y) of the center of the square.
color (Tuple[int, int, int], optional): Color of the square in BGR format. Default is (0, 0, 255) for red.
Returns:
np.ndarray: Image with the drawn square.
"""
x = pixel_coordinate[0]
y = pixel_coordinate[1]
size = MoilFisheyeMarker.__auto_point_size(image)
thickness = MoilFisheyeMarker.__auto_thickness(image)
if thickness == 0:
thickness = 2
cv2.rectangle(image, (x + size, y + size), (x - size, y - size), color, thickness)
return image
[docs]
@staticmethod
def triangle(image: np.ndarray,
pixel_coordinate: Tuple[int, int],
color: Tuple[int, int, int] = (0, 0, 255)) -> np.ndarray:
"""
Draws a triangle centered at the specified pixel coordinates on the image.
Args:
image (np.ndarray): Input image.
pixel_coordinate (Tuple[int, int]): Pixel coordinates (x, y) of the center of the triangle.
color (Tuple[int, int, int], optional): Color of the triangle in BGR format. Default is (0, 0, 255) for red.
Returns:
np.ndarray: Image with the drawn triangle.
"""
x = pixel_coordinate[0]
y = pixel_coordinate[1]
size = MoilFisheyeMarker.__auto_point_size(image)
thickness = MoilFisheyeMarker.__auto_thickness(image)
if thickness == 0:
thickness = 2
cv2.line(image, (x, y - size), (x - size, y + size), color, thickness)
cv2.line(image, (x, y - size), (x + size, y + size), color, thickness)
cv2.line(image, (x + size, y + size), (x - size, y + size), color, thickness)
return image
[docs]
@staticmethod
def boundary_fov(image: np.ndarray,
moildev,
fov: int = 90,
color: tuple = (255, 255, 0)) -> np.ndarray:
"""
Draws the boundary of the field of view (FOV) on the image for a given Moildev object and FOV angle.
Args:
image (np.ndarray): Input image.
moildev: Moildev object representing the camera model.
fov (int, optional): Field of view angle in degrees. Default is 90 degrees.
color (tuple, optional): Color of the boundary in BGR format. Default is (255, 255, 0) for light blue.
Returns:
np.ndarray: Image with the drawn FOV boundary circle.
"""
icx = moildev.icx
icy = moildev.icy
center = (icx, icy)
boundary_radius = int(moildev.get_rho_from_alpha(fov))
thickness = int(MoilFisheyeMarker.__auto_thickness(image) // 1.5)
if thickness == 0:
thickness = 2
image = cv2.circle(image, center, radius=boundary_radius, color=color, thickness=thickness)
return image
[docs]
@staticmethod
def line_horizontal_vertical(image: np.ndarray,
pixel_coordinate: Tuple[int, int],
color: Tuple[int, int, int] = (0, 0, 0),
translucent: float = 0.5):
"""
Draws horizontal and vertical lines passing through the specified pixel coordinates on the image.
Args:
image (np.ndarray): Input image.
pixel_coordinate (Tuple[int, int]): Pixel coordinates (x, y) through which the lines pass.
color (Tuple[int, int, int], optional): Color of the lines in BGR format. Default is (0, 0, 0) for black.
translucent (float, optional): Transparency level of the lines. Should be between 0 and 1.
Default is 0.5, where 0 means fully transparent and 1 means fully opaque.
Returns:
np.ndarray: Image with the drawn horizontal and vertical lines.
"""
overlay = image.copy()
translucent = 1 - translucent
x, y = pixel_coordinate
y_limit, x_limit = image.shape[:2]
thickness = MoilFisheyeMarker.__auto_thickness(image)
if thickness == 0:
thickness = 2
image = cv2.line(image, (0, y), (x_limit, y), color, thickness)
image = cv2.line(image, (x, 0), (x, y_limit), color, thickness)
result = cv2.addWeighted(overlay, translucent, image, 1 - translucent, 0)
return result
[docs]
@staticmethod
def line_p2p_distorted(image: np.ndarray,
moildev,
parameter_file: str,
start_img_point: tuple,
end_img_point: tuple,
color: Tuple[int, int, int] = (0, 0, 255)) -> np.ndarray:
"""
Draws a distorted line between two points on the image based on the camera model parameters.
Args:
image (np.ndarray): Input image.
moildev: Moildev object representing the camera model.
parameter_file (str): Path to the JSON file containing camera model parameters.
start_img_point (tuple): Pixel coordinates (x, y) of the starting point of the line.
end_img_point (tuple): Pixel coordinates (x, y) of the ending point of the line.
color (Tuple[int, int, int], optional): Color of the line in BGR format. Default is (0, 0, 255) for red.
Returns:
np.ndarray: Image with the drawn distorted line between the two points.
"""
f = open(parameter_file)
parameter = json.load(f)
f.close()
# Get the spherical coordinates of p1 and p2 vectors
p1_theta = np.radians(moildev.get_alpha_beta(start_img_point[0], start_img_point[1])[0])
p2_theta = np.radians(moildev.get_alpha_beta(end_img_point[0], end_img_point[1])[0])
p1_phi = np.arctan2((-1) * start_img_point[1] + moildev.icy, start_img_point[0] - moildev.icx)
p2_phi = np.arctan2((-1) * end_img_point[1] + moildev.icy, end_img_point[0] - moildev.icx)
# Transform spherical coordinates into cartesian coordinates
p1_v = MoilFisheyeMarker.__spherical2cartesian(p1_theta, p1_phi)
p2_v = MoilFisheyeMarker.__spherical2cartesian(p2_theta, p2_phi)
# Form a line from p1 to p2 in 3D space
line_ps = np.linspace(0, 1, moildev.image_height + 1)
line = p1_v + (p2_v - p1_v) * line_ps[:, None]
# Convert cartesian coordinates into spherical coordinates
line = MoilFisheyeMarker.__cartesian2spherical(line)
# Convert the line in 3D space to img coordinate
line = MoilFisheyeMarker.__space2img(line, moildev, parameter)
# Round coordinate numbers to integers
line = (np.rint(line)).astype(int)
# Draw the line on the image
radius = MoilFisheyeMarker.__auto_point_size(image) // 5
for c in line:
image = cv2.circle(image, (c[0], c[1]), radius=radius, color=color, thickness=-1)
return image
@staticmethod
def __spherical2cartesian(theta: float, phi: float) -> np.ndarray:
# convert a spherical coordinate (polar theta, azimuth phi) in radian of a vector to the cartesian one (x, y, z)
"""
Converts spherical coordinates (polar theta, azimuth phi) in radians to cartesian coordinates (x, y, z).
Args:
theta (float): Polar angle theta in radians.
phi (float): Azimuth angle phi in radians.
Returns:
np.ndarray: Cartesian coordinates (x, y, z) as a NumPy array.
"""
return np.array([np.sin(theta) * np.cos(phi),
np.sin(theta) * np.sin(phi),
np.cos(theta)])
@staticmethod
def __cartesian2spherical(points: np.ndarray) -> np.ndarray:
# convert cartesian coordinates (x, y, z) of vectors to spherical ones (theta, phi) in radian.
"""
Converts cartesian coordinates (x, y, z) of vectors to spherical coordinates (theta, phi) in radians.
Args:
points (np.ndarray): Cartesian coordinates of vectors as a NumPy array of shape (N, 3).
Returns:
np.ndarray: Spherical coordinates (theta, phi) as a NumPy array of shape (N, 2).
"""
points /= np.linalg.norm(points, axis=1)[:, None]
sph = np.zeros((points.shape[0], 2))
# compute theta
sph[:, 0] = np.arccos(points[:, 2])
# compute phi
sph[:, 1] = np.arctan2(points[:, 1], points[:, 0])
return sph
@staticmethod
def __space2img(points: np.ndarray,
moildev,
parameter: dict) -> np.ndarray:
"""
Converts spherical coordinates of vectors in 3D space to image coordinates.
Args:
points (np.ndarray): Spherical coordinates of vectors in 3D space as a NumPy array of shape (N, 2).
moildev: Moildev object representing the camera model.
parameter (dict): Dictionary containing camera model parameters.
Returns:
np.ndarray: Image coordinates of the vectors as a NumPy array of shape (N, 2).
"""
# Convert spherical coordinates of vectors in 3D space to image coordinates
theta = points[:, 0]
# get the distance of each point on the image to image center
param_2 = moildev.param_2
param_3 = moildev.param_3
param_4 = moildev.param_4
param_5 = moildev.param_5
rho = (param_2 * theta ** 4 +
param_3 * theta ** 3 +
param_4 * theta ** 2 +
param_5 * theta) * parameter['calibrationRatio']
# compute x coordinates
points[:, 0] = rho * np.cos(points[:, 1])
# compute y coordinates
points[:, 1] = rho * np.sin(points[:, 1])
# adjust the origin of the coordinate system
points = points + np.array([moildev.icx, -moildev.icy])
# adjust y coordinates for the image coordinate system
points[:, 1] = - points[:, 1]
return points