diff --git a/.conda/build.sh b/.conda/build.sh index 1ea1d4df0..86ab5af73 100644 --- a/.conda/build.sh +++ b/.conda/build.sh @@ -16,8 +16,8 @@ python setup.py install --single-version-externally-managed --record=record.txt # Copy the activate scripts to $PREFIX/etc/conda/activate.d. # This will allow them to be run on environment activation. -export CHANGE=activate - -mkdir -p "${PREFIX}/etc/conda/${CHANGE}.d" -ls "${RECIPE_DIR}" -cp "${RECIPE_DIR}/${PKG_NAME}_${CHANGE}.sh" "${PREFIX}/etc/conda/${CHANGE}.d/${PKG_NAME}_${CHANGE}.sh" +for CHANGE in "activate" "deactivate" +do + mkdir -p "${PREFIX}/etc/conda/${CHANGE}.d" + cp "${RECIPE_DIR}/${PKG_NAME}_${CHANGE}.sh" "${PREFIX}/etc/conda/${CHANGE}.d/${PKG_NAME}_${CHANGE}.sh" +done \ No newline at end of file diff --git a/.conda/sleap_activate.sh b/.conda/sleap_activate.sh index feebadd60..885879a89 100644 --- a/.conda/sleap_activate.sh +++ b/.conda/sleap_activate.sh @@ -1,4 +1,6 @@ #!/bin/sh +# Remember the old library path for when we deactivate +export SLEAP_OLD_LD_LIBRARY_PATH=$LD_LIBRARY_PATH # Help CUDA find GPUs! export LD_LIBRARY_PATH=$CONDA_PREFIX/lib:$LD_LIBRARY_PATH \ No newline at end of file diff --git a/.conda/sleap_deactivate.sh b/.conda/sleap_deactivate.sh new file mode 100644 index 000000000..857c0f49c --- /dev/null +++ b/.conda/sleap_deactivate.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +# Reset to the old library path for when deactivating the environment +export LD_LIBRARY_PATH=$SLEAP_OLD_LD_LIBRARY_PATH \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 24c20c513..8c95f28dc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -28,7 +28,7 @@ Please include information about how you installed. - OS: - Version(s): - + - SLEAP installation method (listed [here](https://sleap.ai/installation.html#)): - [ ] [Conda from package](https://sleap.ai/installation.html#conda-package) - [ ] [Conda from source](https://sleap.ai/installation.html#conda-from-source) diff --git a/docs/conf.py b/docs/conf.py index b1e79fcc3..572e73ea0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,7 @@ copyright = f"2019–{date.today().year}, Talmo Lab" # The short X.Y version -version = "1.3.2" +version = "1.3.3" # Get the sleap version # with open("../sleap/version.py") as f: @@ -36,7 +36,7 @@ # version = re.search("\d.+(?=['\"])", version_file).group(0) # Release should be the full branch name -release = "v1.3.2" +release = "v1.3.3" html_title = f"SLEAP ({release})" html_short_title = "SLEAP" diff --git a/docs/guides/cli.md b/docs/guides/cli.md index 35ea52171..6a9d05806 100644 --- a/docs/guides/cli.md +++ b/docs/guides/cli.md @@ -389,6 +389,9 @@ optional arguments: --distinctly_color DISTINCTLY_COLOR Specify how to color instances. Options include: "instances", "edges", and "nodes" (default: "instances") + --background BACKGROUND + Specify the type of background to be used to save the videos. + Options: original, black, white and grey. (default: "original") ``` ## Debugging diff --git a/docs/installation.md b/docs/installation.md index c311f0851..eea65cc31 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -137,13 +137,13 @@ SLEAP can be installed three different ways: via {ref}`conda package> $CONDA_PREFIX/etc/conda/activate.d/sleap_activate.sh +echo 'export SLEAP_OLD_LD_LIBRARY_PATH=$LD_LIBRARY_PATH' >> $CONDA_PREFIX/etc/conda/activate.d/sleap_activate.sh echo 'export LD_LIBRARY_PATH=$CONDA_PREFIX/lib:$LD_LIBRARY_PATH' >> $CONDA_PREFIX/etc/conda/activate.d/sleap_activate.sh source $CONDA_PREFIX/etc/conda/activate.d/sleap_activate.sh ``` -These commands only need to be run once and will subsequently run automatically upon activating your `sleap` environment. + +This will set the environment variable `LD_LIBRARY_PATH` each time the environment is activated. The environment variable will remain set in the current terminal even if we deactivate the environment. Although not strictly necessary, if you would also like the environment variable to be reset to the original value when deactivating the environment, we can run the following commands: +```bash +mkdir -p $CONDA_PREFIX/etc/conda/deactivate.d +echo '#!/bin/sh' >> $CONDA_PREFIX/etc/conda/deactivate.d/sleap_deactivate.sh +echo 'export LD_LIBRARY_PATH=$SLEAP_OLD_LD_LIBRARY_PATH' >> $CONDA_PREFIX/etc/conda/deactivate.d/sleap_deactivate.sh +``` + +These commands only need to be run once and will subsequently run automatically upon [de]activating your `sleap` environment. ```` ## Upgrading and uninstalling diff --git a/docs/notebooks/Data_structures.ipynb b/docs/notebooks/Data_structures.ipynb index a3337186c..ff0ea2d3d 100644 --- a/docs/notebooks/Data_structures.ipynb +++ b/docs/notebooks/Data_structures.ipynb @@ -56,7 +56,7 @@ "source": [ "# This should take care of all the dependencies on colab:\n", "!pip uninstall -qqq -y opencv-python opencv-contrib-python\n", - "!pip install -qqq sleap==1.3.1111\n", + "!pip install -qqq \"sleap[pypi]>=1.3.3\"\n", "\n", "# But to do it locally, we'd recommend the conda package (available on Windows + Linux):\n", "# conda create -n sleap -c sleap -c conda-forge -c nvidia sleap" diff --git a/docs/notebooks/Interactive_and_realtime_inference.ipynb b/docs/notebooks/Interactive_and_realtime_inference.ipynb index 5d9c7e33d..4a3b612a2 100644 --- a/docs/notebooks/Interactive_and_realtime_inference.ipynb +++ b/docs/notebooks/Interactive_and_realtime_inference.ipynb @@ -60,7 +60,7 @@ "source": [ "# This should take care of all the dependencies on colab:\n", "!pip uninstall -qqq -y opencv-python opencv-contrib-python\n", - "!pip install -qqq sleap==1.3.1\n", + "!pip install -qqq \"sleap[pypi]>=1.3.3\"\n", "\n", "\n", "# But to do it locally, we'd recommend the conda package (available on Windows + Linux):\n", diff --git a/docs/notebooks/Interactive_and_resumable_training.ipynb b/docs/notebooks/Interactive_and_resumable_training.ipynb index be82c19a5..f30f036f3 100644 --- a/docs/notebooks/Interactive_and_resumable_training.ipynb +++ b/docs/notebooks/Interactive_and_resumable_training.ipynb @@ -62,7 +62,7 @@ "source": [ "# This should take care of all the dependencies on colab:\n", "!pip uninstall -qqq -y opencv-python opencv-contrib-python\n", - "!pip install -qqq sleap==1.3.1\n", + "!pip install -qqq \"sleap[pypi]>=1.3.3\"\n", "\n", "\n", "# But to do it locally, we'd recommend the conda package (available on Windows + Linux):\n", diff --git a/docs/notebooks/Model_evaluation.ipynb b/docs/notebooks/Model_evaluation.ipynb index 62bf3935a..41ca6568c 100644 --- a/docs/notebooks/Model_evaluation.ipynb +++ b/docs/notebooks/Model_evaluation.ipynb @@ -40,7 +40,7 @@ ], "source": [ "!pip uninstall -qqq -y opencv-python opencv-contrib-python\n", - "!pip install -qqq sleap==1.3.1\n", + "!pip install -qqq \"sleap[pypi]>=1.3.3\"\n", "!apt -qq install tree\n", "!wget -q https://storage.googleapis.com/sleap-data/reference/flies13/td_fast.210505_012601.centered_instance.n%3D1800.zip\n", "!unzip -qq -o -d \"td_fast.210505_012601.centered_instance.n=1800\" \"td_fast.210505_012601.centered_instance.n=1800.zip\"" diff --git a/docs/notebooks/Post_inference_tracking.ipynb b/docs/notebooks/Post_inference_tracking.ipynb index 106c7ae88..239176bdb 100644 --- a/docs/notebooks/Post_inference_tracking.ipynb +++ b/docs/notebooks/Post_inference_tracking.ipynb @@ -61,7 +61,7 @@ "source": [ "# This should take care of all the dependencies on colab:\n", "!pip uninstall -qqq -y opencv-python opencv-contrib-python\n", - "!pip install -qqq sleap==1.3.1\n", + "!pip install -qqq \"sleap[pypi]>=1.3.3\"\n", "\n", "# But to do it locally, we'd recommend the conda package (available on Windows + Linux):\n", "# conda create -n sleap -c sleap -c conda-forge -c nvidia sleap" diff --git a/docs/notebooks/Training_and_inference_on_an_example_dataset.ipynb b/docs/notebooks/Training_and_inference_on_an_example_dataset.ipynb index 22c4193f7..b0211bbca 100644 --- a/docs/notebooks/Training_and_inference_on_an_example_dataset.ipynb +++ b/docs/notebooks/Training_and_inference_on_an_example_dataset.ipynb @@ -62,7 +62,7 @@ ], "source": [ "!pip uninstall -qqq -y opencv-python opencv-contrib-python\n", - "!pip install -qqq sleap==1.3.1" + "!pip install -qqq \"sleap[pypi]>=1.3.3\"" ] }, { diff --git a/docs/notebooks/Training_and_inference_using_Google_Drive.ipynb b/docs/notebooks/Training_and_inference_using_Google_Drive.ipynb index 96374982d..1e871861d 100644 --- a/docs/notebooks/Training_and_inference_using_Google_Drive.ipynb +++ b/docs/notebooks/Training_and_inference_using_Google_Drive.ipynb @@ -59,7 +59,7 @@ ], "source": [ "!pip uninstall -qqq -y opencv-python opencv-contrib-python\n", - "!pip install -qqq sleap==1.3.1" + "!pip install -qqq \"sleap[pypi]>=1.3.3\"" ] }, { diff --git a/pypi_requirements.txt b/pypi_requirements.txt index b18637c37..33f419c9c 100644 --- a/pypi_requirements.txt +++ b/pypi_requirements.txt @@ -12,24 +12,34 @@ jsonpickle==1.2 networkx numpy>=1.19.5,<1.23.0 opencv-python>=4.2.0,<=4.6.0 -# opencv-python-headless>=4.2.0.34,<=4.5.5.62 pandas pillow>=8.3.1,<=8.4.0 psutil pykalman==0.9.5 PySide2>=5.13.2,<=5.14.1; platform_machine != 'arm64' PySide6; sys_platform == 'darwin' and platform_machine == 'arm64' -python-rapidjson +# Otherwise error: Microsoft Visual C++ 14.0 is required. +python-rapidjson <=1.10; sys_platform == 'win32' +python-rapidjson; sys_platform != 'win32' pyyaml pyzmq qtpy>=2.0.1 rich==10.16.1 -imageio<=2.15.0 imgaug==0.4.0 scipy>=1.4.1,<=1.9.0 scikit-image scikit-learn ==1.0.* scikit-video seaborn -tensorflow -tensorflow-hub +tensorflow>=2.6.3,<2.9; platform_machine != 'arm64' +tensorflow-hub<=0.14.0 +# These dependencies are untested since we do not offer a wheel for apple silicon atm. +tensorflow-macos==2.9.2; sys_platform == 'darwin' and platform_machine == 'arm64' +tensorflow-metal==0.5.0; sys_platform == 'darwin' and platform_machine == 'arm64' + +# Dependencies of dependencies +# google-auth 2.23.0 has requirement urllib3<2.0 +urllib3<2.0 # Not a 'noticed' runtime-dependency +# tensorboard 2.11.2 has requirement protobuf<4,>=3.9.2 +# tensorflow 2.11.0 has requirement protobuf<3.20,>=3.9.2 +protobuf<3.20 # Makes GUI work in windows \ No newline at end of file diff --git a/sleap/config/labeled_clip_form.yaml b/sleap/config/labeled_clip_form.yaml index be0d64829..9236ad42b 100644 --- a/sleap/config/labeled_clip_form.yaml +++ b/sleap/config/labeled_clip_form.yaml @@ -18,6 +18,10 @@ main: label: Use GUI Visual Settings (colors, line widths) type: bool default: true + - name: background + label: Video Background + type: list + options: original,black,white,grey - name: open_when_done label: Open When Done Saving type: bool diff --git a/sleap/config/training_editor_form.yaml b/sleap/config/training_editor_form.yaml index d10b840a0..eabfc3940 100644 --- a/sleap/config/training_editor_form.yaml +++ b/sleap/config/training_editor_form.yaml @@ -661,6 +661,7 @@ optimization: label: Batch Size name: optimization.batch_size type: int + range: 1,512 - default: 100 help: Maximum number of epochs to train for. Training can be stopped manually or automatically if early stopping is enabled and a plateau is detected. label: Epochs diff --git a/sleap/gui/commands.py b/sleap/gui/commands.py index 698eed756..78a8c2a31 100644 --- a/sleap/gui/commands.py +++ b/sleap/gui/commands.py @@ -1295,6 +1295,7 @@ def do_action(context: CommandContext, params: dict): frames=list(params["frames"]), fps=params["fps"], color_manager=params["color_manager"], + background=params["background"], show_edges=params["show edges"], edge_is_wedge=params["edge_is_wedge"], marker_size=params["marker size"], @@ -1354,6 +1355,7 @@ def ask(context: CommandContext, params: dict) -> bool: params["fps"] = export_options["fps"] params["scale"] = export_options["scale"] params["open_when_done"] = export_options["open_when_done"] + params["background"] = export_options["background"] params["crop"] = None diff --git a/sleap/gui/overlays/base.py b/sleap/gui/overlays/base.py index d27b069ac..879d12810 100644 --- a/sleap/gui/overlays/base.py +++ b/sleap/gui/overlays/base.py @@ -71,7 +71,6 @@ def remove_from_scene(self): except RuntimeError as e: # Internal C++ object (PySide2.QtWidgets.QGraphicsPathItem) already deleted. logger.debug(e) - pass # Stop tracking the items after they been removed from the scene self.items = [] diff --git a/sleap/info/metrics.py b/sleap/info/metrics.py index 2ac61d339..5bec077e4 100644 --- a/sleap/info/metrics.py +++ b/sleap/info/metrics.py @@ -10,75 +10,6 @@ from sleap.io.dataset import Labels -def matched_instance_distances( - labels_gt: Labels, - labels_pr: Labels, - match_lists_function: Callable, - frame_range: Optional[range] = None, -) -> Tuple[List[int], np.ndarray, np.ndarray, np.ndarray]: - - """ - Distances between ground truth and predicted nodes over a set of frames. - - Args: - labels_gt: the `Labels` object with ground truth data - labels_pr: the `Labels` object with predicted data - match_lists_function: function for determining corresponding instances - Takes two lists of instances and returns "sorted" lists. - frame_range (optional): range of frames for which to compare data - If None, we compare every frame in labels_gt with corresponding - frame in labels_pr. - Returns: - Tuple: - * frame indices map: instance idx (for other matrices) -> frame idx - * distance matrix: (instances * nodes) - * ground truth points matrix: (instances * nodes * 2) - * predicted points matrix: (instances * nodes * 2) - """ - - frame_idxs = [] - points_gt = [] - points_pr = [] - for lf_gt in labels_gt.find(labels_gt.videos[0]): - frame_idx = lf_gt.frame_idx - - # Get instances from ground truth/predicted labels - instances_gt = lf_gt.instances - lfs_pr = labels_pr.find(labels_pr.videos[0], frame_idx=frame_idx) - if len(lfs_pr): - instances_pr = lfs_pr[0].instances - else: - instances_pr = [] - - # Sort ground truth and predicted instances. - # We'll then compare points between corresponding items in lists. - # We can use different "match" functions depending on what we want. - sorted_gt, sorted_pr = match_lists_function(instances_gt, instances_pr) - - # Convert lists of instances to (instances, nodes, 2) matrices. - # This allows match_lists_function to return data as either - # a list of Instances or a (instances, nodes, 2) matrix. - if type(sorted_gt[0]) != np.ndarray: - sorted_gt = list_points_array(sorted_gt) - if type(sorted_pr[0]) != np.ndarray: - sorted_pr = list_points_array(sorted_pr) - - points_gt.append(sorted_gt) - points_pr.append(sorted_pr) - frame_idxs.extend([frame_idx] * len(sorted_gt)) - - # Convert arrays to numpy matrixes - # instances * nodes * (x,y) - points_gt = np.concatenate(points_gt) - points_pr = np.concatenate(points_pr) - - # Calculate distances between corresponding nodes for all corresponding - # ground truth and predicted instances. - D = np.linalg.norm(points_gt - points_pr, axis=2) - - return frame_idxs, D, points_gt, points_pr - - def match_instance_lists( instances_a: List[Union[Instance, PredictedInstance]], instances_b: List[Union[Instance, PredictedInstance]], @@ -165,6 +96,75 @@ def match_instance_lists_nodewise( return instances_a, best_points_array +def matched_instance_distances( + labels_gt: Labels, + labels_pr: Labels, + match_lists_function: Callable = match_instance_lists_nodewise, + frame_range: Optional[range] = None, +) -> Tuple[List[int], np.ndarray, np.ndarray, np.ndarray]: + + """ + Distances between ground truth and predicted nodes over a set of frames. + + Args: + labels_gt: the `Labels` object with ground truth data + labels_pr: the `Labels` object with predicted data + match_lists_function: function for determining corresponding instances + Takes two lists of instances and returns "sorted" lists. + frame_range (optional): range of frames for which to compare data + If None, we compare every frame in labels_gt with corresponding + frame in labels_pr. + Returns: + Tuple: + * frame indices map: instance idx (for other matrices) -> frame idx + * distance matrix: (instances * nodes) + * ground truth points matrix: (instances * nodes * 2) + * predicted points matrix: (instances * nodes * 2) + """ + + frame_idxs = [] + points_gt = [] + points_pr = [] + for lf_gt in labels_gt.find(labels_gt.videos[0]): + frame_idx = lf_gt.frame_idx + + # Get instances from ground truth/predicted labels + instances_gt = lf_gt.instances + lfs_pr = labels_pr.find(labels_pr.videos[0], frame_idx=frame_idx) + if len(lfs_pr): + instances_pr = lfs_pr[0].instances + else: + instances_pr = [] + + # Sort ground truth and predicted instances. + # We'll then compare points between corresponding items in lists. + # We can use different "match" functions depending on what we want. + sorted_gt, sorted_pr = match_lists_function(instances_gt, instances_pr) + + # Convert lists of instances to (instances, nodes, 2) matrices. + # This allows match_lists_function to return data as either + # a list of Instances or a (instances, nodes, 2) matrix. + if type(sorted_gt[0]) != np.ndarray: + sorted_gt = list_points_array(sorted_gt) + if type(sorted_pr[0]) != np.ndarray: + sorted_pr = list_points_array(sorted_pr) + + points_gt.append(sorted_gt) + points_pr.append(sorted_pr) + frame_idxs.extend([frame_idx] * len(sorted_gt)) + + # Convert arrays to numpy matrixes + # instances * nodes * (x,y) + points_gt = np.concatenate(points_gt) + points_pr = np.concatenate(points_pr) + + # Calculate distances between corresponding nodes for all corresponding + # ground truth and predicted instances. + D = np.linalg.norm(points_gt - points_pr, axis=2) + + return frame_idxs, D, points_gt, points_pr + + def point_dist( inst_a: Union[Instance, PredictedInstance], inst_b: Union[Instance, PredictedInstance], @@ -238,46 +238,3 @@ def point_match_count(dist_array: np.ndarray, thresh: float = 5) -> int: def point_nonmatch_count(dist_array: np.ndarray, thresh: float = 5) -> int: """Given an array of distances, returns number which are not <= threshold.""" return dist_array.shape[0] - point_match_count(dist_array, thresh) - - -if __name__ == "__main__": - - labels_gt = Labels.load_json("tests/data/json_format_v1/centered_pair.json") - labels_pr = Labels.load_json( - "tests/data/json_format_v2/centered_pair_predictions.json" - ) - - # OPTION 1 - - # Match each ground truth instance node to the closest corresponding node - # from any predicted instance in the same frame. - - nodewise_matching_func = match_instance_lists_nodewise - - # OPTION 2 - - # Match each ground truth instance to a distinct predicted instance: - # We want to maximize the number of "matching" points between instances, - # where "match" means the points are within some threshold distance. - # Note that each sorted list will be as long as the shorted input list. - - instwise_matching_func = lambda gt_list, pr_list: match_instance_lists( - gt_list, pr_list, point_nonmatch_count - ) - - # PICK THE FUNCTION - - inst_matching_func = nodewise_matching_func - # inst_matching_func = instwise_matching_func - - # Calculate distances - frame_idxs, D, points_gt, points_pr = matched_instance_distances( - labels_gt, labels_pr, inst_matching_func - ) - - # Show mean difference for each node - node_names = labels_gt.skeletons[0].node_names - - for node_idx, node_name in enumerate(node_names): - mean_d = np.nanmean(D[..., node_idx]) - print(f"{node_name}\t\t{mean_d}") diff --git a/sleap/io/video.py b/sleap/io/video.py index b73569fa0..4953d2f69 100644 --- a/sleap/io/video.py +++ b/sleap/io/video.py @@ -1118,8 +1118,9 @@ def get_frames(self, idxs: Union[int, Iterable[int]]) -> np.ndarray: def get_frames_safely(self, idxs: Iterable[int]) -> Tuple[List[int], np.ndarray]: """Return list of frame indices and frames which were successfully loaded. + Args: + idxs: An iterable object that contains the indices of frames. - idxs: An iterable object that contains the indices of frames. Returns: A tuple of (frame indices, frames), where * frame indices is a subset of the specified idxs, and diff --git a/sleap/io/visuals.py b/sleap/io/visuals.py index 2018ce0bf..f2dde0be3 100644 --- a/sleap/io/visuals.py +++ b/sleap/io/visuals.py @@ -27,7 +27,13 @@ _sentinel = object() -def reader(out_q: Queue, video: Video, frames: List[int], scale: float = 1.0): +def reader( + out_q: Queue, + video: Video, + frames: List[int], + scale: float = 1.0, + background: str = "original", +): """Read frame images from video and send them into queue. Args: @@ -36,11 +42,13 @@ def reader(out_q: Queue, video: Video, frames: List[int], scale: float = 1.0): video: The `Video` object to read. frames: Full list frame indexes we want to read. scale: Output scale for frame images. + background: output video background. Either original, black, white, grey Returns: None. """ + background = background.lower() cv2.setNumThreads(usable_cpu_count()) total_count = len(frames) @@ -64,6 +72,16 @@ def reader(out_q: Queue, video: Video, frames: List[int], scale: float = 1.0): loaded_chunk_idxs, video_frame_images = video.get_frames_safely( frames_idx_chunk ) + if background != "original": + # fill the frame with the color + fill_values = {"black": 0, "grey": 127, "white": 255} + try: + fill = fill_values[background] + except KeyError: + raise ValueError( + f"Invalid background color: {background}. Options include: {', '.join(fill_values.keys())}" + ) + video_frame_images = video_frame_images * 0 + fill if not loaded_chunk_idxs: print(f"No frames could be loaded from chunk {chunk_i}") @@ -497,6 +515,7 @@ def save_labeled_video( fps: int = 15, scale: float = 1.0, crop_size_xy: Optional[Tuple[int, int]] = None, + background: str = "original", show_edges: bool = True, edge_is_wedge: bool = False, marker_size: int = 4, @@ -515,6 +534,7 @@ def save_labeled_video( fps: Frames per second for output video. scale: scale of image (so we can scale point locations to match) crop_size_xy: size of crop around instances, or None for full images + background: output video background. Either original, black, white, grey show_edges: whether to draw lines between nodes edge_is_wedge: whether to draw edges as wedges (draw as line if False) marker_size: Size of marker in pixels before scaling by `scale` @@ -537,7 +557,7 @@ def save_labeled_video( q2 = Queue(maxsize=10) progress_queue = Queue() - thread_read = Thread(target=reader, args=(q1, video, frames, scale)) + thread_read = Thread(target=reader, args=(q1, video, frames, scale, background)) thread_mark = VideoMarkerThread( in_q=q1, out_q=q2, @@ -695,6 +715,15 @@ def main(args: list = None): "and 'nodes' (default: 'nodes')" ), ) + parser.add_argument( + "--background", + type=str, + default="original", + help=( + "Specify the type of background to be used to save the videos." + "Options for background: original, black, white and grey" + ), + ) args = parser.parse_args(args=args) labels = Labels.load_file( args.data_path, video_search=[os.path.dirname(args.data_path)] @@ -730,6 +759,7 @@ def main(args: list = None): marker_size=args.marker_size, palette=args.palette, distinctly_color=args.distinctly_color, + background=args.background, ) print(f"Video saved as: {filename}") diff --git a/sleap/version.py b/sleap/version.py index ffa7b55b9..437e17fba 100644 --- a/sleap/version.py +++ b/sleap/version.py @@ -12,7 +12,7 @@ """ -__version__ = "1.3.2" +__version__ = "1.3.3" def versions(): diff --git a/tests/info/test_metrics.py b/tests/info/test_metrics.py new file mode 100644 index 000000000..0d2e097e6 --- /dev/null +++ b/tests/info/test_metrics.py @@ -0,0 +1,55 @@ +import numpy as np + +from sleap import Labels +from sleap.info.metrics import ( + match_instance_lists_nodewise, + matched_instance_distances, +) + + +def test_matched_instance_distances(centered_pair_labels, centered_pair_predictions): + labels_gt = centered_pair_labels + labels_pr = centered_pair_predictions + + # Match each ground truth instance node to the closest corresponding node + # from any predicted instance in the same frame. + + inst_matching_func = match_instance_lists_nodewise + + # Calculate distances + frame_idxs, D, points_gt, points_pr = matched_instance_distances( + labels_gt, labels_pr, inst_matching_func + ) + + # Show mean difference for each node + node_names = labels_gt.skeletons[0].node_names + expected_values = { + "head": 0.872426920709296, + "neck": 0.8016280746914615, + "thorax": 0.8602021363390538, + "abdomen": 1.01012200038258, + "wingL": 1.1297727023475939, + "wingR": 1.0869857897008424, + "forelegL1": 0.780584225081443, + "forelegL2": 1.170805798894702, + "forelegL3": 1.1020486509389473, + "forelegR1": 0.9014698776116817, + "forelegR2": 0.9448001033112047, + "forelegR3": 1.308385214215777, + "midlegL1": 0.9095691623265347, + "midlegL2": 1.2203595627907582, + "midlegL3": 0.9813843358470163, + "midlegR1": 0.9871017182813739, + "midlegR2": 1.0209829335569256, + "midlegR3": 1.0990681234096988, + "hindlegL1": 1.0005335192834348, + "hindlegL2": 1.273539518539708, + "hindlegL3": 1.1752245985832817, + "hindlegR1": 1.1402833959265248, + "hindlegR2": 1.3143221301212737, + "hindlegR3": 1.0441458592503365, + } + + for node_idx, node_name in enumerate(node_names): + mean_d = np.nanmean(D[..., node_idx]) + assert np.isclose(mean_d, expected_values[node_name], atol=1e-6) diff --git a/tests/io/test_visuals.py b/tests/io/test_visuals.py index d6144e2c1..a1223bfdf 100644 --- a/tests/io/test_visuals.py +++ b/tests/io/test_visuals.py @@ -1,6 +1,7 @@ import numpy as np import os import pytest +import cv2 from sleap.io.dataset import Labels from sleap.io.visuals import ( save_labeled_video, @@ -63,6 +64,46 @@ def test_serial_pipeline(centered_pair_predictions, tmpdir): ) +@pytest.mark.parametrize("background", ["original", "black", "white", "grey"]) +def test_sleap_render_with_different_backgrounds(background): + args = ( + f"-o test_{background}.avi -f 2 --scale 1.2 --frames 1,2 --video-index 0 " + f"--background {background} " + "tests/data/json_format_v2/centered_pair_predictions.json".split() + ) + sleap_render(args) + assert ( + os.path.exists(f"test_{background}.avi") + and os.path.getsize(f"test_{background}.avi") > 0 + ) + + # Check if the background is set correctly if not original background + if background != "original": + saved_video_path = f"test_{background}.avi" + cap = cv2.VideoCapture(saved_video_path) + ret, frame = cap.read() + + # Calculate mean color of the channels + b, g, r = cv2.split(frame) + mean_b = np.mean(b) + mean_g = np.mean(g) + mean_r = np.mean(r) + + # Set threshold values. Color is white if greater than white threshold, black + # if less than grey threshold and grey if in between both threshold values. + white_threshold = 240 + grey_threshold = 40 + + # Check if the average color is white, grey, or black + if all(val > white_threshold for val in [mean_b, mean_g, mean_r]): + background_color = "white" + elif all(val < grey_threshold for val in [mean_b, mean_g, mean_r]): + background_color = "black" + else: + background_color = "grey" + assert background_color == background + + def test_sleap_render(centered_pair_predictions): args = ( "-o testvis.avi -f 2 --scale 1.2 --frames 1,2 --video-index 0 "