Source code for improutils.other

import os

import cv2
import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np
from IPython.display import display


[docs] def midpoint(ptA, ptB): """Return the midpoint between two points. Parameters ---------- ptA : array | tuple | ndarray The first 2D point considered ptB : array | tuple | ndarray The second 2D point considered Returns ------- The 2D midpoint """ return ((ptA[0] + ptB[0]) * 0.5, (ptA[1] + ptB[1]) * 0.5)
[docs] def artificial_circle_image(size): """Create an image of given size filled with circles. Parameters ---------- size : int size of the image Returns ------- Artificial image with circles """ img_art_circ = np.zeros((int(size), int(size)), dtype=np.uint8) step = 10 for i in range(step, int(size), step): cv2.circle( img_art_circ, (int(size / 2.0), int(size / 2.0)), i - step, np.random.randint(0, 255), thickness=4, ) return img_art_circ
[docs] def order_points(pts): """Sort the points based on their coordinates, in top-left, top-right, bottom-right, and bottom-left order. Parameters ---------- pts : ndarray 4 2D Points to be sorted. Returns ------- Sorted points, the coordinates in top-left, top-right, bottom-right, and bottom-left order """ if not isinstance(pts, np.ndarray): raise ValueError( "Invalid input point format. Numpy ndarray expected. Got {}".format( type(pts) ) ) if len(pts) < 4: raise ValueError( "Invalid amount of input points. Got {} elements".format(len(pts)) ) xSorted = pts[np.argsort(pts[:, 0]), :] # grab the left-most and right-most points from the sorted # x-roodinate points leftMost = xSorted[:2, :] rightMost = xSorted[2:, :] # now, sort the left-most coordinates according to their # y-coordinates so we can grab the top-left and bottom-left # points, respectively leftMost = leftMost[np.argsort(leftMost[:, 1]), :] (bl, tl) = leftMost # now that we have the top-left coordinate, use it as an # anchor to calculate the Euclidean distance between the # top-left and right-most points; by the Pythagorean # theorem, the point with the largest distance will be # our bottom-right point rightMost = rightMost[np.argsort(rightMost[:, 1]), :] (br, tr) = rightMost # return the coordinates in top-left, top-right, # bottom-right, and bottom-left order return np.array([tl, tr, br, bl], dtype="float32")
[docs] def pcd_to_depth(pcd, height, width): """Reduce point-cloud to coordinates, point cloud [x, y, z, rgb] -> depth[x, y, z]. Parameters ---------- pcd : array point cloud height : int height of captured img width : int width of a captured img Returns ------- Array of coordinates. """ data = pcd data = [float(x.split(" ")[2]) for x in data] data = np.reshape(data, (height, width)) return data
[docs] def create_file_path(folder, file_name): """Create path for filename inside a folder. Parameters ---------- folder : string Base folder directory in string notation. If the directory does not exist, it is created. file_name : string File name that should be inside the base folder. Returns ------- Path to the file. """ if not os.path.isdir(folder): os.mkdir(folder) return os.path.join(folder, file_name)
[docs] def create_slider(min, max, description): """Create an integer range slider widget. Parameters ---------- min : int The minimum value for the range slider. max : int The maximum value for the range slider. description : str The label to display alongside the slider. Returns ------- ipywidgets.IntRangeSlider An IntRangeSlider widget with the specified bounds and label. """ description = description.ljust(30, "\xa0") return widgets.IntRangeSlider( min=min, max=max, step=1, value=[min, max], description=description, continuous_update=False, orientation="horizontal", style=dict(description_width="initial"), layout=widgets.Layout(width="auto"), )
[docs] def multicolor_segmentation(func, colors): """Interactive HSV thresholding for multiple colors with saving and returning thresholds that are picked by the user. Parameters ---------- func : function function with arguments hue = h_range (int, range: 0-360), saturation = s_range (int, range: 0-255), value = v_range (int, range: 0-255) colors : list list of colors that the user can choose from, e.g. ['red', 'green', 'blue'], these colors will be used as keys in the output dictionary Returns ------- color_thresholds: dict Returns a dictionary with the chosen thresholds for each color, e.g. {'red': (0, 0, 0), 'green': (0, 0, 0), 'blue': (0, 0, 0)}, can be also empty if no thresholds were saved """ color_thresholds = {} # initialize sliders, buttons etc. h_slider = create_slider(min=0, max=360, description="Hue:") s_slider = create_slider(min=0, max=255, description="Saturation:") v_slider = create_slider(min=0, max=255, description="Value:") color_dropdown = widgets.Dropdown( options=colors, description="Color:".ljust(30, "\xa0"), style={"description_width": "initial"}, layout={"width": "max-content"}, ) save_button = widgets.Button( description="Save threshold for color", layout=widgets.Layout(width="auto"), button_style="success", ) finish_button = widgets.Button( description="Return saved thresholds", layout=widgets.Layout(width="auto"), button_style="danger", ) text_output = widgets.Output() interactive_output = widgets.interactive_output( func, {"h_range": h_slider, "s_range": s_slider, "v_range": v_slider} ) # widget layout input_box = widgets.VBox([h_slider, s_slider, v_slider, color_dropdown]) button_box = widgets.HBox([save_button, finish_button]) other_box = widgets.VBox([text_output, interactive_output]) def reset_sliders(): h_slider.value = (0, 360) s_slider.value = (0, 255) v_slider.value = (0, 255) # button callbacks def on_save_clicked(b): with text_output: text_output.clear_output() color_thresholds[color_dropdown.value] = ( h_slider.value, s_slider.value, v_slider.value, ) print( f"Saved for color '{color_dropdown.value}', threshold: {color_thresholds[color_dropdown.value]}\nResetting sliders...\nChanging to next color..." ) reset_sliders() # set next color in dropdown color_dropdown.value = colors[ (colors.index(color_dropdown.value) + 1) % len(colors) ] def on_finish_clicked(b): with text_output: text_output.clear_output() print("Returned saved thresholds!") reset_sliders() save_button.on_click(on_save_clicked) finish_button.on_click(on_finish_clicked) # display widget display(input_box, button_box, other_box) return color_thresholds
def _plot_grid(xv, yv, squares, ax): for i in np.linspace(0, xv.shape[1] - 1, squares + 1, dtype=int): ax.plot(xv[i, :], yv[i, :], "k-") for j in np.linspace(0, xv.shape[0] - 1, squares + 1, dtype=int): ax.plot(xv[:, j], yv[:, j], "k-") ax.axis("off") def _radial_distortion(xv, yv, k): xv_radial = np.zeros_like(xv) yv_radial = np.zeros_like(yv) for i in range(xv.shape[0]): for j in range(xv.shape[1]): r = np.sqrt(xv[i, j] ** 2 + yv[i, j] ** 2) radial = (1 + (k[0] * (r**2) + k[1] * (r**4) + k[2] * (r**6))) / ( 1 + (k[3] * (r**2) + k[4] * (r**4) + k[5] * (r**6)) ) xv_radial[i, j] = xv[i, j] * radial yv_radial[i, j] = yv[i, j] * radial return xv_radial, yv_radial def _tangetial_distortion(xv, yv, p): xv_tang = np.zeros_like(xv) yv_tang = np.zeros_like(yv) for i in range(xv.shape[0]): for j in range(xv.shape[1]): x = xv[i, j] y = yv[i, j] r = np.sqrt(x**2 + y**2) x_tang = x + (2 * p[0] * x * y + p[1] * (r**2 + 2 * x**2)) y_tang = y + (p[0] * (r**2 + 2 * y**2) + 2 * p[1] * x * y) xv_tang[i, j] = x_tang yv_tang[i, j] = y_tang return xv_tang, yv_tang
[docs] def plot_distortion( k1: float, k2: float, k3: float, k4: float, k5: float, k6: float, p1: float, p2: float, ) -> None: """Vizualization of distortion parameters and their influence. Plots radial, tangential and compounded (radial + tangential) distortion grid using the Brown-Conrady (OpenCV) model. Parameters ---------- k1 : float Radial distortion coefficient. k2 : float Radial distortion coefficient. k3 : float Radial distortion coefficient. k4 : float Radial distortion coefficient. k5 : float Radial distortion coefficient. k6 : float Radial distortion coefficient. p1 : float Tangential distortion coefficient. p2 : float Tangential distortion coefficient. Returns ------- None """ k = (k1, k2, k3, k4, k5, k6) p = (p1, p2) squares = 10 # amount of squares in the grid pts = 100 # realistical values for image with 2500 x 2500 pixels with focal length of 35mm which is close to 10500 pixels with basler camera pixel size, origin is in the center - therfore x and y should be within values +-(2500/10500)/2 width = 0.23 height = 0.23 xv, yv = np.meshgrid( np.linspace(-width / 2, width / 2, pts), np.linspace(-height / 2, height / 2, pts), ) xv_radial, yv_radial = _radial_distortion(xv, yv, k) xv_tang, yv_tang = _tangetial_distortion(xv, yv, p) _, axs = plt.subplots(1, 3, figsize=(15, 5)) _plot_grid(xv_radial, yv_radial, squares, axs[0]) axs[0].set_title("Radial distortion grid") _plot_grid(xv_tang, yv_tang, squares, axs[1]) axs[1].set_title("Tangential distortion grid") _plot_grid(xv_radial + xv_tang, yv_radial + yv_tang, squares, axs[2]) axs[2].set_title("Compounded distortion grid") plt.show()