Source code for simba.roi_tools.ROI_size_standardizer

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

import math
import os
from typing import Union

import numpy as np
import pandas as pd
from scipy.spatial import ConvexHull
from shapely.affinity import scale
from shapely.geometry import Polygon

from simba.mixins.config_reader import ConfigReader
from simba.mixins.feature_extraction_mixin import FeatureExtractionMixin
from simba.utils.errors import NoROIDataError, ParametersFileError
from simba.utils.printing import stdout_success


[docs]class ROISizeStandardizer(ConfigReader, FeatureExtractionMixin): """ Standardize ROI sizes according to a reference video. .. note:: Example: You select a "baseline" video, say that this baseline video has a pixel per millimeter of `10`. Say there are a further two videos in the project with ROIs, and these videos has pixels per millimeter of `9` and `11`. At runtime, the area of the rectangles, circles and polygons in the two additional videos get their ROI areas increased/decreased with 10% while the baseline video ROIs are unchanged. .. note:: See ROI tutorial on `GitHub <https://github.com/sgoldenlab/simba/blob/master/docs/roi_tutorial_new_2025.md>`_ or `ReadTheDocs https://simba-uw-tf-dev.readthedocs.io/en/latest/tutorials_rst/roi_tutorial_new_2025.html>`_ :param str Union[str, os.PathLike]: path to SimBA project config file in Configparser format. :param str reference_video: Name of baseline video without extension, e.g., `Video_1`. :example: >>> test = ROISizeStandardizer(config_path='/Users/simon/Desktop/envs/troubleshooting/DLC_2_Black_animals/project_folder/project_config.ini', reference_video='Together_1') >>> test.run() >>> test.save() """ def __init__(self, config_path: Union[str, os.PathLike], reference_video: str): ConfigReader.__init__(self, config_path=config_path) FeatureExtractionMixin.__init__(self) self.reference_video_name = reference_video self.read_roi_data() self.checks() def checks(self): if self.reference_video_name not in self.video_names_w_rois: raise NoROIDataError(msg=f"The reference video {self.reference_video_name} does not have any defined ROIs.", source=self.__class__.__name__) for video_name in self.video_names_w_rois: if video_name not in list(self.video_info_df["Video"]): raise ParametersFileError(msg=f"Found defined ROIs for video {video_name}, but this video does not have pixels per millimeter defined in the {self.video_info_path} file.", source=self.__class__.__name__) def compute_correction_factors(self): self.correction_factors = {} for video_name in self.video_names_w_rois: px_per_mm = self.read_video_info(video_name=video_name)[1] self.correction_factors[video_name] = round( (((px_per_mm / self.reference_px_per_mm) - 1) * 100), 4 ) def find_scale_factor(self, video_name: str, correction_factor: float): if self.correction_factors[video_name] < 0: return math.sqrt(1 - abs(correction_factor) / 100) else: return math.sqrt(1 + correction_factor / 100) def run(self): _, self.reference_px_per_mm, _ = self.read_video_info( video_name=self.reference_video_name ) self.compute_correction_factors() self.video_names_to_change = [ x for x in self.video_names_w_rois if x != self.reference_video_name ] updated_rectangles, updated_polygons, updated_circles = ( pd.DataFrame(columns=self.rectangles_df.columns), pd.DataFrame(columns=self.polygon_df.columns), pd.DataFrame(columns=self.circles_df.columns), ) for video_name in self.video_names_to_change: rectangles = self.rectangles_df[self.rectangles_df["Video"] == video_name] circles = self.circles_df[self.circles_df["Video"] == video_name] polygons = self.polygon_df[self.polygon_df["Video"] == video_name] # UPDATE RECTANGLES for idx, rectangle in rectangles.iterrows(): if self.correction_factors[video_name] != 0: vertices = np.array( [ rectangle["Tags"]["Top left tag"], rectangle["Tags"]["Top right tag"], rectangle["Tags"]["Bottom left tag"], rectangle["Tags"]["Bottom right tag"], ] ) scale_factor = self.find_scale_factor( video_name=video_name, correction_factor=self.correction_factors[video_name], ) new_vertices = np.unique( np.array( scale( Polygon(vertices), xfact=scale_factor, yfact=scale_factor, ).exterior.coords ).astype(np.int16), axis=0, ) top_left = np.min(new_vertices, axis=0) bottom_right = np.max(new_vertices, axis=0) rectangle["topLeftX"], rectangle["topLeftY"] = ( top_left[0], top_left[1], ) rectangle["Bottom_right_X"], rectangle["Bottom_right_Y"] = ( bottom_right[0], bottom_right[1], ) rectangle["width"], rectangle["height"] = int( rectangle["Bottom_right_X"] - rectangle["topLeftX"] ), int(rectangle["Bottom_right_Y"] - rectangle["topLeftY"]) ( rectangle["Tags"]["Top left tag"], rectangle["Tags"]["Top right tag"], ) = (int(rectangle["topLeftX"]), int(rectangle["topLeftY"])), ( int(rectangle["Bottom_right_X"]), int(rectangle["topLeftY"]), ) ( rectangle["Tags"]["Bottom left tag"], rectangle["Tags"]["Bottom right tag"], ) = (rectangle["topLeftX"], rectangle["Bottom_right_Y"]), ( rectangle["Bottom_right_X"], rectangle["Bottom_right_Y"], ) rectangle["Tags"]["Left tag"] = ( int(rectangle["topLeftX"]), int(rectangle["topLeftY"] + rectangle["height"] / 2), ) rectangle["Tags"]["Right tag"] = ( int(rectangle["topLeftX"] + rectangle["width"]), int(rectangle["topLeftY"] + rectangle["height"] / 2), ) rectangle["Tags"]["Top tag"] = ( int(rectangle["topLeftX"] + rectangle["width"] / 2), int(rectangle["topLeftY"]), ) rectangle["Tags"]["Bottom tag"] = ( int(rectangle["topLeftX"] + rectangle["width"] / 2), int(rectangle["topLeftY"] + rectangle["height"]), ) updated_rectangles.loc[idx] = rectangle # UPDATE POLYGONS for idx, polygon in polygons.iterrows(): if self.correction_factors[video_name] != 0: scale_factor = self.find_scale_factor( video_name=video_name, correction_factor=self.correction_factors[video_name], ) new_vertices = np.array( scale( Polygon(polygon["vertices"]), xfact=scale_factor, yfact=scale_factor, ).exterior.coords ) new_vertices = ConvexHull(new_vertices).points.astype(np.int32) polygon["vertices"] = new_vertices for cnt, i in enumerate(polygon["vertices"]): polygon["Tags"][f"Tag_{cnt}"] = i updated_polygons.loc[idx] = polygon # UPDATE CIRCLES for idx, circle in circles.iterrows(): if self.correction_factors[video_name] != 0: circle["radius"] = int( circle["radius"] * (self.correction_factors[video_name] / 100) ) circle["Tags"]["Border tag"] = ( int(circle["centerX"] - circle["radius"]), circle["centerY"], ) updated_circles.loc[idx] = circle self.rectangles_df.update(updated_rectangles) self.polygon_df.update(updated_polygons) self.circles_df.update(updated_circles) def save(self): store = pd.HDFStore(self.roi_coordinates_path, mode="w") store["rectangles"] = self.rectangles_df store["circleDf"] = self.circles_df store["polygons"] = self.polygon_df store.close() stdout_success(msg=f"ROI size definitions standardized according to pixels per millimeter in video {self.reference_video_name}", source=self.__class__.__name__)
# test = ROISizeStandardizer(config_path='/Users/simon/Desktop/envs/troubleshooting/DLC_2_Black_animals/project_folder/project_config.ini', reference_video='Together_1') # test.run() # test.save() # # test = ROISizeStandardizer(config_path='/Users/simon/Desktop/envs/troubleshooting/two_black_animals_14bp/project_folder/project_config.ini', reference_video='Together_1') # test.run() # test.save()