Source code for simba.ui.blob_quick_check_interface

import os
from datetime import datetime
from tkinter import *
from typing import Optional, Tuple, Union

import cv2
import numpy as np
from PIL import Image, ImageTk
from shapely.geometry import MultiPolygon, Polygon

from simba.mixins.geometry_mixin import GeometryMixin
from simba.mixins.image_mixin import ImageMixin
from simba.ui.tkinter_functions import (CreateLabelFrameWithIcon, Entry_Box,
                                        SimbaButton)
from simba.utils.checks import (check_if_valid_img, check_instance, check_int,
                                check_str, check_valid_tuple)
from simba.utils.enums import Formats
from simba.utils.errors import FrameRangeError, InvalidVideoFileError
from simba.utils.lookups import get_icons_paths
from simba.utils.printing import SimbaTimer
from simba.utils.read_write import get_video_meta_data, read_frm_of_video
from simba.video_processors.video_processing import create_average_frm

FRAME_NAME = 'QUICK CHECK'
MENU_ICONS = get_icons_paths()

[docs]class BlobQuickChecker(): """ Interactive tool for visual comparisons using threshold-based difference detection with support for inclusion zones and interactive frame navigation. .. video:: _static/img/blob_quick_check.webm :width: 800 :autoplay: :loop: :muted: :align: center :param Union[str, os.PathLike] video_path: Path to the video file being analyzed. :param Union[str, os.PathLike] bg_video_path: Path to the background reference video. :param str method: Method for computing the difference image. Options: 'absolute', 'light', 'dark'. Default is 'absolute'. :param int threshold: Threshold value for difference computation (1-255). Default is 70. :param Optional[Union[Polygon, MultiPolygon]] inclusion_zones: Optional geometric regions of interest to highlight in the processed frames. :example: >>> _ = BlobQuickChecker(video_path=r"C:/troubleshooting/mitra/test/501_MA142_Gi_Saline_0515.mp4", bg_video_path=r"C:/troubleshooting/mitra/test/background_dir/501_MA142_Gi_Saline_0515.mp4") """ def __init__(self, video_path: Union[str, os.PathLike], bg_video_path: Union[str, os.PathLike], method: str = 'absolute', threshold: int = 70, inclusion_zones: Optional[Union[Polygon, MultiPolygon]] = None, status_label: Optional[Label] = None, close_kernel_size: Optional[Tuple[int, int]] = None, close_kernel_iterations: int = 3, open_kernel_size: Optional[Tuple[int, int]] = None, open_kernel_iterations: int = 3): self.video_meta, bg_meta = get_video_meta_data(video_path=video_path), get_video_meta_data(video_path=bg_video_path) if self.video_meta['resolution_str'] != bg_meta['resolution_str']: msg = f'The video, and background reference video, for {self.video_meta["video_name"]} have different resolutions: {self.video_meta["resolution_str"]} vs {bg_meta["resolution_str"]}' self._set_status_bar_panel(text=msg, fg='red') raise InvalidVideoFileError(msg=msg, source=self.__class__.__name__) if inclusion_zones is not None: check_instance(source=f'{self.__class__.__name__} inclusion_zones', instance=inclusion_zones, accepted_types=(Polygon, MultiPolygon,), raise_error=True) if status_label is not None: check_instance(source=f'{self.__class__.__name__} status_label', instance=status_label, accepted_types=(Label,), raise_error=True) self.status_label = status_label check_str(name='method', value=method, options=['absolute', 'light', 'dark'], raise_error=True) check_int(name='threshold', value=threshold, min_value=1, max_value=255) if close_kernel_size is not None: check_valid_tuple(x=close_kernel_size, source=f'{self.__class__.__name__} close kernel', accepted_lengths=(2,), valid_dtypes=(int,), min_integer=1) check_int(name=f'{self.__class__.__name__} close iterations', value=close_kernel_iterations, min_value=1, raise_error=True) self.closing_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, close_kernel_size) else: self.closing_kernel = None if open_kernel_size is not None: check_valid_tuple(x=open_kernel_size, source=f'{self.__class__.__name__} open_kernel_size', accepted_lengths=(2,), valid_dtypes=(int,), min_integer=1) check_int(name=f'{self.__class__.__name__} openiterations', value=open_kernel_iterations, min_value=1, raise_error=True) self.opening_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, open_kernel_size) else: self.opening_kernel = None self.bg_video_path, self.video_path, self.close_its, self.open_its = bg_video_path, video_path, close_kernel_iterations, open_kernel_iterations self.threshold, self.method = threshold, method self.img_idx = 0 self.img_window = Toplevel() self.img_window.title(FRAME_NAME) for k in MENU_ICONS.keys(): MENU_ICONS[k]["img"] = ImageTk.PhotoImage(image=Image.open(MENU_ICONS[k]["icon_path"])) try: self.img_window.iconphoto(False, MENU_ICONS['magnifying']["img"]) if 'magnifying' in list(MENU_ICONS.keys()) else None except: pass self.img_lbl = Label(self.img_window, name='img_lbl') self.img_lbl.grid(row=0, column=0, sticky=NW) self.inclusion_zones = inclusion_zones self.interact_panel = CreateLabelFrameWithIcon(parent=self.img_window, header="CHANGE IMAGE", font=Formats.FONT_HEADER.value, padx=5, pady=5, icon_name='frames') self._set_status_bar_panel(text=f'WAIT: COMPUTING BACKGROUND FOR VIDEO {self.video_meta["video_name"]} ({datetime.now().strftime("%H:%M:%S")})...') bg_timer = SimbaTimer(start=True) self.avg_frm = create_average_frm(video_path=self.bg_video_path, verbose=False) bg_timer.stop_timer() self._set_status_bar_panel(text=f'BACKGROUND COMPLETE FOR VIDEO {self.video_meta["video_name"]} (elapsed time: {bg_timer.elapsed_time_str}s...') self.create_img(idx=self.img_idx) self.forward_1s_btn = SimbaButton(parent=self.interact_panel, txt="+1s", img='plus_green_2', font=Formats.FONT_REGULAR.value, txt_clr='darkgreen', cmd=self._change_img, cmd_kwargs={'stride': int(self.video_meta['fps'])}) self.backwards_1s_btn = SimbaButton(parent=self.interact_panel, txt="-1s", img='minus_blue_2', font=Formats.FONT_REGULAR.value, txt_clr='darkblue', cmd=self._change_img, cmd_kwargs={'stride': -int(self.video_meta['fps'])}) self.custom_seconds_entry = Entry_Box(parent=self.interact_panel, fileDescription='CUSTOM SECONDS:', labelwidth=18, validation='numeric', entry_box_width=4, value=10) self.custom_fwd_btn = SimbaButton(parent=self.interact_panel, txt="FORWARD", img='fastforward_green_2', font=Formats.FONT_REGULAR.value, txt_clr='darkgreen', cmd=self._change_img, cmd_kwargs={'stride': 'custom_forward'}) self.custom_backwards_btn = SimbaButton(parent=self.interact_panel, txt="REVERSE", img='rewind_blue_2', font=Formats.FONT_REGULAR.value, txt_clr='darkblue', cmd=self._change_img,cmd_kwargs={'stride': 'custom_backward'}) self.first_frm_btn = SimbaButton(parent=self.interact_panel, txt="FIRST FRAME", img='first_frame_blue', font=Formats.FONT_REGULAR.value, txt_clr='darkblue', cmd=self._change_img, cmd_kwargs={'stride': 'first'}) self.last_frm_btn = SimbaButton(parent=self.interact_panel, txt="LAST FRAME", img='last_frame_blue', font=Formats.FONT_REGULAR.value, txt_clr='darkblue', cmd=self._change_img, cmd_kwargs={'stride': 'last'}) self.interact_panel.grid(row=1, column=0, sticky=NW) self.forward_1s_btn.grid(row=0, column=0, sticky=NW, pady=10) self.backwards_1s_btn.grid(row=0, column=1, sticky=NW, pady=10, padx=10) self.custom_seconds_entry.grid(row=0, column=2, sticky=NW, pady=10) self.custom_fwd_btn.grid(row=0, column=3, sticky=NW, pady=10) self.custom_backwards_btn.grid(row=0, column=4, sticky=NW, pady=10) self.first_frm_btn.grid(row=0, column=5, sticky=NW, pady=10) self.last_frm_btn.grid(row=0, column=6, sticky=NW, pady=10) self.img_window.protocol("WM_DELETE_WINDOW", self._close) self.img_window.mainloop() def draw_img(self, img: np.ndarray): self.pil_image = Image.fromarray(img) self.tk_image = ImageTk.PhotoImage(self.pil_image) self.img_lbl.configure(image=self.tk_image) self.img_lbl.image = self.tk_image def create_img(self, idx: int): img = read_frm_of_video(video_path=self.video_path, frame_index=idx) diff_img = ImageMixin.img_diff(x=img, y=self.avg_frm, threshold=self.threshold, method=self.method) diff_img = cv2.cvtColor(diff_img, cv2.COLOR_GRAY2RGB) if self.opening_kernel is not None: diff_img = cv2.morphologyEx(diff_img, cv2.MORPH_OPEN, self.opening_kernel, iterations=self.open_its) if self.closing_kernel is not None: diff_img = cv2.morphologyEx(diff_img, cv2.MORPH_CLOSE, self.closing_kernel, iterations=self.close_its) if self.inclusion_zones is not None: diff_img = GeometryMixin().view_shapes(shapes=[self.inclusion_zones], bg_img=diff_img, pixel_buffer=1) self.draw_img(img=diff_img) def _set_status_bar_panel(self, text: str, fg: str = 'blue'): if self.status_label is not None: self.status_label.configure(text=text, fg=fg) self.status_label.update_idletasks() def _change_img(self, stride: Union[int, str]): custom_s, new_frm_idx = self.custom_seconds_entry.entry_get.strip(), None if isinstance(stride, int): new_frm_idx = self.img_idx + stride elif stride == 'custom_forward': check_int(name='CUSTOM SECONDS', value=custom_s, min_value=1) custom_s = int(custom_s) new_frm_idx = int(self.img_idx + (custom_s * self.video_meta['fps'])) elif stride == 'custom_backward': check_int(name='CUSTOM SECONDS', value=custom_s, min_value=1) custom_s = int(custom_s) new_frm_idx = int(self.img_idx - (custom_s * self.video_meta['fps'])) elif stride == 'first': new_frm_idx = 0 elif stride == 'last': new_frm_idx = self.video_meta['frame_count']-1 if (0 > new_frm_idx) or (new_frm_idx > self.video_meta['frame_count']-1): msg = f'Cannot change frame. The new frame index {new_frm_idx} is outside the video {self.video_meta["video_name"]} frame range (video has {self.video_meta["frame_count"]} frames).' self._set_status_bar_panel(text=msg, fg='red') raise FrameRangeError(msg=msg, source=self.__class__.__name__) else: self.create_img(idx=new_frm_idx) self.img_idx = new_frm_idx self._set_status_bar_panel(text=f'Showing frame {self.img_idx} (video: {self.video_meta["video_name"]})...') def _close(self): self._set_status_bar_panel(text=f'Closing check for video {self.video_meta["video_name"]}...', fg='blue') try: self.img_window.destroy() self.img_window.quit() except: pass
# quick_checker = BlobQuickChecker(video_path=r"E:\open_video\barnes_maze\Run 33 - ID 231 - Barnes Maze Test (04-24-23).mp4", # bg_video_path=r"E:\open_video\barnes_maze\Run 33 - ID 231 - Barnes Maze Test (04-24-23).mp4", # close_kernel_size=(5, 5), # open_kernel_size=(7,7), # open_kernel_iterations=3, # close_kernel_iterations=7) # quick_checker = BlobQuickChecker(video_path=r"D:\EPM\sample_2\2025-02-24 08-25-56.mp4", # bg_video_path=r"D:\EPM\sample_2\bg\2025-02-24 08-25-56.mp4", # close_kernel_size=(7, 7), # close_kernel_iterations=3)