Source code for simba.ui.px_to_mm_ui

__author__ = "Simon Nilsson; sronilsson@gmail.com"

import os
from typing import Optional, Union

import cv2
import numpy as np

from simba.mixins.image_mixin import ImageMixin
from simba.mixins.plotting_mixin import PlottingMixin
from simba.utils.checks import check_file_exist_and_readable, check_float
from simba.utils.enums import ROI_SETTINGS, TextOptions
from simba.utils.read_write import get_fn_ext, get_video_meta_data

PIXEL_SENSITIVITY = ROI_SETTINGS.CLICK_SENSITIVITY.value
DRAW_COLOR = (144, 0, 255)
WIN_NAME = "Select coordinates: double left mouse click at two locations. Press ESC when done"


[docs]class GetPixelsPerMillimeterInterface(): """ Graphical interface to compute how many pixels represents a metric millimeter. .. video:: _static/img/GetPixelsPerMillimeterInterface.webm :width: 800 :autoplay: :loop: :muted: :align: center :param Union[str, os.PathLike] video_path: Path to a video file on disk. :param float known_metric_mm: Known millimeter distance to get the pixels conversion factor for. :return: The number of pixels per metric millimeter. :rtype: float :example: >>> runner = GetPixelsPerMillimeterInterface(video_path='/Users/simon/Desktop/envs/simba/troubleshooting/two_black_animals_14bp/project_folder/videos/Together_1.avi', known_metric_mm=140) >>> runner.run() >>> runner = GetPixelsPerMillimeterInterface(video_path='/Users/simon/Desktop/blank_video.mp4', known_metric_mm=140) >>> runner.run() """ def __init__(self, video_path: Union[str, os.PathLike], known_metric_mm: float, line_thickness: Optional[float] = None, circle_size: Optional[float] = None): check_file_exist_and_readable(file_path=video_path) self.video_meta_data = get_video_meta_data(video_path=video_path) check_float(name='distance', value=known_metric_mm, allow_zero=False, allow_negative=False) if circle_size is not None: check_float(name='circle_size', value=circle_size, allow_zero=False, allow_negative=False) if line_thickness is not None: check_float(name='line_thickness', value=line_thickness, allow_zero=False, allow_negative=False) self.video_path, self.known_metric_mm = video_path, float(known_metric_mm) self.frame, _ = ImageMixin.find_first_non_uniform_clr_frm(video_path=video_path, start_idx=0, end_idx=self.video_meta_data['fps']) self.video_dir, self.video_name, _ = get_fn_ext(filepath=self.video_path) self.font_scale, self.spacing_x, self.spacing_y = PlottingMixin().get_optimal_font_scales(text='"Press ESC to save and exit"', accepted_px_width=int(self.video_meta_data['width'] * 0.8), accepted_px_height=int(self.video_meta_data['height'] * 0.3)) self.circle_scale = PlottingMixin().get_optimal_circle_size(frame_size=(int(self.video_meta_data['width']), int(self.video_meta_data['height'])), circle_frame_ratio=70) self.circle_scale = circle_size if circle_size is not None else self.circle_scale self.line_thickness = line_thickness if line_thickness is not None else int(max(int(self.circle_scale/3), TextOptions.LINE_THICKNESS.value)) self.original_frm, self.overlay_frm = self.frame.copy(), self.frame.copy() self.ix, self.iy, self.cord_results = -1, -1, [] self.cord_status, self.move_status = False, False self.insert_status, self.change_loop = False, False self.coord_change, self.new_cord_lst = [], [] def _draw_circle(self, event, x, y, flags, param): if (event == cv2.EVENT_LBUTTONDBLCLK) and (len(self.cord_results) < 4): cv2.circle(self.overlay_frm, (x, y), self.circle_scale, DRAW_COLOR, -1) self.cord_results.append(x) self.cord_results.append(y) if len(self.cord_results) == 4: self.cord_status = True cv2.line(self.overlay_frm, (self.cord_results[0], self.cord_results[1]), (self.cord_results[2], self.cord_results[3]), DRAW_COLOR, self.line_thickness) def _select_cord_to_change(self, event, x, y, flags, param): if event == cv2.EVENT_LBUTTONDBLCLK: if np.sqrt((x - self.cord_results[0]) ** 2 + (y - self.cord_results[1]) ** 2) <= PIXEL_SENSITIVITY: self.coord_change = [1, self.cord_results[0], self.cord_results[1]] self.move_status = True elif np.sqrt((x - self.cord_results[2]) ** 2 + (y - self.cord_results[3]) ** 2) <= PIXEL_SENSITIVITY: self.coord_change = [2, self.cord_results[2], self.cord_results[3]] self.move_status = True def _select_new_dot_location(self, event, x, y, flags, param): if event == cv2.EVENT_LBUTTONDBLCLK: self.new_cord_lst.append(x) self.new_cord_lst.append(y) self.insert_status = True
[docs] def run(self): cv2.namedWindow(WIN_NAME, cv2.WINDOW_NORMAL) while 1: if self.cord_status == False and (self.move_status == False) and (self.insert_status == False): cv2.setMouseCallback(WIN_NAME, self._draw_circle) cv2.imshow(WIN_NAME, self.overlay_frm) k = cv2.waitKey(20) & 0xFF if k == 27: break if (self.cord_status == True) and (self.move_status == False) and (self.insert_status == False): if self.change_loop == True: self.overlay_frm = self.original_frm.copy() cv2.circle(self.overlay_frm, (self.cord_results[0], self.cord_results[1]), self.circle_scale, DRAW_COLOR, -1) cv2.circle(self.overlay_frm, (self.cord_results[2], self.cord_results[3]), self.circle_scale, DRAW_COLOR, -1) cv2.line(self.overlay_frm, (self.cord_results[0], self.cord_results[1]), (self.cord_results[2], self.cord_results[3]), DRAW_COLOR, self.line_thickness) cv2.putText(self.overlay_frm, "Click on circle to move", (TextOptions.BORDER_BUFFER_X.value, 25), cv2.FONT_HERSHEY_TRIPLEX, self.font_scale, DRAW_COLOR, 2) cv2.putText(self.overlay_frm, "Press ESC to save and exit", (TextOptions.BORDER_BUFFER_X.value, 25 + self.spacing_y), cv2.FONT_HERSHEY_TRIPLEX, self.font_scale, DRAW_COLOR, 2) cv2.imshow(WIN_NAME, self.overlay_frm) cv2.setMouseCallback(WIN_NAME, self._select_cord_to_change) if (self.move_status == True) and (self.insert_status == False): if self.change_loop == True: self.frame = self.original_frm.copy() self.change_loop = False if self.coord_change[0] == 1: cv2.circle(self.frame, (self.cord_results[2], self.cord_results[3]), self.circle_scale, DRAW_COLOR, -1) if self.coord_change[0] == 2: cv2.circle(self.frame, (self.cord_results[0], self.cord_results[1]), self.circle_scale, DRAW_COLOR, -1) cv2.imshow(WIN_NAME, self.frame) cv2.putText(self.frame, "Click on new circle location", (TextOptions.BORDER_BUFFER_X.value, 50), cv2.FONT_HERSHEY_TRIPLEX, self.font_scale, DRAW_COLOR, 2) cv2.setMouseCallback(WIN_NAME, self._select_new_dot_location) if self.insert_status == True: if self.coord_change[0] == 1: cv2.circle(self.frame, (self.cord_results[2], self.cord_results[3]), self.circle_scale, DRAW_COLOR, -1) cv2.circle(self.frame, (self.new_cord_lst[-2], self.new_cord_lst[-1]), self.circle_scale, DRAW_COLOR, -1) cv2.line(self.frame, (self.cord_results[2], self.cord_results[3]), (self.new_cord_lst[-2], self.new_cord_lst[-1]), DRAW_COLOR, self.line_thickness) self.cord_results = [self.new_cord_lst[-2], self.new_cord_lst[-1], self.cord_results[2], self.cord_results[3]] self.cord_status = True self.move_status = False self.insert_status = False self.change_loop = True if self.coord_change[0] == 2: cv2.circle(self.frame, (self.cord_results[0], self.cord_results[1]), self.circle_scale, DRAW_COLOR, -1) cv2.circle(self.frame, (self.new_cord_lst[-2], self.new_cord_lst[-1]), self.circle_scale, DRAW_COLOR, -1) cv2.line(self.frame, (self.cord_results[0], self.cord_results[1]), (self.new_cord_lst[-2], self.new_cord_lst[-1]), DRAW_COLOR, self.line_thickness) self.cord_results = [self.cord_results[0], self.cord_results[1], self.new_cord_lst[-2], self.new_cord_lst[-1]] self.cord_status = True self.move_status = False self.insert_status = False self.change_loop = True cv2.imshow(WIN_NAME, self.frame) k = cv2.waitKey(20) & 0xFF if k == 27: break self.cord_status = False self.move_status = False self.insert_status = False self.change_loop = False cv2.destroyAllWindows() euclidean_px_dist = np.sqrt((self.cord_results[0] - self.cord_results[2]) ** 2 + (self.cord_results[1] - self.cord_results[3]) ** 2) self.ppm = euclidean_px_dist / self.known_metric_mm