Source code for discorpy.util.utility

# ============================================================================
# Copyright (c) 2023 Diamond Light Source Ltd. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
# Author: Nghia T. Vo
# E-mail:
# Description: Module for utility methods
# Publication date: 10 July 2023
# ============================================================================
# Contributors:
# ============================================================================


"""
Module of utility methods:

-   Generate a dot-pattern, line-pattern, and chessboard image.
-   Find corresponding points between the distorted and undistorted space.
-   Unwarp a color image with an option to keep the original field of view.
-   Unwarping an image or video using the 'remap' function in Opencv for fast
    performance.
"""

import numpy as np
import scipy.ndimage as ndi
from scipy.ndimage import map_coordinates
import discorpy.proc.processing as proc


[docs] def make_circle_mask(width, ratio): """ Create a circle mask. Parameters ----------- width : int Width of a square array. ratio : float Ratio between the diameter of the mask and the width of the array. Returns ------ array_like Square array. """ mask = np.zeros((width, width), dtype=np.float32) center = width // 2 radius = ratio * center y, x = np.ogrid[-center:width - center, -center:width - center] mask_check = x * x + y * y <= radius * radius mask[mask_check] = 1.0 return mask
[docs] def make_dot_pattern(height=1800, width=2000, dot_distance=90, dot_size=15, margin=150): """ Generate a dot-pattern image. Parameters ---------- height : int Height of the image. width : int Width of the image. dot_distance : int Distance between two dots. dot_size : int Size of each dot. margin : int Blank area between the dots and the edges. Returns ------- array_like Dot-pattern image. """ dot_size = np.clip(dot_size, 1, min(height, width) // 8) mat = np.zeros((height, width), dtype=np.float32) if isinstance(margin, tuple) or isinstance(margin, list): marg_ver, marg_hor = margin[0:2] else: marg_ver = marg_hor = margin half_dot = dot_size // 2 + 1 mask = make_circle_mask(dot_size, 1.0) mat[marg_ver + half_dot: height - marg_ver - half_dot:dot_distance, marg_hor + half_dot: width - marg_hor - half_dot:dot_distance] = 1 dot_pattern = np.float32(ndi.binary_dilation(mat, iterations=1, structure=mask)) return 1 - dot_pattern
[docs] def make_line_pattern(height=1800, width=2000, line_distance=90, line_size=7, margin=100): """ Generate a dot-pattern image. Parameters ---------- height : int Height of the image. width : int Width of the image. line_distance : int Distance between two dots. line_size : int Size of each dot. margin : int Blank area between the lines and the edges. Returns ------- array_like Dot-pattern image. """ line_size = np.clip(line_size, 1, min(height, width) // 8) mat = np.zeros((height, width), dtype=np.float32) if isinstance(margin, tuple) or isinstance(margin, list): marg_ver, marg_hor = margin[0:2] else: marg_ver = marg_hor = margin list_i = np.arange(marg_ver, height - marg_ver - line_size, line_distance) list_j = np.arange(marg_hor, width - marg_hor - line_size, line_distance) for i in list_i: mat[i:i + line_size, list_j[0]:list_j[-1] + line_size] = 1 for j in list_j: mat[list_i[0]:list_i[-1] + line_size, j:j + line_size] = 1 return 1 - mat
[docs] def make_chessboard(height=1800, width=2000, size=100, margin=100, margin_grayscale=0.95): """ Generate a chessboard image. Parameters ---------- height : int Height of the image. width : int Width of the image. size : int Size of each cell. margin : int Blank area between the chessboard and the edges. margin_grayscale : float Gray scale of margin area (0: black 1: white). Returns ------- array_like Chessboard image. """ mat = margin_grayscale * np.ones((height, width), dtype=np.float32) num = 0 for i in range(size + margin, height - margin - size, size): if num % 2 == 0: num1 = 0 for j in range(size + margin, width - margin - size, size): if num1 % 2 == 0: mat[i: i + size, j: j + size] = 1.0 else: mat[i: i + size, j: j + size] = 0.0 num1 = num1 + 1 else: num1 = 0 for j in range(size + margin, width - margin - size, size): if num1 % 2 == 0: mat[i: i + size, j: j + size] = 0.0 else: mat[i: i + size, j: j + size] = 1.0 num1 = num1 + 1 num = num + 1 return mat
[docs] def find_point_to_point(points, xcenter, ycenter, list_fact, output_order="xy"): """ Calculate corresponding points between distorted and undistorted space. This function can be used both ways: - Given the input is a distorted point and a forward model, the output is the undistorted point. - Given the input is an undistorted point and a backward model, the output is the distorted point. Parameters ---------- points : tuple of point indexes. Tuple of (row_index, column_index) of the point. Note that the format is ij-index not xy-coordinate. xcenter : float Center of distortion in x-direction. ycenter : float Center of distortion in y-direction. list_fact : list of float Polynomial coefficients of the backward/forward model. output_order : {"xy", "yx"} To select the order of the output. "yx" <-> "ij". Returns ------- tuple of float x- and y-coordinate of the point in another space. """ xi, yi = points[1] - xcenter, points[0] - ycenter ri = np.sqrt(xi * xi + yi * yi) factor = np.float64(np.sum( list_fact * np.power(ri, np.arange(len(list_fact))))) xo = xcenter + factor * xi yo = ycenter + factor * yi if output_order == "xy": return xo, yo else: return yo, xo
def _calc_pad(pad, height, width, xcenter, ycenter, list_fact): """ Supplementary method for calculating pad-width. """ t_pad, b_pad, l_pad, r_pad = 0, 0, 0, 0 if isinstance(pad, bool): if pad is True: ref_points = [[i - ycenter, j - xcenter] for i in np.linspace(0, height, 40) for j in np.linspace(0, width, 40)] list_tfact = proc.transform_coef_backward_and_forward( list_fact, ref_points=ref_points) xu_tl, yu_tl = find_point_to_point((0, 0), xcenter, ycenter, list_tfact) xu_tr, yu_tr = find_point_to_point((0, width - 1), xcenter, ycenter, list_tfact) xu_br, yu_br = find_point_to_point((height - 1, width - 1), xcenter, ycenter, list_tfact) xu_bl, yu_bl = find_point_to_point((height - 1, 0), xcenter, ycenter, list_tfact) l_val = min(xu_tl, xu_bl) if l_val < 0: l_pad = int(-l_val) r_val = max(xu_tr, xu_br) if r_val > width: r_pad = int(r_val - width) t_val = min(yu_tl, yu_tr) if t_val < 0: t_pad = int(-t_val) b_val = max(yu_bl, yu_br) if b_val > height: b_pad = int(b_val - height) elif isinstance(pad, int): t_pad = b_pad = l_pad = r_pad = pad elif isinstance(pad, tuple) or isinstance(pad, list): if len(pad) != 4: raise ValueError("Incorrect format!!! Please use a tuple/list of " "(top_pad, bottom_pad, left_pad, right_pad)") else: t_pad, b_pad, l_pad, r_pad = pad else: raise ValueError("Invalid format of the 'pad' parameter!!!") return t_pad, b_pad, l_pad, r_pad
[docs] def unwarp_color_image_backward(mat, xcenter, ycenter, list_fact, order=1, mode="reflect", pad=False, pad_mode='constant'): """ Unwarp a color image using a backward model. Parameters ---------- mat : array_like 2D/3D array. xcenter : float Center of distortion in x-direction. ycenter : float Center of distortion in y-direction. list_fact : list of float Polynomial coefficients of the backward model. order : int, optional. The order of the spline interpolation. mode : {'reflect', 'grid-mirror', 'constant', 'grid-constant', 'nearest', 'mirror', 'grid-wrap', 'wrap'}, optional To determine how to handle image boundaries. pad : bool, int, or tuple of int. Use to keep the original view. If pad is True, the pad width is calculated automatically. pad_mode : str To select a method for padding: 'reflect', 'edge', 'mean', 'linear_ramp', 'symmetric',... Returns ------- array_like 2D/3D array. Distortion-corrected image. """ (height, width) = mat.shape[:2] t_pad, b_pad, l_pad, r_pad = _calc_pad(pad, height, width, xcenter, ycenter, list_fact) num_dim = len(mat.shape) if num_dim == 2: pad_width = [(t_pad, b_pad), (l_pad, r_pad)] else: pad_width = [(t_pad, b_pad), (l_pad, r_pad), (0, 0)] mat_pad = np.pad(mat, pad_width, mode=pad_mode) (height, width) = mat_pad.shape[:2] xcenter = xcenter + l_pad ycenter = ycenter + t_pad xu_list = np.arange(width) - xcenter yu_list = np.arange(height) - ycenter xu_mat, yu_mat = np.meshgrid(xu_list, yu_list) ru_mat = np.sqrt(xu_mat ** 2 + yu_mat ** 2) fact_mat = np.sum(np.asarray( [factor * ru_mat ** i for i, factor in enumerate(list_fact)]), axis=0) xd_mat = np.float32(np.clip(xcenter + fact_mat * xu_mat, 0, width - 1)) yd_mat = np.float32(np.clip(ycenter + fact_mat * yu_mat, 0, height - 1)) indices = np.reshape(yd_mat, (-1, 1)), np.reshape(xd_mat, (-1, 1)) if num_dim == 2: mat_corr = np.reshape(map_coordinates( mat_pad, indices, order=order, mode=mode), (height, width)) else: mat_corr = [] for i in range(mat_pad.shape[-1]): mat_corr.append(np.reshape(map_coordinates( mat_pad[:, :, i], indices, order=order, mode=mode), (height, width))) mat_corr = np.moveaxis(np.asarray(mat_corr), 0, 2) return mat_corr
[docs] def mapping_cv2(mat, xmat, ymat, method=None, border=None): # pragma: no cover """ Apply a geometric transformation to a 2D array using Opencv. Parameters ---------- mat : array_like. 2D/3D array. xmat : array_like 2D array of the x-coordinates. Origin is at the left of the image. ymat : array_like 2D array of the y-coordinates. Origin is at the top of the image. method : opencv-object To select interpolation method. Note to use the prefix: cv2.<method> https://tinyurl.com/3afmv6jc border : opencv-object To select method for boundary handling. Note to use the prefix: cv2.<method> https://tinyurl.com/52xzkwt2 Returns ------- array_like """ try: import cv2 except ImportError: raise ValueError("You must install Opencv before using this function!") if method is None: method = cv2.INTER_LINEAR if border is None: border = cv2.BORDER_CONSTANT mat = cv2.remap(mat, xmat, ymat, interpolation=method, borderMode=border) return mat
[docs] def unwarp_image_backward_cv2(mat, xcenter, ycenter, list_fact, method=None, border=None, pad=False, pad_mode='constant'): # pragma: no cover """ Unwarp an image using the backward model with the Opencv 'remap' function for fast performance. Parameters ---------- mat : array_like Input image. Can be a color image. xcenter : float Center of distortion in x-direction. ycenter : float Center of distortion in y-direction. list_fact : list of float Polynomial coefficients of the backward model. method : opencv-object To select interpolation method. Note to use the prefix: cv2.<method> https://tinyurl.com/3afmv6jc border : opencv-object To select method for boundary handling. Note to use the prefix: cv2.<method> https://tinyurl.com/52xzkwt2 pad : bool, int, or tuple of int. Use to keep the original view. If pad is True, the pad width is calculated automatically. pad_mode : str To select a method for padding: 'reflect', 'edge', 'mean', 'linear_ramp', 'symmetric',... Returns ------- array_like Distortion-corrected image. """ (height, width) = mat.shape[:2] t_pad, b_pad, l_pad, r_pad = _calc_pad(pad, height, width, xcenter, ycenter, list_fact) num_dim = len(mat.shape) if num_dim == 2: pad_width = [(t_pad, b_pad), (l_pad, r_pad)] else: pad_width = [(t_pad, b_pad), (l_pad, r_pad), (0, 0)] mat_pad = np.pad(mat, pad_width, mode=pad_mode) (height, width) = mat_pad.shape[:2] xu_list = np.arange(width) - (xcenter + l_pad) yu_list = np.arange(height) - (ycenter + t_pad) xu_mat, yu_mat = np.meshgrid(xu_list, yu_list) ru_mat = np.sqrt(xu_mat ** 2 + yu_mat ** 2) fact_mat = np.sum(np.asarray( [factor * ru_mat ** i for i, factor in enumerate(list_fact)]), axis=0) xd_mat = np.float32( np.clip(xcenter + l_pad + fact_mat * xu_mat, 0, width - 1)) yd_mat = np.float32( np.clip(ycenter + t_pad + fact_mat * yu_mat, 0, height - 1)) mat = mapping_cv2(mat_pad, xd_mat, yd_mat, method=method, border=border) return mat
[docs] def unwarp_video_cv2(cam_obj, xcenter, ycenter, list_fact, method=None, border=None, pad=True, pad_mode='constant'): # pragma: no cover """ Unwarp frames from Opencv video object using the backward model. Parameters ---------- cam_obj : obj Opencv camera object. e.g. cv2.VideoCapture(0) xcenter : float Center of distortion in x-direction. ycenter : float Center of distortion in y-direction. list_fact : list of float Polynomial coefficients of the backward model. method : opencv-object To select interpolation method. Note to use the prefix: cv2.<method> https://tinyurl.com/3afmv6jc border : opencv-object To select method for boundary handling. Note to use the prefix: cv2.<method> https://tinyurl.com/52xzkwt2 pad : bool, int, or tuple of int. Use to keep the original view. If pad is True, the pad width is calculated automatically. pad_mode : str To select a method for padding: 'reflect', 'edge', 'mean', 'linear_ramp', 'symmetric',... Returns ------- Generator """ try: import cv2 except ImportError: raise ValueError("You must install Opencv before using this function!") width = int(cam_obj.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cam_obj.get(cv2.CAP_PROP_FRAME_HEIGHT)) t_pad, b_pad, l_pad, r_pad = _calc_pad(pad, height, width, xcenter, ycenter, list_fact) width = width + l_pad + r_pad height = height + t_pad + b_pad xcenter = xcenter + l_pad ycenter = ycenter + t_pad xu_list = np.arange(width) - xcenter yu_list = np.arange(height) - ycenter xu_mat, yu_mat = np.meshgrid(xu_list, yu_list) ru_mat = np.sqrt(xu_mat ** 2 + yu_mat ** 2) fact_mat = np.sum(np.asarray( [factor * ru_mat ** i for i, factor in enumerate(list_fact)]), axis=0) xd_mat = np.float32(np.clip(xcenter + fact_mat * xu_mat, 0, width - 1)) yd_mat = np.float32(np.clip(ycenter + fact_mat * yu_mat, 0, height - 1)) check, frame = cam_obj.read() if check: num_dim = len(frame.shape) if num_dim == 2: pad_width = [(t_pad, b_pad), (l_pad, r_pad)] else: pad_width = [(t_pad, b_pad), (l_pad, r_pad), (0, 0)] while True: check, frame = cam_obj.read() if check: if (pad is False) or (pad == 0): uframe = mapping_cv2(frame, xd_mat, yd_mat, method=method, border=border) else: frame_pad = np.pad(frame, pad_width, mode=pad_mode) uframe = mapping_cv2(frame_pad, xd_mat, yd_mat, method=method, border=border) cv2.imshow("Press ESC to exit", uframe) c = cv2.waitKey(1) if c == 27: break cam_obj.release() cv2.destroyAllWindows()