__author__ = "Simon Nilsson; sronilsson@gmail.com"
import math
import os
import warnings
from copy import copy, deepcopy
from tkinter import *
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
try:
from typing import Literal
except:
from typing_extensions import Literal
if TYPE_CHECKING:
from simba.roi_tools.interactive_roi_bufferer import InteractiveROIBufferer
import cv2
import numpy as np
import pandas as pd
from PIL import ImageTk
from scipy.spatial.distance import cdist
from shapely.geometry import Polygon
from simba.utils.checks import (check_file_exist_and_readable, check_int,
check_str, check_valid_array,
check_valid_dataframe, check_valid_tuple,
check_video_and_data_frm_count_align)
from simba.utils.enums import (ROI_SETTINGS, ConfigKey, Formats, Keys, Options,
Paths)
from simba.utils.errors import (InvalidInputError, NoROIDataError,
NotDirectoryError)
from simba.utils.printing import stdout_success, stdout_trash
from simba.utils.read_write import (find_files_of_filetypes_in_directory,
get_fn_ext, get_video_meta_data,
read_config_file, read_df, read_roi_data)
from simba.utils.warnings import (FrameRangeWarning, NoFileFoundWarning,
VideoFileWarning)
from simba.video_processors.roi_selector import ROISelector
from simba.video_processors.roi_selector_circle import ROISelectorCircle
from simba.video_processors.roi_selector_polygon import ROISelectorPolygon
warnings.simplefilter(action='ignore', category=pd.errors.PerformanceWarning)
DRAW_FRAME_NAME = "DEFINE SHAPE"
[docs]def create_rectangle_entry(rectangle_selector: Union[ROISelector, "InteractiveROIBufferer"],
video_name: str,
shape_name: str,
clr_name: str,
clr_bgr: Tuple[int, int, int],
thickness: int,
ear_tag_size: int,
px_conversion_factor: float):
return {'Video': video_name,
'Shape_type': ROI_SETTINGS.RECTANGLE.value,
'Name': shape_name,
'Color name': clr_name,
'Color BGR': clr_bgr,
'Thickness': thickness,
'Center_X': rectangle_selector.center[0],
'Center_Y': rectangle_selector.center[1],
'topLeftX': rectangle_selector.top_left[0],
'topLeftY': rectangle_selector.top_left[1],
'Bottom_right_X': rectangle_selector.bottom_right[0],
'Bottom_right_Y': rectangle_selector.bottom_right[1],
'width': rectangle_selector.width,
'height': rectangle_selector.height,
'width_cm': round((rectangle_selector.width / px_conversion_factor) / 10, 2),
'height_cm': round((rectangle_selector.height / px_conversion_factor) / 10, 2),
'area_cm': round((round((rectangle_selector.width / px_conversion_factor) / 10, 2) * round((rectangle_selector.height / px_conversion_factor) / 10, 2)), 2),
"Tags": {"Center tag": rectangle_selector.center,
"Top left tag": rectangle_selector.top_left,
"Bottom right tag": rectangle_selector.bottom_right,
"Top right tag": rectangle_selector.top_right_tag,
"Bottom left tag": rectangle_selector.bottom_left_tag,
"Top tag": rectangle_selector.top_tag,
"Right tag": rectangle_selector.right_tag,
"Left tag": rectangle_selector.left_tag,
"Bottom tag": rectangle_selector.bottom_tag},
'Ear_tag_size': ear_tag_size}
[docs]def create_circle_entry(circle_selector: Union[ROISelectorCircle, "InteractiveROIBufferer"],
video_name: str,
shape_name: str,
clr_name: str,
clr_bgr: Tuple[int, int, int],
thickness: int,
ear_tag_size: int,
px_conversion_factor: float):
return {'Video': video_name,
'Shape_type': ROI_SETTINGS.CIRCLE.value,
'Name': shape_name,
'Color name': clr_name,
'Color BGR': clr_bgr,
'Thickness': thickness,
'centerX': int(circle_selector.circle_center[0]),
'centerY': int(circle_selector.circle_center[1]),
'radius': int(circle_selector.circle_radius),
'radius_cm': round((circle_selector.circle_radius / px_conversion_factor) / 10, 2),
'area_cm': round(math.pi * (round((circle_selector.circle_radius / px_conversion_factor) / 10, 2) **2), 2),
"Tags": {"Center tag": circle_selector.circle_center,
"Border tag": circle_selector.left_border_tag},
'Ear_tag_size': ear_tag_size}
[docs]def create_polygon_entry(polygon_selector: Union[ROISelectorPolygon, "InteractiveROIBufferer"],
video_name: str,
shape_name: str,
clr_name: str,
clr_bgr: Tuple[int, int, int],
thickness: int,
ear_tag_size: int,
px_conversion_factor: float) -> dict:
return {'Video': video_name,
'Shape_type': ROI_SETTINGS.POLYGON.value,
'Name': shape_name,
'Color name': clr_name,
'Color BGR': clr_bgr,
'Thickness': thickness,
'Center_X': int(polygon_selector.polygon_centroid[0]),
'Center_Y': int(polygon_selector.polygon_centroid[1]),
'vertices': polygon_selector.polygon_arr,
'center': tuple(polygon_selector.polygon_centroid),
'area': polygon_selector.polygon_area,
'max_vertice_distance': polygon_selector.max_vertice_distance,
"area_cm": round(polygon_selector.polygon_area / px_conversion_factor, 2),
'Tags': polygon_selector.tags,
'Ear_tag_size': ear_tag_size}
[docs]def create_duplicated_rectangle_entry(shape_entry: dict, jump_size: int) -> dict:
shape_entry['Name'] = f'{shape_entry["Name"]}_duplicated'
shape_entry['Center_X'] = shape_entry['Center_X'] + jump_size
shape_entry['Center_Y'] = shape_entry['Center_Y'] + jump_size
shape_entry['topLeftX'] = shape_entry['topLeftX'] + jump_size
shape_entry['topLeftY'] = shape_entry['topLeftY'] + jump_size
shape_entry['Bottom_right_X'] = shape_entry['Bottom_right_X'] + jump_size
shape_entry['Bottom_right_Y'] = shape_entry['Bottom_right_Y'] + jump_size
new_shape_tags = {}
for tag_name, tag_value in shape_entry['Tags'].items():
new_shape_tags[tag_name] = (int(tag_value[0] + jump_size), int(tag_value[1] + jump_size))
shape_entry['Tags'] = new_shape_tags
return shape_entry
[docs]def create_duplicated_circle_entry(shape_entry: dict, jump_size: int) -> dict:
out = copy(shape_entry)
out['Name'] = f'{out["Name"]}_duplicated'
out['centerX'] = out['centerX'] + jump_size
out['centerY'] = out['centerY'] + jump_size
out['Tags'] = {'Center tag': ((out['centerX']), int(out['centerY'])),
'Border tag': (int(out['Tags']['Border tag'][0] + jump_size), int(out['Tags']['Border tag'][1] + jump_size))}
return out
[docs]def create_duplicated_polygon_entry(shape_entry: dict, jump_size: int) -> dict:
shape_entry['Name'] = f'{shape_entry["Name"]}_duplicated'
shape_entry['Center_X'] = shape_entry['Center_X'] + jump_size
shape_entry['Center_Y'] = shape_entry['Center_Y'] + jump_size
shape_entry['vertices'] = shape_entry['vertices'] + jump_size
new_shape_tags = {}
for tag_name, tag_value in shape_entry['Tags'].items():
new_shape_tags[tag_name] = (int(tag_value[0] + jump_size), int(tag_value[1] + jump_size))
shape_entry['Tags'] = new_shape_tags
return shape_entry
[docs]def set_roi_metric_sizes(roi_dict: dict, px_conversion_factor: Union[int, float]) -> dict:
"""
Helper to update the metric attributes of a shape dictionary.
"""
out = {}
for name, data in roi_dict.items():
if data['Shape_type'].lower() == ROI_SETTINGS.RECTANGLE.value:
data['width_cm'] = round((data['width'] / px_conversion_factor) / 10, 2)
data['height_cm'] = round((data['height'] / px_conversion_factor) / 10, 2)
data['area_cm'] = round((round((data['width'] / px_conversion_factor) / 10, 2) * round((data['height'] / px_conversion_factor) / 10, 2)), 2)
elif data['Shape_type'].lower() == ROI_SETTINGS.CIRCLE.value:
data['radius_cm'] = round((data['radius'] / px_conversion_factor) / 10, 2)
data['area_cm'] = round(math.pi * (round((data['radius'] / px_conversion_factor) / 10, 2) ** 2), 2)
elif data['Shape_type'].lower() == ROI_SETTINGS.POLYGON.value:
data['area'] = Polygon(data['vertices']).simplify(tolerance=20, preserve_topology=True).area
data['area_cm'] = round(data['area'] / px_conversion_factor, 2)
data['max_vertice_distance'] = np.max(cdist(data['vertices'], data['vertices']).astype(np.int32))
out[name] = data
return out
[docs]def get_roi_df_from_dict(roi_dict: dict, video_name_nesting: Optional[bool] = False) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
"""
Helper create DataFrames from a shape dictionary.
If nesting is True, then the roi_dict has video name, and shape name keys.
"""
rectangles_df, circles_df, polygon_df = pd.DataFrame(columns=get_rectangle_df_headers()), pd.DataFrame(columns=get_circle_df_headers()), pd.DataFrame(columns=get_polygon_df_headers())
if not video_name_nesting:
for shape_name, shape_data in roi_dict.items():
if shape_data['Shape_type'].lower() == ROI_SETTINGS.RECTANGLE.value:
rectangles_df = pd.concat([rectangles_df, pd.DataFrame([shape_data])], ignore_index=True)
elif shape_data['Shape_type'].lower() == ROI_SETTINGS.CIRCLE.value:
circles_df = pd.concat([circles_df, pd.DataFrame([shape_data])], ignore_index=True)
elif shape_data['Shape_type'].lower() == ROI_SETTINGS.POLYGON.value:
polygon_df = pd.concat([polygon_df, pd.DataFrame([shape_data])], ignore_index=True)
else:
for video_name, video_data in roi_dict.items():
for shape_name, shape_data in video_data.items():
if shape_data['Shape_type'].lower() == ROI_SETTINGS.RECTANGLE.value:
rectangles_df = pd.concat([rectangles_df, pd.DataFrame([shape_data])], ignore_index=True)
elif shape_data['Shape_type'].lower() == ROI_SETTINGS.CIRCLE.value:
circles_df = pd.concat([circles_df, pd.DataFrame([shape_data])], ignore_index=True)
elif shape_data['Shape_type'].lower() == ROI_SETTINGS.POLYGON.value:
polygon_df = pd.concat([polygon_df, pd.DataFrame([shape_data])], ignore_index=True)
return (rectangles_df, circles_df, polygon_df)
[docs]def get_roi_dict_from_dfs(rectangle_df: pd.DataFrame,
circle_df: pd.DataFrame,
polygon_df: pd.DataFrame,
video_name_nesting: Optional[bool] = False) -> dict:
"""
Helper create dict from a shape dataframes.
"""
out = {}
for idx, row in rectangle_df.iterrows():
if not video_name_nesting:
out[row['Name']] = row.to_dict()
else:
if row['Video'] not in list(out.keys()):
out[row['Video']] = {}
out[row['Video']][row['Name']] = row.to_dict()
for idx, row in circle_df.iterrows():
if not video_name_nesting:
out[row['Name']] = row.to_dict()
else:
if row['Video'] not in list(out.keys()):
out[row['Video']] = {}
out[row['Video']][row['Name']] = row.to_dict()
for idx, row in polygon_df.iterrows():
if not video_name_nesting:
out[row['Name']] = row.to_dict()
else:
if row['Video'] not in list(out.keys()):
out[row['Video']] = {}
out[row['Video']][row['Name']] = row.to_dict()
return out
[docs]def get_roi_data(roi_path: Union[str, os.PathLike], video_name: str) -> tuple:
""" Helper to read in ROI data from disk"""
rectangles_df, circles_df, polygon_df = pd.DataFrame(columns=get_rectangle_df_headers()), pd.DataFrame(columns=get_circle_df_headers()), pd.DataFrame(columns=get_polygon_df_headers())
other_roi_dict = {}
roi_names, roi_dict = [''], {}
other_video_names_w_rois = ['']
if os.path.isfile(roi_path):
in_rectangles_df, in_circles_df, in_polygon_df = read_roi_data(roi_path=roi_path)
other_video_names_w_rois = list(set(list(in_rectangles_df['Video'].unique()) + list(in_circles_df['Video'].unique()) + list(in_polygon_df['Video'].unique())))
other_video_names_w_rois = [x for x in other_video_names_w_rois if x != video_name]
if len(other_video_names_w_rois) == 0: other_video_names_w_rois = ['']
rectangles_df = in_rectangles_df[in_rectangles_df['Video'] == video_name].reset_index(drop=True)
circles_df = in_circles_df[in_circles_df['Video'] == video_name].reset_index(drop=True)
polygon_df = in_polygon_df[in_polygon_df['Video'] == video_name].reset_index(drop=True)
other_rectangles_df = in_rectangles_df[in_rectangles_df['Video'] != video_name].reset_index(drop=True)
other_circles_df = in_circles_df[in_circles_df['Video'] != video_name].reset_index(drop=True)
other_polygon_df = in_polygon_df[in_polygon_df['Video'] != video_name].reset_index(drop=True)
other_roi_dict = get_roi_dict_from_dfs(rectangle_df=other_rectangles_df, circle_df=other_circles_df, polygon_df=other_polygon_df, video_name_nesting=True)
if len(rectangles_df) + len(circles_df) + len(polygon_df) > 0:
roi_names = list(set(list(rectangles_df['Name'].unique()) + list(circles_df['Name'].unique()) + list(polygon_df['Name'].unique())))
roi_dict = get_roi_dict_from_dfs(rectangle_df=rectangles_df, circle_df=circles_df, polygon_df=polygon_df, video_name_nesting=False)
return (rectangles_df, circles_df, polygon_df, roi_dict, roi_names, other_roi_dict, other_video_names_w_rois)
[docs]def get_roi_data_for_video_name(roi_path: Union[str, os.PathLike], video_name: str):
in_rectangles_df, in_circles_df, in_polygon_df = read_roi_data(roi_path=roi_path)
rectangles_df = in_rectangles_df[in_rectangles_df['Video'] == video_name].reset_index(drop=True)
circles_df = in_circles_df[in_circles_df['Video'] == video_name].reset_index(drop=True)
polygon_df = in_polygon_df[in_polygon_df['Video'] == video_name].reset_index(drop=True)
roi_dict = get_roi_dict_from_dfs(rectangle_df=rectangles_df, circle_df=circles_df, polygon_df=polygon_df, video_name_nesting=False)
return roi_dict
[docs]def get_video_roi_data_from_dict(roi_dict: dict, video_name: str) -> dict:
out = {}
for video_key, data in roi_dict.items():
if video_key == video_name:
for shape_name, shape_data in data.items():
out[shape_name] = shape_data
return out
[docs]def change_roi_dict_video_name(roi_dict: dict, video_name: str) -> dict:
out = {}
for shape_name, shape_data in roi_dict.items():
new_shape_data = copy(shape_data)
new_shape_data['Video'] = video_name
out[shape_name] = new_shape_data
return out
[docs]def get_vertices_hexagon(center: Tuple[int, int],
radius: int) -> Tuple[np.ndarray, Dict[str, Tuple[int, int]]]:
"""
Generates the vertices of a regular hexagon centered at a given point with a specified radius.
.. image:: _static/img/get_vertices_hexagon.webp
:alt: Get vertices hexagon
:width: 400
:align: center
:param Tuple[int, int] center: A tuple (x, y) representing the center coordinates of the hexagon. Must contain exactly two numeric values.
:param int radius: The radius of the hexagon, which represents the distance from the center to any of the vertices. Must be a positive integer.
:return: A tuple containing vertices as array and dict.
:rtype: Tuple[np.ndarray, Dict[str, Tuple[int, int]]]
"""
check_valid_tuple(x=center, source=get_vertices_hexagon.__name__, accepted_lengths=(2,), valid_dtypes=Formats.NUMERIC_DTYPES.value)
check_int(name='radius', value=radius, min_value=1)
vertices = []
x_c, y_c = center
for i in range(6):
angle_rad = np.deg2rad(60 * i)
x_i = x_c + radius * np.cos(angle_rad)
y_i = y_c + radius * np.sin(angle_rad)
vertices.append((x_i, y_i))
vertices_dict = {"Center_tag": (center[0], center[1])}
for tag_id, tag in enumerate(vertices):
vertices_dict[f"Tag_{tag_id}"] = (round(tag[0]), round(tag[1]))
return (np.round(np.array(vertices)).astype(np.int32), vertices_dict)
[docs]def get_half_circle_vertices(center: Tuple[int, int],
radius: int,
direction: Literal['NORTH', 'SOUTH', 'WEST', 'EAST', 'NORTH-EAST', 'NORTH-WEST', 'SOUTH-EAST', 'SOUTH-WEST'],
n_points: Optional[int] = 50) -> Tuple[np.ndarray, Dict[str, Tuple[int, int]]]:
"""
Generates vertices for a half-circle with a given radius and direction, centered at a specific point.
.. image:: _static/img/get_half_circle_vertices.webp
:alt: Get half circle vertices
:width: 600
:align: center
:param Tuple[int, int] center: A tuple (x, y) representing the center coordinates of the half-circle. Must contain exactly two numeric values.
:param int radius: The radius of the half-circle. Must be a positive integer.
:param Literal['NORTH', 'SOUTH', 'WEST', 'EAST', 'NORTH-EAST', 'NORTH-WEST', 'SOUTH-EAST', 'SOUTH-WEST'] direction: The direction in which the half-circle is oriented (the side the curved arc faces).
:param Optional[int] n_points: The number of vertices used to approximate the half-circle. Defaults to 50.
:return: A tuple containing vertices as array and dict.
:rtype: Tuple[np.ndarray, Dict[str, Tuple[int, int]]]
"""
check_valid_tuple(x=center, source=get_vertices_hexagon.__name__, accepted_lengths=(2,), valid_dtypes=Formats.NUMERIC_DTYPES.value)
check_int(name='radius', value=radius, min_value=1)
check_str(name='direction', options=['NORTH', 'SOUTH', 'WEST', 'EAST', 'NORTH-EAST', 'NORTH-WEST', 'SOUTH-EAST', 'SOUTH-WEST'], value=direction)
x_c, y_c = center
if direction == "WEST":
a = np.linspace(np.pi / 2, 3 * np.pi / 2, n_points)
elif direction == "EAST":
a = np.linspace(-np.pi / 2, np.pi / 2, n_points)
elif direction == "SOUTH":
a = np.linspace(0, np.pi, n_points)
elif direction == 'SOUTH-WEST':
a = np.linspace(np.pi / 4, 5 * np.pi / 4, n_points)
elif direction == 'SOUTH-EAST':
a = np.linspace(-np.pi / 4, 3 * np.pi / 4, n_points)
elif direction == 'NORTH-WEST':
a = np.linspace(3 * np.pi / 4, 7 * np.pi / 4, n_points)
elif direction == 'NORTH-EAST':
a = np.linspace(5 * np.pi / 4, 9 * np.pi / 4, n_points)
else:
a = np.linspace(np.pi, 2 * np.pi, n_points)
x, y = x_c + radius * np.cos(a), y_c + radius * np.sin(a)
vertices = np.round(np.column_stack((x, y))).astype(np.int32)
shape_center = np.round(np.array(Polygon(vertices).centroid.coords)[0]).astype(np.int32)
vertices_dict = {"Center_tag": (shape_center[0], shape_center[1])}
for tag_id in range(vertices.shape[0]):
vertices_dict[f"Tag_{tag_id}"] = (vertices[tag_id][0], vertices[tag_id][1])
return (np.round(np.array(vertices)).astype(np.int32), vertices_dict)
[docs]def get_triangle_vertices(center: Tuple[int, int],
side_length: int,
direction: int) -> Tuple[np.ndarray, Dict[str, Tuple[int, int]]]:
"""
Find equilateral triangle vertices knowing the center, direction and length side.
.. image:: _static/img/get_triangle_vertices.webp
:alt: Get triangle vertices
:width: 600
:align: center
:param Tuple[int, int] center: A tuple (x, y) representing the center coordinates of the hexagon. Must contain exactly two numeric values.
:param int side_length: The length of each of the three sides in pixels.
:param int direction: The direction to point the vertices.
:return: 2-part tuple containing vertices as array and dict.
:rtype: Tuple[np.ndarray, Dict[str, Tuple[int, int]]]
"""
check_valid_tuple(x=center, source=get_vertices_hexagon.__name__, accepted_lengths=(2,), valid_dtypes=Formats.NUMERIC_DTYPES.value)
check_int(name='side_length', value=side_length, min_value=1)
check_int(name='direction', value=direction, min_value=0, max_value=360)
direction_radians = np.radians(direction)
radius = side_length / np.sqrt(3)
top_vertex = (center[0] + radius * np.cos(direction_radians), center[1] + radius * np.sin(direction_radians))
vertex2 = (center[0] + radius * np.cos(direction_radians + np.radians(120)), center[1] + radius * np.sin(direction_radians + np.radians(120)))
vertex3 = (center[0] + radius * np.cos(direction_radians + np.radians(-120)), center[1] + radius * np.sin(direction_radians + np.radians(-120)))
vertices = np.round(np.array([top_vertex, vertex2, vertex3, top_vertex])).astype(np.int32)
vertices_dict = {"Center_tag": (int(center[0]), int(center[1])), 'Tag_0': (int(top_vertex[0]), int(top_vertex[1])), 'Tag_1': (int(vertex2[0]), int(vertex2[1])), 'Tag_2': (int(vertex3[0]), int(vertex3[1]))}
return (vertices, vertices_dict)
[docs]def multiply_ROIs(filename: Union[str, os.PathLike],
config_path: Optional[Union[str, os.PathLike]] = None,
roi_coordinates_path: Optional[Union[str, os.PathLike]] = None,
videos_dir: Optional[Union[str, os.PathLike]] = None) -> None:
"""
Reproduce ROIs in one video to all other videos in SimBA project.
:param Union[str, os.PathLike] config_path: Path to SimBA project config file.
:param Union[str, os.PathLike] filename: Path to video in project for which ROIs should be duplicated in the other videos in the project
:return: None. The results are stored in the ``/project_folder/logs/measures\ROI_definitions.h5`` of the SimBA project
:example:
>>> multiply_ROIs(config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini", filename=r"C:\troubleshooting\mitra\project_folder\videos\501_MA142_Gi_CNO_0514.mp4")
"""
if config_path is not None:
check_file_exist_and_readable(file_path=config_path)
config = read_config_file(config_path=config_path)
project_path = config.get(ConfigKey.GENERAL_SETTINGS.value, ConfigKey.PROJECT_PATH.value)
videos_dir = os.path.join(project_path, "videos")
roi_coordinates_path = os.path.join(project_path, "logs", Paths.ROI_DEFINITIONS.value)
check_file_exist_and_readable(file_path=filename)
_, video_name, video_ext = get_fn_ext(filename)
if not os.path.isdir(videos_dir):
raise NotDirectoryError(msg=f'Could not find the videos directory in the SimBA project. SimBA expected a directory at location: {videos_dir}')
if not os.path.isfile(roi_coordinates_path):
raise NoROIDataError(msg=f"Cannot multiply ROI definitions: no ROI definitions exist in SimBA project. Could find find a file at expected location {roi_coordinates_path}", source=multiply_ROIs.__name__)
with pd.HDFStore(roi_coordinates_path) as hdf: roi_data_keys = [x[1:] for x in hdf.keys()]
missing_keys = [x for x in roi_data_keys if x not in [Keys.ROI_RECTANGLES.value, Keys.ROI_CIRCLES.value, Keys.ROI_POLYGONS.value]]
if len(missing_keys) > 0:
raise NoROIDataError(msg=f'The ROI data file {roi_coordinates_path} is corrupted. Missing the following keys: {missing_keys}', source=multiply_ROIs.__name__)
rectangles_df = pd.read_hdf(path_or_buf=roi_coordinates_path, key=Keys.ROI_RECTANGLES.value)
circles_df = pd.read_hdf(path_or_buf=roi_coordinates_path, key=Keys.ROI_CIRCLES.value)
polygon_df = pd.read_hdf(path_or_buf=roi_coordinates_path, key=Keys.ROI_POLYGONS.value)
check_valid_dataframe(df=rectangles_df, source=f'{multiply_ROIs.__name__} rectangles_df', required_fields=['Video', 'Name'])
check_valid_dataframe(df=circles_df, source=f'{multiply_ROIs.__name__} circles_df', required_fields=['Video', 'Name'])
check_valid_dataframe(df=polygon_df, source=f'{multiply_ROIs.__name__} polygon_df', required_fields=['Video', 'Name'])
videos_w_rectangles = list(rectangles_df["Video"].unique())
videos_w_circles = list(circles_df["Video"].unique())
videos_w_polygons = list(polygon_df["Video"].unique())
videos_w_shapes = list(set(videos_w_rectangles + videos_w_circles + videos_w_polygons))
if video_name not in videos_w_shapes:
raise NoROIDataError(msg=f"Cannot replicate ROIs to all other videos: no ROI records exist for {video_name}. Create ROIs for for video {video_name}", source=multiply_ROIs.__name__)
other_video_file_paths = find_files_of_filetypes_in_directory(directory=videos_dir, extensions=Options.ALL_VIDEO_FORMAT_OPTIONS.value)
other_video_file_paths = [x for x in other_video_file_paths if x != filename]
if len(other_video_file_paths) == 0:
raise NoROIDataError(msg=f"Cannot replicate ROIs to other videos. No other videos exist in project {videos_dir} directory.", source=multiply_ROIs.__name__)
r_df = [pd.DataFrame(columns=get_rectangle_df_headers()) if video_name not in videos_w_rectangles else rectangles_df[rectangles_df["Video"] == video_name]][0]
c_df = [pd.DataFrame(columns=get_circle_df_headers()) if video_name not in videos_w_circles else circles_df[circles_df["Video"] == video_name]][0]
p_df = [pd.DataFrame(columns=get_polygon_df_headers()) if video_name not in videos_w_polygons else polygon_df[polygon_df["Video"] == video_name]][0]
rectangle_results, circle_results, polygon_results = deepcopy(r_df), deepcopy(c_df), deepcopy(p_df)
for other_video_file_name in other_video_file_paths:
_, other_vid_name, ext = get_fn_ext(other_video_file_name)
if len(r_df) > 0:
x = deepcopy(r_df); x['Video'] = other_vid_name
rectangle_results = pd.concat([rectangle_results, x], axis=0)
if len(circle_results) > 0:
x = deepcopy(c_df); x['Video'] = other_vid_name
circle_results = pd.concat([circle_results, x], axis=0)
if len(polygon_results) > 0:
x = deepcopy(p_df); x['Video'] = other_vid_name
polygon_results = pd.concat([polygon_results, x], axis=0)
rectangle_results = rectangle_results.drop_duplicates(subset=["Video", "Name"], keep="first")
circle_results = circle_results.drop_duplicates(subset=["Video", "Name"], keep="first")
polygon_results = polygon_results.drop_duplicates(subset=["Video", "Name"], keep="first")
store = pd.HDFStore(roi_coordinates_path, mode="w")
store[Keys.ROI_RECTANGLES.value] = rectangle_results
store[Keys.ROI_CIRCLES.value] = circle_results
store[Keys.ROI_POLYGONS.value] = polygon_results
store.close()
stdout_success(msg=f"ROIs for {video_name} applied to a further {len(other_video_file_paths)} videos (Duplicated rectangles count: {len(r_df)}, circles: {len(c_df)}, polygons: {len(p_df)}).")
#print('Next, click on "DRAW" to modify ROI location(s) or click on "RESET" to remove ROI drawing(s)')
[docs]def reset_video_ROIs(config_path: Union[str, os.PathLike],
filename: Union[str, os.PathLike]) -> None:
"""
Delete drawn ROIs for a single video in a SimBA project.
:param Union[str, os.PathLike] config_path: Path to SimBA project config file.
:param Union[str, os.PathLike] filename: Path to video in project for which ROIs should be duplicated in the other videos in the project
:return: None. The results are stored in the ``/project_folder/logs/measures\ROI_definitions.h5`` of the SimBA project
:example:
>>> reset_video_ROIs(config_path=r"C:\troubleshooting\mitra\project_folder\project_config.ini", filename=r"C:\troubleshooting\mitra\project_folder\videos\501_MA142_Gi_CNO_0514.mp4")
"""
check_file_exist_and_readable(file_path=config_path)
check_file_exist_and_readable(file_path=filename)
_, video_name, video_ext = get_fn_ext(filename)
config = read_config_file(config_path=config_path)
project_path = config.get(ConfigKey.GENERAL_SETTINGS.value, ConfigKey.PROJECT_PATH.value)
roi_coordinates_path = os.path.join(project_path, "logs", Paths.ROI_DEFINITIONS.value)
if not os.path.isfile(roi_coordinates_path):
raise NoROIDataError(msg=f"Cannot reset/delete ROI definitions: no ROI definitions exist in SimBA project. Could find find a file at expected location {roi_coordinates_path}. Create ROIs before deleting ROIs.", source=reset_video_ROIs.__name__)
with pd.HDFStore(roi_coordinates_path) as hdf: roi_data_keys = [x[1:] for x in hdf.keys()]
missing_keys = [x for x in roi_data_keys if x not in [Keys.ROI_RECTANGLES.value, Keys.ROI_CIRCLES.value, Keys.ROI_POLYGONS.value]]
if len(missing_keys) > 0:
raise NoROIDataError(msg=f'The ROI data file {roi_coordinates_path} is corrupted. Missing the following keys: {missing_keys}', source=reset_video_ROIs.__name__)
rectangles_df = pd.read_hdf(path_or_buf=roi_coordinates_path, key=Keys.ROI_RECTANGLES.value)
circles_df = pd.read_hdf(path_or_buf=roi_coordinates_path, key=Keys.ROI_CIRCLES.value)
polygon_df = pd.read_hdf(path_or_buf=roi_coordinates_path, key=Keys.ROI_POLYGONS.value)
check_valid_dataframe(df=rectangles_df, source=f'{reset_video_ROIs.__name__} rectangles_df', required_fields=['Video'])
check_valid_dataframe(df=circles_df, source=f'{reset_video_ROIs.__name__} circles_df', required_fields=['Video'])
check_valid_dataframe(df=polygon_df, source=f'{reset_video_ROIs.__name__} polygon_df', required_fields=['Video'])
video_rectangle_roi_records = rectangles_df[rectangles_df["Video"] == video_name]
video_circle_roi_records = circles_df[circles_df["Video"] == video_name]
video_polygon_roi_records = polygon_df[polygon_df["Video"] == video_name]
video_roi_cnt = len(video_rectangle_roi_records) + len(video_circle_roi_records) + len(video_polygon_roi_records)
if video_roi_cnt == 0:
raise NoROIDataError(msg=f"Cannot delete ROIs for video {video_name}: no ROI records exist for {video_name}. Create ROIs for for video {video_name} first", source=reset_video_ROIs.__name__)
store = pd.HDFStore(roi_coordinates_path, mode="w")
store[Keys.ROI_RECTANGLES.value] = rectangles_df[rectangles_df["Video"] != video_name]
store[Keys.ROI_CIRCLES.value] = circles_df[circles_df["Video"] != video_name]
store[Keys.ROI_POLYGONS.value] = polygon_df[polygon_df["Video"] != video_name]
store.close()
stdout_trash(msg=f"Deleted ROI records for video {video_name}. Deleted rectangle count: {len(video_rectangle_roi_records)}, circles: {len(video_circle_roi_records)}, polygons: {len(video_polygon_roi_records)}.")
[docs]def get_image_from_label(tk_lbl: Label) -> np.ndarray:
""" Given a tkinter label with an image, retrieve image in array format"""
if not hasattr(tk_lbl, 'image'):
raise InvalidInputError(msg=f'The label {tk_lbl} does not have a valid image')
else:
tk_img = tk_lbl.image
pil_image = ImageTk.getimage(tk_img)
img = np.asarray(pil_image)
return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
[docs]def get_pose_for_roi_ui(pose_path: Union[str, os.PathLike],
video_path: Union[str, os.PathLike]) -> Union[None, np.ndarray]:
video_meta_data = get_video_meta_data(video_path=video_path, raise_error=False)
if video_meta_data is None:
VideoFileWarning(msg=f'Cannot plot pose on ROI as cannot read meta data for video {video_path}.', source=get_pose_for_roi_ui.__name__)
return None
file_readable = check_file_exist_and_readable(file_path=pose_path, raise_error=False)
if not file_readable:
NoFileFoundWarning(msg=f'Cannot plot tracking in ROI window: {pose_path} is unreadable.', source=get_pose_for_roi_ui.__name__)
return None
pose_df = read_df(file_path=pose_path, file_type='csv')
data_align = check_video_and_data_frm_count_align(video=video_path, data=pose_path, raise_error=False)
if not data_align:
FrameRangeWarning(msg=f'Cannot plot tracking in ROI window: The data contains {len(pose_path)} frames and the video has {video_meta_data["frame_count"]} frames', source=get_pose_for_roi_ui.__name__)
return None
pose_df = pose_df.drop(pose_df.columns[2::3], axis=1)
pose_data = pose_df.values.reshape(len(pose_df), round(len(pose_df.columns) / 2), 2).astype(np.int32)
valid_pose = check_valid_array(data=pose_data, source=f'{get_pose_for_roi_ui.__name__} pose_data', accepted_ndims=(3,), accepted_axis_0_shape=[video_meta_data['frame_count']], accepted_dtypes=Formats.INTEGER_DTYPES.value, raise_error=False)
if not valid_pose:
FrameRangeWarning(msg=f'Cannot plot tracking in ROI window: The pose data from path {pose_path} is not a 3D numeric array with length {video_meta_data["frame_count"]}', source=get_pose_for_roi_ui.__name__)
return pose_data
[docs]def insert_gridlines_on_roi_img(img: np.ndarray,
grid: List[Polygon],
color: Tuple[int, int, int],
thickness: int) -> np.ndarray:
"""
Draw the outlines of a set of grid polygons onto an image.
Each polygon's exterior is drawn as a closed polyline; useful for overlaying a tiling grid on an ROI frame.
.. image:: _static/img/insert_gridlines_on_roi_img.webp
:alt: Insert gridlines on roi img
:width: 600
:align: center
:param np.ndarray img: The image to draw on, as a (height, width, 3) BGR array.
:param List[Polygon] grid: A list of Shapely polygons whose exteriors are drawn. If ``None`` or empty, the image is returned unchanged.
:param Tuple[int, int, int] color: The BGR color of the gridlines.
:param int thickness: The line thickness in pixels.
:return: The image with the gridlines drawn on it.
:rtype: np.ndarray
"""
if grid is None or len(grid) == 0:
return img
else:
try:
for polygon in grid:
cords = np.round(np.array(polygon.exterior.coords)).astype(np.int32)
img = cv2.polylines(img=img, pts=[cords], isClosed=True, color=color, thickness=thickness, lineType=8)
return img
except Exception as e:
msg = f'Cannot draw gridlines: {e.args}'
raise InvalidInputError(msg=msg, source=f'{insert_gridlines_on_roi_img.__name__} draw')