import os
import io
import cv2
import numpy as np
import matplotlib.pyplot as plt
from improutils import *
%matplotlib inline
np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})
def rotate_image(image, angle, image_center=None):
""" Rotates the input image by specified angle.
Parameters
----------
image : np.ndarray
Image to be rotated.
angle : float
Rotation angle.
image_center : Optional[tuple(int, int)]
Center of rotation.
Returns
-------
np.ndarray
Returns the rotated input image by specified angle.
"""
if image_center is None:
image_center = tuple(np.array(image.shape[1::-1]) / 2)
rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0)
result = cv2.warpAffine(image, rot_mat, image.shape[1::-1], flags=cv2.INTER_LINEAR)
return result
def draw_rotated_text(img, text, point, angle, text_scale, text_color, text_thickness):
img_filled = np.full(img.shape, text_color, dtype=np.uint8)
# create rotated text mask
text_mask = np.zeros((img.shape[0], img.shape[1]), dtype=np.uint8)
cv2.putText(text_mask, "{:.2f} cm".format(text), point, 0, text_scale, (255, 255, 255), text_thickness)
if angle > 0:
angle = -angle + 90
elif angle < 0:
angle = angle + 90
text_mask = rotate_image(text_mask, -angle, point)
result = copy_to(img_filled, img.copy(), text_mask)
return result
def draw_real_sizes(img, rect, width_text, height_text, lbl_size_scale=2, lbl_color=(0, 0, 255), lbl_thickness=8):
"""
Draws real sizes of rotated rectangle into the image.
Parameters
----------
img : ndarray
Input image.
rect : tuple
Rotated rectangle.
width_text : string
Width of the rectangle in the form of string.
height_text : string
Height of the rectangle in the form of string.
lbl_size_scale : double
Scale of text.
lbl_color : tuple
Color of text.
lbl_thickness : int
Thickness of text.
Returns
-------
Output image.
"""
"""
For simplicity, we don't care about which side is width, which is height (as it can be ambiguous),
we just map the sides to correspoinding dimension texts based on the lengths.
This way it always maps correctly no matter the direction of the rotation.
"""
tl, tr, br, bl = order_points(cv2.boxPoints(rect))
side_a = tl - tr
side_b = tr - br
mid_pt_a = midpoint(tl, tr)
mid_pt_b = midpoint(tr, br)
shorter, longer = sorted([int(width_text), int(height_text)])
if np.linalg.norm(side_a) <= np.linalg.norm(side_b):
label_a, label_b = shorter, longer
else:
label_a, label_b = longer, shorter
pt_label_a = (int(mid_pt_a[0] + 10), int(mid_pt_a[1] + 10))
pt_label_b = (int(mid_pt_b[0] - 10), int(mid_pt_b[1]))
result = draw_rotated_text(img, label_a, pt_label_a, rect[2], lbl_size_scale, lbl_color, lbl_thickness)
result = draw_rotated_text(result, label_b, pt_label_b, rect[2], lbl_size_scale, lbl_color, lbl_thickness)
return result
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
import ipywidgets as widgets
from IPython.display import display
def create_slider(min, max, description):
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'),
)
def multicolor_segmentation(func,colors):
""" Allows 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