__author__ = "Simon Nilsson; sronilsson@gmail.com"
import os
from tkinter import *
from typing import Optional, Union
import pandas as pd
from simba.mixins.config_reader import ConfigReader
from simba.mixins.pop_up_mixin import PopUpMixin
from simba.ui.px_to_mm_ui import GetPixelsPerMillimeterInterface
from simba.ui.tkinter_functions import (CreateLabelFrameWithIcon, Entry_Box,
SimbaButton, SimBALabel,
SimBASeperator)
from simba.utils.checks import (check_file_exist_and_readable, check_float,
check_if_dir_exists, check_int)
from simba.utils.enums import (ConfigKey, Dtypes, Formats, Keys, Links,
Options, TagNames)
from simba.utils.errors import InvalidInputError, PermissionError
from simba.utils.printing import log_event, stdout_success
from simba.utils.read_write import (find_files_of_filetypes_in_directory,
get_video_meta_data, read_config_entry,
read_frm_of_video, read_video_info_csv)
VALID_CLR = 'yellowgreen'
INVALID_CLR = 'lightsalmon'
[docs]class VideoInfoTable(ConfigReader, PopUpMixin):
"""
Create GUI that allows users to modify resolutions, fps, and pixels-per-mm
interactively of videos within the SimBA project. Data is stored within the project_folder/logs/video_info.csv
file in the SimBA project.
:param Union[str, os.PathLike] config_path: path to SimBA project config file in Configparser format
:param Union[str, os.PathLike] video_dir: Optional path to directory with video files. If None, then read from the SimBA project as dictated by project config. Default None.
:param Optional[Union[str, os.PathLike]] video_info_path: Optional path to vide_info.csv file. If None, then read from the SimBA project as dictated by project config. Default None.
.. seealso::
`Tutorial <https://github.com/sgoldenlab/simba/blob/master/docs/Scenario1.md#step-3-set-video-parameters>`__.
:example:
>>> ui = VideoInfoTable(config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini")
>>> ui.run()
"""
def __init__(self,
config_path: Union[str, os.PathLike],
video_dir: Optional[Union[str, os.PathLike]] = None,
video_info_path: Optional[Union[str, os.PathLike]] = None):
ConfigReader.__init__(self, config_path=config_path, read_video_info=False)
log_event(logger_name=str(__class__.__name__), log_type=TagNames.CLASS_INIT.value, msg=self.create_log_msg_from_init_args(locals=locals()))
if video_dir is not None:
check_if_dir_exists(in_dir=video_dir, source=self.__class__.__name__)
self.video_dir = video_dir
if video_info_path is not None:
check_file_exist_and_readable(file_path=video_info_path)
self.video_info_path = video_info_path
if os.path.isfile(self.video_info_path):
self.video_info_df = read_video_info_csv(self.video_info_path).reset_index(drop=True)
self.prior_videos = list(self.video_info_df['Video'])
else:
self.video_info_df, self.prior_videos = None, []
self.distance_mm = read_config_entry(self.config, ConfigKey.FRAME_SETTINGS.value, ConfigKey.DISTANCE_MM.value, Dtypes.FLOAT.value,0.00)
self.distance_mm = 'None' if self.distance_mm == 0.0 else self.distance_mm
self.video_paths = find_files_of_filetypes_in_directory(directory=self.video_dir, extensions=Options.ALL_VIDEO_FORMAT_OPTIONS.value, raise_error=True, as_dict=True)
self.max_char_vid_name = len(max(self.video_paths.keys(), key=len))
PopUpMixin.__init__(self, title="VIDEO INFO", size=(1550, 800), icon='video')
def _check_float_bg(self, entry_box: Entry_Box, allow_blank: bool = False):
allow_blank = allow_blank or getattr(entry_box, 'allow_blank', False)
value = entry_box.entry_get
if value.strip() == '' and allow_blank:
entry_box.set_bg_clr(clr=VALID_CLR)
else:
bg_clr = VALID_CLR if check_float(value=value, name='', allow_zero=False, allow_negative=False, raise_error=False)[0] else INVALID_CLR
entry_box.set_bg_clr(clr=bg_clr)
def _check_int_bg(self, entry_box: Entry_Box, allow_blank: bool = False):
allow_blank = allow_blank or getattr(entry_box, 'allow_blank', False)
value = entry_box.entry_get
if value.strip() == '' and allow_blank:
entry_box.set_bg_clr(clr=VALID_CLR)
else:
bg_clr = VALID_CLR if check_int(value=value, name='', allow_zero=False, allow_negative=False, raise_error=False)[0] else INVALID_CLR
entry_box.set_bg_clr(clr=bg_clr)
def _get_video_table_rows(self):
self.videos = {}
padx = (0, 12)
for cnt, (video_name, video_path) in enumerate(self.video_paths.items()):
self.videos[video_name] = {}
row = cnt * 2 + 2 + 4
if video_name in self.prior_videos:
try:
prior_data = self.read_video_info(video_name=video_name, raise_error=False)[0].reset_index(drop=True).iloc[0].to_dict()
fps, width, height, distance, pixels_per_mm = prior_data['fps'], prior_data['Resolution_width'], prior_data['Resolution_height'], round(prior_data['Distance_in_mm'], 4), round(prior_data['pixels/mm'], 4)
except:
fps, width, height = self.video_meta_data[video_name]['fps'], self.video_meta_data[video_name]['width'], self.video_meta_data[video_name]['height']
distance, pixels_per_mm = self.distance_mm, 'None'
else:
fps, width, height = self.video_meta_data[video_name]['fps'], self.video_meta_data[video_name]['width'], self.video_meta_data[video_name]['height']
distance, pixels_per_mm = self.distance_mm, 'None'
try:
img = read_frm_of_video(video_path=video_path, frame_index=0, raise_error=False, size=(280, 120), keep_aspect_ratio=True)
except:
img = None
self.videos[video_name]["video_idx_lbl"] = SimBALabel(parent=self.video_frm, txt=str(cnt+1), font=Formats.FONT_REGULAR_BOLD.value)
self.videos[video_name]["video_name_lbl"] = SimBALabel(parent=self.video_frm, txt=video_name, font=Formats.FONT_REGULAR_BOLD.value, hover_img=img)
self.videos[video_name]["video_name_w_ext"] = os.path.basename(video_path)
self.videos[video_name]["video_fps_eb"] = Entry_Box(parent=self.video_frm, fileDescription='', labelwidth=0, value=fps, entry_box_width=12, justify='center', trace=self._check_float_bg)
self.videos[video_name]["video_width_eb"] = Entry_Box(parent=self.video_frm, fileDescription='', labelwidth=0, value=width, entry_box_width=12, justify='center', validation='numeric', trace=self._check_int_bg)
self.videos[video_name]["video_height_eb"] = Entry_Box(parent=self.video_frm, fileDescription='', labelwidth=0, value=height, entry_box_width=12, justify='center', validation='numeric', trace=self._check_int_bg)
self.videos[video_name]["video_known_distance_eb"] = Entry_Box(parent=self.video_frm, fileDescription='', labelwidth=0, value=distance, entry_box_width=12, justify='center', trace=self._check_float_bg)
self.videos[video_name]["find_dist_btn"] = SimbaButton(parent=self.video_frm, txt="CALCULATE DISTANCE", txt_clr='black', img='calipher', font=Formats.FONT_REGULAR_BOLD.value, cmd=lambda k=video_name: self._initate_find_distance(k), hover_font=Formats.FONT_REGULAR_BOLD.value)
self.videos[video_name]["video_px_per_mm"] = Entry_Box(parent=self.video_frm, fileDescription='', labelwidth=0, value=pixels_per_mm, entry_box_width=12, justify='center', trace=self._check_float_bg)
self.videos[video_name]["video_idx_lbl"].grid(row=row, column=0, sticky=NW, padx=padx)
self.videos[video_name]["video_name_lbl"].grid(row=row, column=1, sticky=W, padx=padx)
self.videos[video_name]["video_fps_eb"].grid(row=row, column=2, sticky=W, padx=padx)
self.videos[video_name]["video_width_eb"].grid(row=row, column=3, sticky=W, padx=padx)
self.videos[video_name]["video_height_eb"].grid(row=row, column=4, sticky=W, padx=padx)
self.videos[video_name]["find_dist_btn"].grid(row=row, column=5, sticky=W, padx=padx)
self.videos[video_name]["video_known_distance_eb"].grid(row=row, column=6, sticky=W, padx=padx)
self.videos[video_name]["video_px_per_mm"].grid(row=row, column=7, sticky=W, padx=padx)
# Apply initial background from current value (e.g. None -> invalid color)
self._check_float_bg(self.videos[video_name]["video_fps_eb"])
self._check_int_bg(self.videos[video_name]["video_width_eb"])
self._check_int_bg(self.videos[video_name]["video_height_eb"])
self._check_float_bg(self.videos[video_name]["video_known_distance_eb"])
self._check_float_bg(self.videos[video_name]["video_px_per_mm"])
if cnt != len(self.video_paths.keys()) -1:
sep = SimBASeperator(parent=self.video_frm, orient='horizontal', height=1, color="#ccc")
sep.grid(row=row + 1, column=0, columnspan=15, sticky="ew")
def _get_quick_set(self):
self.quick_set_frm = CreateLabelFrameWithIcon(parent=self.main_frm, header="BATCH QUICK SET", icon_name=Keys.DOCUMENTATION.value, icon_link=Links.VIDEO_PARAMETERS.value)
self.quick_set_frm_known_distance_eb = Entry_Box(parent=self.quick_set_frm, fileDescription='KNOWN DISTANCE (MM):', labelwidth=30, value='', entry_box_width=10, img='distance_red', justify='center', tooltip_key='VIDEO_INFO_KNOWN_DISTANCE', trace=self._check_float_bg, bg_clr=INVALID_CLR, allow_blank=True)
self.quick_set_frm_known_distance_btn = SimbaButton(parent=self.quick_set_frm, txt="APPLY", txt_clr='black', img='tick', font=Formats.FONT_REGULAR_BOLD.value, cmd=self._set_known_distance, cmd_kwargs={'distance': lambda: self.quick_set_frm_known_distance_eb.entry_get})
self.quick_set_px_mm_eb = Entry_Box(parent=self.quick_set_frm, fileDescription='PIXEL PER MILLIMETER:', labelwidth=30, value='', entry_box_width=10, img='ruler', justify='center', tooltip_key='VIDEO_INFO_PIXEL_PER_MM', bg_clr=INVALID_CLR, trace=self._check_float_bg, allow_blank=True)
self.quick_set_px_mm_btn = SimbaButton(parent=self.quick_set_frm, txt="APPLY", txt_clr='black', img='tick', font=Formats.FONT_REGULAR_BOLD.value, cmd=self._set_px_per_mm, cmd_kwargs={'px_per_mm': lambda: self.quick_set_px_mm_eb.entry_get})
self.quick_set_frm.grid(row=1, column=0, sticky=NW)
self.quick_set_frm_known_distance_eb.grid(row=0, column=0, sticky=NW)
self.quick_set_frm_known_distance_btn.grid(row=0, column=1, sticky=NW)
self.quick_set_px_mm_eb.grid(row=1, column=0, sticky=NW)
self.quick_set_px_mm_btn.grid(row=1, column=1, sticky=NW)
def _set_known_distance(self, distance: str):
check_float(name=f'{self.__class__.__name__} KNOWN DISTANCE', value=distance, allow_negative=False, allow_zero=False, raise_error=True)
for cnt, (video_name, video_path) in enumerate(self.video_paths.items()):
eb = self.videos[video_name]["video_known_distance_eb"]
eb.entry_set(val=distance)
self._check_float_bg(eb)
def _set_px_per_mm(self, px_per_mm: str):
check_float(name=f'{self.__class__.__name__} PIXEL PER MILLIMETER', value=px_per_mm, allow_negative=False, allow_zero=False, raise_error=True)
for cnt, (video_name, video_path) in enumerate(self.video_paths.items()):
eb = self.videos[video_name]["video_px_per_mm"]
eb.entry_set(val=px_per_mm)
self._check_float_bg(eb)
def _get_video_meta_data(self):
self.video_meta_data = {}
for cnt, (video_name, video_path) in enumerate(self.video_paths.items()):
self.video_meta_data[video_name] = get_video_meta_data(video_path=video_path)
def _duplicate_idx_1_distance(self):
first_key = list(self.video_paths.keys())[0]
first_val = self.videos[first_key]["video_known_distance_eb"].entry_get
is_none_or_empty = first_val is None or (isinstance(first_val, str) and first_val.strip().lower() in ('', 'none'))
if not is_none_or_empty and not check_float(name=self.__class__.__name__, value=first_val, allow_negative=False, allow_zero=False, raise_error=False)[0]:
raise InvalidInputError(msg=f'Distance for {first_key} cannot be duplicated. Not a valid distance value: {first_val}', source=self.__class__.__name__)
for cnt, (video_name, video_path) in enumerate(self.video_paths.items()):
eb = self.videos[video_name]["video_known_distance_eb"]
eb.entry_set(val=first_val)
self._check_float_bg(eb)
def _duplicate_idx_1_px_mm(self):
first_key = list(self.video_paths.keys())[0]
first_val = self.videos[first_key]["video_px_per_mm"].entry_get
is_none_or_empty = first_val is None or (isinstance(first_val, str) and first_val.strip().lower() in ('', 'none'))
if not is_none_or_empty and not check_float(name=self.__class__.__name__, value=first_val, allow_negative=False, allow_zero=False, raise_error=False)[0]:
raise InvalidInputError(msg=f'Pixel per millimeter for {first_key} cannot be duplicated. Not a valid distance value: {first_val}', source=self.__class__.__name__)
for cnt, (video_name, video_path) in enumerate(self.video_paths.items()):
eb = self.videos[video_name]["video_px_per_mm"]
eb.entry_set(val=first_val)
self._check_float_bg(eb)
def _initate_find_distance(self, video_name: str):
video_path = self.video_paths[video_name]
known_distance = self.videos[video_name]['video_known_distance_eb'].entry_get
check_float(name=f'{video_name} known distance', value=known_distance, allow_negative=False, allow_zero=False, raise_error=True)
px_per_mm_interface = GetPixelsPerMillimeterInterface(video_path=video_path, known_metric_mm=float(known_distance))
px_per_mm_interface.run()
eb = self.videos[video_name]["video_px_per_mm"]
eb.entry_set(str(round(px_per_mm_interface.ppm, 4)))
self._check_float_bg(eb)
def _get_instructions_frm(self):
self.instructions_frm = CreateLabelFrameWithIcon(parent=self.main_frm, header="INSTRUCTIONS", icon_name=Keys.DOCUMENTATION.value, icon_link=Links.VIDEO_PARAMETERS.value)
self.instructions_label_1 = SimBALabel(parent=self.instructions_frm, txt='1. Enter the known distance (mm) in "DISTANCE IN MM" column. Use "autopopulate" or "BATCH QUICK SET" for similar videos.', font=Formats.FONT_REGULAR_ITALICS.value)
self.instructions_label_2 = SimBALabel(parent=self.instructions_frm, txt='2. Click on "CALCULATE DISTANCE" button(s) to calculate pixels/mm for each video.', font=Formats.FONT_REGULAR_ITALICS.value)
self.instructions_label_3 = SimBALabel(parent=self.instructions_frm, txt="3. Click <SAVE PIXEL PER MILLIMETER DATA> when all the data are filled in.", font=Formats.FONT_REGULAR_ITALICS.value)
self.instructions_frm.grid(row=0, column=0, sticky=W)
self.instructions_label_1.grid(row=0, column=0, sticky=W)
self.instructions_label_2.grid(row=1, column=0, sticky=W)
self.instructions_label_3.grid(row=2, column=0, sticky=W)
def _get_save_frm(self, index: int):
self.save_frm = LabelFrame(self.main_frm, text="SAVE", font=Formats.FONT_HEADER.value)
self.save_data_btn = SimbaButton(parent=self.save_frm, txt="SAVE PIXEL PER MILLIMETER DATA", txt_clr='black', img='save_large', font=Formats.FONT_HEADER.value, hover_font=Formats.FONT_HEADER.value, cmd=self._save)
self.save_frm.grid(row=index, column=0, sticky=NW)
self.save_data_btn.grid(row=0, column=0, sticky=NW)
def _get_video_table(self):
padx = (0, 12)
self.video_frm = CreateLabelFrameWithIcon(parent=self.main_frm, header="PROJECT VIDEOS", icon_name='video_large', icon_link=Links.VIDEO_PARAMETERS.value, font=Formats.FONT_HEADER.value)
idx_header = SimBALabel(parent=self.video_frm, txt="INDEX", font=Formats.FONT_HEADER.value, justify='center', img='list', tooltip_key='VIDEO_INFO_HEADER_INDEX')
video_name_header = SimBALabel(parent=self.video_frm, txt="VIDEO NAME", font=Formats.FONT_HEADER.value, justify='center', img='video_2', tooltip_key='VIDEO_INFO_HEADER_VIDEO_NAME')
fps_header = SimBALabel(parent=self.video_frm, txt="FPS", font=Formats.FONT_HEADER.value, justify='center', img='camera', tooltip_key='VIDEO_INFO_HEADER_FPS')
width_header = SimBALabel(parent=self.video_frm, txt="VIDEO WIDTH", font=Formats.FONT_HEADER.value, justify='center', img='width', tooltip_key='VIDEO_INFO_HEADER_VIDEO_WIDTH')
height_header = SimBALabel(parent=self.video_frm, txt="VIDEO HEIGHT", font=Formats.FONT_HEADER.value, justify='center', img='height', tooltip_key='VIDEO_INFO_HEADER_VIDEO_HEIGHT')
find_dist_header = SimBALabel(parent=self.video_frm, txt="FIND DISTANCE", font=Formats.FONT_HEADER.value, justify='center', tooltip_key='VIDEO_INFO_HEADER_FIND_DISTANCE')
dist_header = SimBALabel(parent=self.video_frm, txt="DISTANCE IN MM", font=Formats.FONT_HEADER.value, justify='center', img='distance_red', tooltip_key='VIDEO_INFO_HEADER_DISTANCE_IN_MM')
px_mm_header = SimBALabel(parent=self.video_frm, txt="PIXELS PER MM", font=Formats.FONT_HEADER.value, justify='center', img='ruler', tooltip_key='VIDEO_INFO_HEADER_PIXELS_PER_MM')
seperator = SimBASeperator(parent=self.video_frm, color=None, orient='horizontal', borderwidth=1)
seperator.grid(row=1, column=0, columnspan=15, rowspan=1, sticky="ew")
px_mm_header.grid(row=0, column=7, sticky=W)
dist_header.grid(row=0, column=6, sticky=W, padx=padx)
find_dist_header.grid(row=0, column=5, sticky=W, padx=padx)
height_header.grid(row=0, column=4, sticky=W, padx=padx)
width_header.grid(row=0, column=3, sticky=W, padx=padx)
fps_header.grid(row=0, column=2, sticky=W, padx=padx)
video_name_header.grid(row=0, column=1, sticky=W, padx=padx)
idx_header.grid(row=0, column=0, sticky=NW, padx=padx)
self.video_frm.grid(row=3, column=0, sticky=W)
self._get_video_table_rows()
def _get_duplicate_btns(self):
self.duplicate_distance_btn = SimbaButton(parent=self.video_frm, txt="DUPLICATE INDEX 1", txt_clr='red', img='danger', font=Formats.FONT_REGULAR_ITALICS.value, cmd=self._duplicate_idx_1_distance, tooltip_key='VIDEO_INFO_DUPLICATE_DISTANCE')
self.duplicate_distance_btn.grid(row=2, column=6, sticky=W, padx=12)
self.duplicate_btn = SimbaButton(parent=self.video_frm, txt="DUPLICATE INDEX 1", txt_clr='red', img='danger', font=Formats.FONT_REGULAR_ITALICS.value, cmd=self._duplicate_idx_1_px_mm, tooltip_key='VIDEO_INFO_DUPLICATE_PX_PER_MM')
self.duplicate_btn.grid(row=2, column=7, sticky=W, padx=12)
sep = SimBASeperator(parent=self.video_frm, orient='horizontal', height=2, color="#ccc")
sep.grid(row=3, column=0, columnspan=15, sticky="ew")
def _save(self):
self.video_info_df = pd.DataFrame(columns=Formats.EXPECTED_VIDEO_INFO_COLS.value)
for video_name, video_path in self.videos.items():
fps = self.videos[video_name]["video_fps_eb"].entry_get
width = self.videos[video_name]["video_width_eb"].entry_get
height = self.videos[video_name]["video_height_eb"].entry_get
known_distance = self.videos[video_name]["video_known_distance_eb"].entry_get
px_per_mm = self.videos[video_name]["video_px_per_mm"].entry_get
check_float(name=f'{video_name} fps', value=fps, allow_zero=False, allow_negative=False,)
check_int(name=f'{video_name} width', value=width, min_value=1, raise_error=True)
check_int(name=f'{video_name} height', value=height, min_value=1, raise_error=True)
check_float(name=f'{video_name} known_distance', value=known_distance, allow_zero=False, allow_negative=False, raise_error=True)
check_float(name=f'{video_name} px_per_mm', value=px_per_mm, allow_zero=False, allow_negative=False, raise_error=True)
self.video_info_df.loc[len(self.video_info_df)] = [video_name, fps, width, height, known_distance, px_per_mm]
self.video_info_df.drop_duplicates(subset=["Video"], inplace=True)
self.video_info_df = self.video_info_df.set_index("Video")
try:
self.video_info_df.to_csv(self.video_info_path)
except PermissionError:
raise PermissionError(msg=f"SimBA tried to write to {self.video_info_path}, but was not allowed. If this file is open in another program, try closing it.", source=self.__class__.__name__)
stdout_success(msg=f"VIDEO INFO saved at {self.video_info_path}", source=self.__class__.__name__)
[docs] def run(self):
self._get_video_meta_data()
self._get_instructions_frm()
self._get_save_frm(index=2)
self._get_quick_set()
self._get_video_table()
self._get_duplicate_btns()
self._get_save_frm(index=4)
# ui = VideoInfoTable(config_path=r"E:\troubleshooting\mitra_pbn\mitra_pbn\project_folder\project_config.ini")
# ui.run()
# test.create_window()
#test.main_frm.mainloop()
# test = VideoInfoTable(config_path='/Users/simon/Desktop/troubleshooting/train_model_project/project_folder/project_config.ini')
# test.create_window()
# test.main_frm.mainloop()
#
# test = VideoInfoTable(config_path='/Users/simon/Desktop/envs/troubleshooting/sleap_5_animals_2/project_folder/project_config.ini')
# test.create_window()
# test.main_frm.mainloop()
# test = VideoInfoTable(config_path='/Users/simon/Desktop/envs/troubleshooting/two_black_animals_14bp/project_folder/project_config.ini')
# test.create_window()
# test.main_frm.mainloop()