From 6eed6d965c8de181cc9d973cea52f5ae3000f86c Mon Sep 17 00:00:00 2001 From: Liezl Maree <38435167+roomrys@users.noreply.github.com> Date: Thu, 14 Sep 2023 10:52:13 -0700 Subject: [PATCH 01/10] Add version restrictions to tensorflow for pypi (#1485) * Add version restrictions to tensorflow for pypi * Get GUI working on Linux * Add comments * Get GUI working on Windows * Get inference working on windows * Restrict urllib3 range (non-blocking error in installation) * Get training/inference working on linux minimal environment --- pypi_requirements.txt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pypi_requirements.txt b/pypi_requirements.txt index b18637c37..eb4d63076 100644 --- a/pypi_requirements.txt +++ b/pypi_requirements.txt @@ -12,14 +12,15 @@ 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 @@ -31,5 +32,15 @@ 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 From c033f85885de18500b2c404fa02e80ffd5ced8eb Mon Sep 17 00:00:00 2001 From: Liezl Maree <38435167+roomrys@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:14:21 -0700 Subject: [PATCH 02/10] Remove `imageio` pin (#1501) --- pypi_requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/pypi_requirements.txt b/pypi_requirements.txt index eb4d63076..33f419c9c 100644 --- a/pypi_requirements.txt +++ b/pypi_requirements.txt @@ -25,7 +25,6 @@ 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 From cf831b61e018805cce465c86b916b479b0b811fa Mon Sep 17 00:00:00 2001 From: Liezl Maree <38435167+roomrys@users.noreply.github.com> Date: Fri, 15 Sep 2023 09:17:50 -0700 Subject: [PATCH 03/10] Reset LD_LIBRARY_PATH on deactivate (#1502) --- .conda/build.sh | 10 +++++----- .conda/sleap_activate.sh | 2 ++ .conda/sleap_deactivate.sh | 4 ++++ docs/installation.md | 16 +++++++++++++--- 4 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 .conda/sleap_deactivate.sh 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/docs/installation.md b/docs/installation.md index c311f0851..9b4f7c4db 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -343,14 +343,15 @@ python -c "import tensorflow as tf; print(tf.config.list_physical_devices('GPU') ````{warning} TensorFlow 2.7+ is currently failing to detect CUDA Toolkit and CuDNN on some systems (see [Issue thread](https://github.com/tensorflow/tensorflow/issues/52988)). -If you run into issues, try downgrading the TensorFlow 2.6: +If you run into issues, either try downgrading the TensorFlow 2.6: ```bash pip install tensorflow==2.6.3 ``` +or follow the note below. ```` ````{note} -If you are on Linux, have a NVIDIA GPU, and are having trouble utilizing your GPU: +If you are on Linux, have a NVIDIA GPU, but cannot detect your GPU: ```bash W tensorflow/stream_executor/platform/default/dso_loader.cc:64 Could not load dynamic @@ -368,10 +369,19 @@ and run the commands: ```bash mkdir -p $CONDA_PREFIX/etc/conda/activate.d echo '#!/bin/sh' >> $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 From a2092f08fe68dd2337f1eb62cb3432756d645dbb Mon Sep 17 00:00:00 2001 From: Liezl Maree <38435167+roomrys@users.noreply.github.com> Date: Fri, 15 Sep 2023 15:55:48 -0700 Subject: [PATCH 04/10] Brown bag bump to 1.3.3 (#1484) * Add version restrictions to tensorflow for pypi * Bump to 1.3.3 * Advise 1.3.3 pip install * Get GUI working on Linux * Get GUI working on Windows * Get inference working on windows * Restrict urllib3 range (non-blocking error in installation) * Get training/inference working on linux minimal environment * Put quotes around sleap install --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- docs/conf.py | 4 ++-- docs/installation.md | 2 +- docs/notebooks/Data_structures.ipynb | 2 +- docs/notebooks/Interactive_and_realtime_inference.ipynb | 2 +- docs/notebooks/Interactive_and_resumable_training.ipynb | 2 +- docs/notebooks/Model_evaluation.ipynb | 2 +- docs/notebooks/Post_inference_tracking.ipynb | 2 +- .../Training_and_inference_on_an_example_dataset.ipynb | 2 +- .../notebooks/Training_and_inference_using_Google_Drive.ipynb | 2 +- sleap/version.py | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) 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/installation.md b/docs/installation.md index 9b4f7c4db..c028cd5b1 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -232,7 +232,7 @@ Although you do not need Mambaforge installed to perform a `pip install`, we rec 3. Finally, we can perform the `pip install`: ```bash - pip install sleap[pypi]==1.3.2 + pip install sleap[pypi]==1.3.3 ``` This works on **any OS except Apple silicon** and on **Google Colab**. 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/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(): From f77af11029eb2af1b5f4cd03d09ca64897bcafb4 Mon Sep 17 00:00:00 2001 From: Liezl Maree <38435167+roomrys@users.noreply.github.com> Date: Fri, 15 Sep 2023 16:51:23 -0700 Subject: [PATCH 05/10] SLEAP 1.3.3 (#1505) * Do not try to remove item if already deleted (#1498) * Set `LD_LIBRARY_PATH` on `mamba activate` (#1496) * Add version restrictions to tensorflow for pypi (#1485) * Remove `imageio` pin (#1501) * Reset LD_LIBRARY_PATH on deactivate (#1502) * Brown bag bump to 1.3.3 (#1484) --- .conda/README.md | 4 +- .conda/build.sh | 10 ++++- .conda/sleap_activate.sh | 6 +++ .conda/sleap_deactivate.sh | 4 ++ .github/ISSUE_TEMPLATE/bug_report.md | 2 +- docs/conf.py | 4 +- docs/installation.md | 39 ++++++++++++++++++- docs/notebooks/Data_structures.ipynb | 2 +- .../Interactive_and_realtime_inference.ipynb | 2 +- .../Interactive_and_resumable_training.ipynb | 2 +- docs/notebooks/Model_evaluation.ipynb | 2 +- docs/notebooks/Post_inference_tracking.ipynb | 2 +- ..._and_inference_on_an_example_dataset.ipynb | 2 +- ...ing_and_inference_using_Google_Drive.ipynb | 2 +- environment.yml | 1 + pypi_requirements.txt | 20 +++++++--- sleap/gui/overlays/base.py | 20 +++++++--- sleap/version.py | 2 +- 18 files changed, 100 insertions(+), 26 deletions(-) create mode 100644 .conda/sleap_activate.sh create mode 100644 .conda/sleap_deactivate.sh diff --git a/.conda/README.md b/.conda/README.md index 65fadd36e..71a49d7f1 100644 --- a/.conda/README.md +++ b/.conda/README.md @@ -3,7 +3,7 @@ This folder defines the conda package build for Linux and Windows. There are run To build, first go to the base repo directory and install the build environment: ``` -conda env create -f environment_build.yml -n sleap_build && conda activate sleap_build +mamba env create -f environment_build.yml -n sleap_build && conda activate sleap_build ``` And finally, run the build command pointing to this directory: @@ -15,7 +15,7 @@ conda build .conda --output-folder build -c conda-forge -c nvidia -c https://con To install the local package: ``` -conda create -n sleap_0 -c conda-forge -c nvidia -c ./build -c https://conda.anaconda.org/sleap/ -c anaconda sleap=x.x.x +mamba create -n sleap_0 -c conda-forge -c nvidia -c ./build -c https://conda.anaconda.org/sleap/ -c anaconda sleap=x.x.x ``` replacing x.x.x with the version of SLEAP that you just built. diff --git a/.conda/build.sh b/.conda/build.sh index 620cd127a..86ab5af73 100644 --- a/.conda/build.sh +++ b/.conda/build.sh @@ -12,4 +12,12 @@ pip install --no-cache-dir -r ./requirements.txt # Install sleap itself. This does not install the requirements, but will list which # requirements are missing (see "install_requires") when user attempts to install. -python setup.py install --single-version-externally-managed --record=record.txt \ No newline at end of file +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. +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 new file mode 100644 index 000000000..885879a89 --- /dev/null +++ b/.conda/sleap_activate.sh @@ -0,0 +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/installation.md b/docs/installation.md index ee9dad1ea..c028cd5b1 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -232,7 +232,7 @@ Although you do not need Mambaforge installed to perform a `pip install`, we rec 3. Finally, we can perform the `pip install`: ```bash - pip install sleap[pypi]==1.3.2 + pip install sleap[pypi]==1.3.3 ``` This works on **any OS except Apple silicon** and on **Google Colab**. @@ -343,10 +343,45 @@ python -c "import tensorflow as tf; print(tf.config.list_physical_devices('GPU') ````{warning} TensorFlow 2.7+ is currently failing to detect CUDA Toolkit and CuDNN on some systems (see [Issue thread](https://github.com/tensorflow/tensorflow/issues/52988)). -If you run into issues, try downgrading the TensorFlow 2.6: +If you run into issues, either try downgrading the TensorFlow 2.6: ```bash pip install tensorflow==2.6.3 ``` +or follow the note below. +```` + +````{note} +If you are on Linux, have a NVIDIA GPU, but cannot detect your GPU: + +```bash +W tensorflow/stream_executor/platform/default/dso_loader.cc:64 Could not load dynamic +library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object +file: No such file or directory +``` + +then activate the environment: + +```bash +mamba activate sleap +``` + +and run the commands: +```bash +mkdir -p $CONDA_PREFIX/etc/conda/activate.d +echo '#!/bin/sh' >> $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 +``` + +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/environment.yml b/environment.yml index 9f9ff903d..67ed39d01 100644 --- a/environment.yml +++ b/environment.yml @@ -46,3 +46,4 @@ dependencies: - pip: - "--editable=.[conda_dev]" + \ No newline at end of file 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/gui/overlays/base.py b/sleap/gui/overlays/base.py index 019f87355..d27b069ac 100644 --- a/sleap/gui/overlays/base.py +++ b/sleap/gui/overlays/base.py @@ -8,13 +8,13 @@ so that current frame must be redrawn). """ -from qtpy import QtWidgets - -import attr import abc -import numpy as np +import logging from typing import Sequence, Union, Optional, List +import attr +import numpy as np +from qtpy import QtWidgets from qtpy.QtWidgets import QGraphicsItem from sleap import Labels, Video @@ -22,6 +22,8 @@ from sleap.nn.data.providers import VideoReader from sleap.nn.inference import VisualPredictor +logger = logging.getLogger(__name__) + @attr.s(auto_attribs=True) class BaseOverlay(abc.ABC): @@ -64,7 +66,15 @@ def remove_from_scene(self): if self.items is None: return for item in self.items: - self.player.scene.removeItem(item) + try: + self.player.scene.removeItem(item) + + 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 = [] def redraw(self, video, frame_idx, *args, **kwargs): """Remove all items from the scene before adding new items to the scene. 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(): From 864c9941c1b092ac1d62c8461ac37d4d8a9b8fac Mon Sep 17 00:00:00 2001 From: roomrys <38435167+roomrys@users.noreply.github.com> Date: Mon, 18 Sep 2023 09:40:13 -0700 Subject: [PATCH 06/10] Update installation docs --- docs/installation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index c028cd5b1..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 Date: Mon, 18 Sep 2023 09:50:58 -0700 Subject: [PATCH 07/10] Remove no-op code from #1498 --- sleap/gui/overlays/base.py | 1 - 1 file changed, 1 deletion(-) 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 = [] From c76e6022a73ea430c7b75e7a9f5a3b79a85c5c3b Mon Sep 17 00:00:00 2001 From: Scott Yang <67733409+scott-yj-yang@users.noreply.github.com> Date: Mon, 18 Sep 2023 16:02:29 -0400 Subject: [PATCH 08/10] Add options to set background color when exporting video (#1328) * implement #921 * simplified form / refractor * Add test function and update cli docs * Improve test function to check background color * Improve comments * Change background options to lowercase * Use coderabbitai suggested `fill` --------- Co-authored-by: Shrivaths Shyam <52810689+shrivaths16@users.noreply.github.com> Co-authored-by: Liezl Maree <38435167+roomrys@users.noreply.github.com> --- docs/guides/cli.md | 3 +++ sleap/config/labeled_clip_form.yaml | 4 +++ sleap/gui/commands.py | 2 ++ sleap/io/video.py | 3 ++- sleap/io/visuals.py | 34 ++++++++++++++++++++++-- tests/io/test_visuals.py | 41 +++++++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 3 deletions(-) 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/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/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/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/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 " From 41001532601b1190342dc37cd8e2a94a000a2f0c Mon Sep 17 00:00:00 2001 From: Liezl Maree <38435167+roomrys@users.noreply.github.com> Date: Mon, 25 Sep 2023 08:20:26 -0700 Subject: [PATCH 09/10] Increase range on batch size (#1513) * Increase range on batch size * Set maximum to a factor of 2 --- sleap/config/training_editor_form.yaml | 1 + 1 file changed, 1 insertion(+) 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 From ed77b49164b654bf9e43c29fcc3af73be2f8eb3b Mon Sep 17 00:00:00 2001 From: Liezl Maree <38435167+roomrys@users.noreply.github.com> Date: Fri, 29 Sep 2023 09:42:58 -0400 Subject: [PATCH 10/10] Set default callable for `match_lists_function` (#1520) * Set default for `match_lists_function` * Move test code to official tests * Check using expected values --- sleap/info/metrics.py | 181 ++++++++++++++----------------------- tests/info/test_metrics.py | 55 +++++++++++ 2 files changed, 124 insertions(+), 112 deletions(-) create mode 100644 tests/info/test_metrics.py 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/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)