Skip to content

Commit

Permalink
video tools
Browse files Browse the repository at this point in the history
  • Loading branch information
sronilsson committed Jun 1, 2024
1 parent fecfa62 commit 8eb8aa6
Show file tree
Hide file tree
Showing 17 changed files with 607 additions and 207 deletions.
Binary file added docs/_static/img/convert_to_mp4_1.webp
Binary file not shown.
Binary file added docs/_static/img/convert_to_mp4_2.webp
Binary file not shown.
Binary file added docs/_static/img/create_uniform_img.webp
Binary file not shown.
Binary file added docs/_static/img/img_emd.webp
Binary file not shown.
Binary file added docs/_static/img/img_matrix_mse.webp
Binary file not shown.
Binary file added docs/_static/img/pad_img_stack.webp
Binary file not shown.
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
myst_url_schemes = ["http", "https"]
extensions = ['sphinx.ext.napoleon',
'sphinx.ext.imgmath',
'sphinx.ext.mathjax'
'sphinx.ext.autodoc',
'sphinx.ext.todo',
'sphinx.ext.viewcode',
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ opencv-python == 3.4.5.20; python_version=="3.6"
opencv-python == 4.8.1.78; python_version>="3.9"
pandas==0.25.3;python_version=="3.6"
pandas==2.2.2;python_version>="3.9"
scikit-image == 0.14.2; python_version=="3.6"
scikit-image == 0.17.2;python_version=="3.6"
scikit-image == 0.23.2; python_version>="3.9"
scipy== 1.5.4; python_version=="3.6"
scipy== 1.13.0; python_version>="3.9"
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

setuptools.setup(
name="Simba-UW-tf-dev",
version="1.94.3",
version="1.94.6",
author="Simon Nilsson, Jia Jie Choong, Sophia Hwang",
author_email="[email protected]",
description="Toolkit for computer classification of behaviors in experimental animals",
Expand Down
486 changes: 387 additions & 99 deletions simba/mixins/image_mixin.py

Large diffs are not rendered by default.

123 changes: 114 additions & 9 deletions simba/mixins/statistics_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
from typing_extensions import Literal

import numpy as np
from numba import (bool_, float32, float64, int8, jit, njit, objmode, optional,
prange, typed, types)
from numba import (bool_, float32, float64, int8, jit, njit, objmode, optional, prange, typed, types)
from scipy import stats
from scipy.stats.distributions import chi2
from sklearn.covariance import EllipticEnvelope
Expand Down Expand Up @@ -251,9 +250,7 @@ def rolling_cohens_d(data: np.ndarray, time_windows: np.ndarray, fps: float) ->
results = np.full((data.shape[0], time_windows.shape[0]), 0.0)
for i in prange(time_windows.shape[0]):
window_size = int(time_windows[i] * fps)
data_split = np.split(
data, list(range(window_size, data.shape[0], window_size))
)
data_split = np.split(data, list(range(window_size, data.shape[0], window_size)))
for j in prange(1, len(data_split)):
window_start = int(window_size * j)
window_end = int(window_start + window_size)
Expand Down Expand Up @@ -2987,9 +2984,7 @@ def sokal_sneath(

@staticmethod
@njit([(float32[:, :], float32[:, :]), (float32[:, :], types.misc.Omitted(None))])
def bray_curtis_dissimilarity(
x: np.ndarray, w: Optional[np.ndarray] = None
) -> np.ndarray:
def bray_curtis_dissimilarity(x: np.ndarray, w: Optional[np.ndarray] = None) -> np.ndarray:
"""
Jitted compute of the Bray-Curtis dissimilarity matrix between samples based on feature values.
Expand Down Expand Up @@ -3661,7 +3656,7 @@ def dunn_index(x: np.ndarray, y: np.ndarray) -> float:
Uses Euclidean distances.
:param np.ndarray x: 2D array representing the data points. Shape (n_samples, n_features).
:param np.ndarray y: 2D array representing cluster labels for each data point. Shape (n_samples,).
:param np.ndarray y: 1D array representing cluster labels for each data point. Shape (n_samples,).
:return float: The Dunn index value
:example:
Expand Down Expand Up @@ -3932,6 +3927,116 @@ def adjusted_mutual_info(x: np.ndarray, y: np.ndarray) -> float:
)
return adjusted_mutual_info_score(labels_true=x, labels_pred=y)

@staticmethod
@jit(nopython=True)
def czebyshev_distance(sample_1: np.ndarray, sample_2: np.ndarray) -> float:
"""
Calculate the Czebyshev distance between two N-dimensional samples.
The Czebyshev distance is defined as the maximum absolute difference
between the corresponding elements of the two arrays.
.. note::
Normalize arrays sample_1 and sample_2 before passing it to ensure accurate results.
.. math::
D_\infty(p, q) = \max_i \left| p_i - q_i \right|
:param np.ndarray sample_1: The first sample, an N-dimensional NumPy array.
:param np.ndarray sample_2: The second sample, an N-dimensional NumPy array.
:return float: The Czebyshev distance between the two samples.
:example:
>>> sample_1 = np.random.randint(0, 10, (10000,100))
>>> sample_2 = np.random.randint(0, 10, (10000,100))
>>> Statistics.czebyshev_distance(sample_1=sample_1, sample_2=sample_2)
"""

c = 0.0
for idx in np.ndindex(sample_1.shape):
c = max((c, np.abs(sample_1[idx] - sample_2[idx])))
return c

@staticmethod
@njit(["(float32[:, :], float64[:], int64)", ])
def sliding_czebyshev_distance(x: np.ndarray, window_sizes: np.ndarray, sample_rate: float) -> np.ndarray:
"""
Calculate the sliding Chebyshev distance for a given signal with different window sizes.
This function computes the sliding Chebyshev distance for a signal `x` using
different window sizes specified by `window_sizes`. The Chebyshev distance measures
the maximum absolute difference between the corresponding elements of two signals.
.. note::
Normalize array x before passing it to ensure accurate results.
.. math::
D_\infty(p, q) = \max_i \left| p_i - q_i \right|
:param np.ndarray x: Input signal, a 2D array with shape (n_samples, n_features).
:param np.ndarray window_sizes: Array containing window sizes for sliding computation.
:param float sample_rate: Sampling rate of the signal.
:return np.ndarray: 2D array of Chebyshev distances for each window size and position.
:example:
>>> sample_1 = np.random.randint(0, 10, (200,5)).astype(np.float32)
>>> sample_2 = np.random.randint(0, 10, (10000,100))
>>> Statistics.sliding_czebyshev_distance(x=sample_1, window_sizes=np.array([1.0, 2.0]), sample_rate=10.0)
"""

result = np.full((x.shape[0], window_sizes.shape[0]), 0.0)
for i in range(window_sizes.shape[0]):
window_size = int(window_sizes[i] * sample_rate)
for l, r in zip(range(0, x.shape[0] + 1), range(window_size, x.shape[0] + 1)):
sample, c = x[l:r, :], 0.0
for j in range(sample.shape[1]):
c = max(c, (np.abs(np.min(sample[:, j]) - np.max(sample[:, j]))))
result[r - 1, i] = c
return result

@staticmethod
@njit(["(int64[:], int64[:], float64[:])", "(int64[:], int64[:], types.misc.Omitted(None))",
"(int64[:, :], int64[:, :], float64[:])", "(int64[:, :], int64[:, :], types.misc.Omitted(None))"])
def sokal_michener(x: np.ndarray, y: np.ndarray, w: Optional[np.ndarray] = None) -> float:
"""
Jitted compute of the Sokal-Michener dissimilarity between two binary vectors or matrices.
Higher values indicate more dissimilar vectors or matrices, while lower values indicate more similar vectors or matrices.
The Sokal-Michener dissimilarity is a measure of dissimilarity between two sets
based on the presence or absence of similar attributes. This implementation supports weighted dissimilarity.
.. note::
Adapted from `umap <https://github.com/lmcinnes/umap/blob/e7f2fb9e5e772edd5c8f38612365ec6a35a54373/umap/distances.py#L468>`_.
.. math::
D(x, y) = \frac{2 \cdot \sum_{i} w_i \cdot \mathbb{1}(x_i \neq y_i)}{N + \sum_{i} w_i \cdot \mathbb{1}(x_i \neq y_i)}
where:
- :math:`x` and :math:`y` are the binary vectors or matrices.
- :math:`w_i` is the weight for the i-th element.
- :math:`\mathbb{1}(x_i \neq y_i)` is an indicator function that is 1 if :math:`x_i \neq y_i` and 0 otherwise.
- :math:`N` is the total number of elements in :math:`x` or :math:`y`.
:param np.ndarray x: First binary vector or matrix.
:param np.ndarray y: Second binary vector or matrix.
:param Optional[np.ndarray] w: Optional weight vector. If None, all weights are considered as 1.
:return float: Sokal-Michener dissimilarity between `x` and `y`.
:example:
>>> x = np.random.randint(0, 2, (200,))
>>> y = np.random.randint(0, 2, (200,))
>>> sokal_michener = Statistics.sokal_michener(x=x, y=y)
"""

if w is None:
w = np.ones(x.shape[0]).astype(np.float64)
unequal_cnt = 0.0
for i in np.ndindex(x.shape):
x_i, y_i = x[i], y[i]
if x_i != y_i:
unequal_cnt += 1 * w[i[0]]
return (2.0 * unequal_cnt) / (x.size + unequal_cnt)

# sample_1 = np.random.random_integers(low=1, high=2, size=(10, 50)).astype(np.float64)
# sample_2 = np.random.random_integers(low=7, high=20, size=(10, 50)).astype(np.float64)
Expand Down
3 changes: 2 additions & 1 deletion simba/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ opencv-python == 3.4.5.20;python_version=="3.6"
opencv-python;python_version>="3.9"
pandas==0.25.3;python_version=="3.6"
pandas;python_version>="3.9"
scikit-image
scikit-image == 0.17.2;python_version=="3.6"
scikit-image;python_version>="3.9"
scipy
kaleido
seaborn == 0.11.0
Expand Down
19 changes: 14 additions & 5 deletions simba/ui/pop_ups/video_processing_pop_up.py
Original file line number Diff line number Diff line change
Expand Up @@ -2366,10 +2366,12 @@ class Convert2MP4PopUp(PopUpMixin):
def __init__(self):
super().__init__(title="CONVERT VIDEOS TO MP4")
settings_frm = CreateLabelFrameWithIcon(parent=self.main_frm, header="SETTINGS", icon_name=Keys.DOCUMENTATION.value, icon_link=Links.VIDEO_TOOLS.value)
self.MP4_CODEC_LK = {'HEVC (H.265)': 'libx265', 'H.264 (AVC)': 'libx264', 'VP9': 'vp9', 'Guranteed powerpoint compatible': 'powerpoint'}
self.quality_dropdown = DropDownMenu(settings_frm, "OUTPUT VIDEO QUALITY:", list(range(10, 110, 10)), labelwidth=25)
self.MP4_CODEC_LK = {'HEVC (H.265)': 'libx265', 'H.264 (AVC)': 'libx264', 'VP9': 'vp9', 'GPU (h264_cuvid)': 'h264_cuvid', 'Guranteed powerpoint compatible': 'powerpoint'}
self.cpu_codec_qualities = list(range(10, 110, 10))
self.gpu_codec_qualities = ['Low', 'Medium', 'High']
self.quality_dropdown = DropDownMenu(settings_frm, "OUTPUT VIDEO QUALITY:", self.cpu_codec_qualities, labelwidth=25)
self.quality_dropdown.setChoices(60)
self.codec_dropdown = DropDownMenu(settings_frm, "COMPRESSION CODEC:", list(self.MP4_CODEC_LK.keys()), labelwidth=25)
self.codec_dropdown = DropDownMenu(settings_frm, "COMPRESSION CODEC:", list(self.MP4_CODEC_LK.keys()), labelwidth=25, com=self.update_quality_dropdown)
self.codec_dropdown.setChoices('HEVC (H.265)')
settings_frm.grid(row=0, column=0, sticky=NW)
self.quality_dropdown.grid(row=0, column=0, sticky=NW)
Expand All @@ -2388,8 +2390,15 @@ def __init__(self):
multiple_video_frm.grid(row=2, column=0, sticky=NW)
self.selected_video_dir.grid(row=0, column=0, sticky=NW)
multiple_video_run.grid(row=1, column=0, sticky=NW)
self.main_frm.mainloop()

#self.main_frm.mainloop()
def update_quality_dropdown(self, k):
self.quality_dropdown.popupMenu['menu'].delete(0, 'end')
if k == 'GPU (h264_cuvid)': option_lst = self.gpu_codec_qualities
else: option_lst = self.cpu_codec_qualities
for option in option_lst:
self.quality_dropdown.popupMenu['menu'].add_command(label=option, command=lambda value=option: self.quality_dropdown.dropdownvar.set(value))
self.quality_dropdown.setChoices(option_lst[0])

def run(self, multiple: bool):
if not multiple:
Expand All @@ -2399,7 +2408,7 @@ def run(self, multiple: bool):
video_path = self.selected_video_dir.folder_path
check_if_dir_exists(in_dir=video_path, source=self.__class__.__name__)
codec = self.MP4_CODEC_LK[self.codec_dropdown.getChoices()]
quality = int(self.quality_dropdown.getChoices())
quality = self.quality_dropdown.getChoices()
threading.Thread(target=convert_to_mp4(path=video_path, codec=codec, quality=quality))

class Convert2AVIPopUp(PopUpMixin):
Expand Down
44 changes: 37 additions & 7 deletions simba/utils/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
check_instance, check_int, check_str,
check_that_column_exist,
check_that_hhmmss_start_is_before_end,
check_valid_array, check_valid_dataframe)
check_valid_array, check_valid_dataframe, check_if_valid_rgb_tuple)
from simba.utils.enums import ConfigKey, Dtypes, Keys, Options
from simba.utils.errors import (BodypartColumnNotFoundError, CountError,
InvalidFileTypeError, InvalidInputError,
Expand Down Expand Up @@ -295,19 +295,19 @@ def create_color_palettes(
return colorListofList


def create_color_palette(
pallete_name: str,
increments: int,
as_rgb_ratio: Optional[bool] = False,
as_hex: Optional[bool] = False,
) -> list:
def create_color_palette(pallete_name: str,
increments: int,
as_rgb_ratio: Optional[bool] = False,
as_hex: Optional[bool] = False,
as_int: Optional[bool] = False) -> List[Union[str, float]]:
"""
Create a list of colors in RGB from specified color palette.
:param str pallete_name: Palette name (e.g., ``jet``)
:param int increments: Numbers of colors in the color palette to create.
:param Optional[bool] as_rgb_ratio: Return RGB to ratios. Default: False
:param Optional[bool] as_hex: Return values as HEX. Default: False
:param Optional[bool] as_int: Return RGB values as integers rather than float if possible. Default: False
.. note::
If **both** as_rgb_ratio and as_hex, HEX values will be returned.
Expand All @@ -330,13 +330,43 @@ def create_color_palette(
rgb = list((cmap(i)[:3]))
if not as_rgb_ratio:
rgb = [i * 255 for i in rgb]
if as_int:
rgb = [int(x) for x in rgb]
rgb.reverse()
if as_hex:
rgb = matplotlib.colors.to_hex(rgb)
color_lst.append(rgb)
return color_lst


def interpolate_color_palette(start_color: Tuple[int, int, int],
end_color: Tuple[int, int, int],
n: Optional[int] = 10):
"""
Generate a list of colors interpolated between two passed RGB colors.
:param start_color: Tuple of RGB values for the start color.
:param end_color: Tuple of RGB values for the end color.
:param n: Number of colors to generate.
:return: List of interpolated RGB colors.
:example:
>>> red, black = (255, 0, 0), (0, 0, 0)
>>> colors = interpolate_color_palette(start_color=red, end_color=black, n = 10)
"""

check_if_valid_rgb_tuple(data=start_color)
check_if_valid_rgb_tuple(data=end_color)
check_int(name=f'{interpolate_color_palette.__name__} n', value=n, min_value=3)
return [(
int(start_color[0] + (end_color[0] - start_color[0]) * i / (n - 1)),
int(start_color[1] + (end_color[1] - start_color[1]) * i / (n - 1)),
int(start_color[2] + (end_color[2] - start_color[2]) * i / (n - 1))
) for i in range(n)]




def smooth_data_savitzky_golay(
config: configparser.ConfigParser,
file_path: Union[str, os.PathLike],
Expand Down
1 change: 1 addition & 0 deletions simba/utils/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class Formats(Enum):
MP4_CODEC = "mp4v"
AVI_CODEC = "XVID"
BATCH_CODEC = "libx264"
NUMERIC_DTYPES = (np.float32, np.float64, np.int64, np.int32, np.int8, np.int, np.float, int, float)
LABELFRAME_HEADER_FORMAT = ("Helvetica", 12, "bold")
LABELFRAME_HEADER_CLICKABLE_FORMAT = ("Helvetica", 12, "bold", "underline")
LABELFRAME_HEADER_CLICKABLE_COLOR = f"#{5:02x}{99:02x}{193:02x}"
Expand Down
4 changes: 1 addition & 3 deletions simba/utils/read_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -1348,9 +1348,7 @@ def copy_multiple_videos_to_project(
try:
os.symlink(file_path, dest1)
except OSError:
raise PermissionError(
msg="Symbolic link privilege not held. Try running SimBA in terminal opened in admin mode"
)
raise PermissionError(msg="Symbolic link privilege not held. Try running SimBA in terminal opened in admin mode")
timer.stop_timer()
if not symlink:
print(
Expand Down
Loading

0 comments on commit 8eb8aa6

Please sign in to comment.