Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(0) Add camera group data structures #1258

Draft
wants to merge 43 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ad65e1f
Add camera group and calibration as attributes to `Video`
roomrys Apr 1, 2023
30707be
Add docs for calibration and camera group functions.
roomrys Apr 3, 2023
cd5c1c3
Add CameraCluster class
roomrys Apr 3, 2023
e34a028
Remove all calibration/camera-group related stuff from video class
roomrys Apr 3, 2023
c60d69e
Add initial Camcorder class and minimal tests
roomrys Apr 3, 2023
9bf2cff
Fix eq on Camcorder attrs to get tests working
roomrys Apr 4, 2023
c206944
Convert Camcorder to a wrapper class
roomrys Apr 4, 2023
e1fc405
Non-logical clean-up
roomrys Apr 4, 2023
57c0a90
Non-logical clean-up
roomrys Apr 4, 2023
3ac82de
Lint
roomrys Apr 4, 2023
12d8ae9
Merge branch 'develop' into liezl/add-camera-group
roomrys Apr 4, 2023
f6ef0cf
Remove camera attr from `Video`, fix test path, and clean-up
roomrys Apr 4, 2023
212fa4c
Remove unused imports
roomrys Apr 4, 2023
492a78b
Correct type-hinting `Camera` -> `Camcorder`
roomrys Apr 4, 2023
3f1b31d
Touch-up docstrings
roomrys Apr 4, 2023
21c3fb3
Merge branch 'develop' into liezl/add-camera-group
roomrys Apr 6, 2023
7951ae6
Merge branch 'develop' into liezl/add-camera-group
roomrys Apr 10, 2023
baa7420
Merge branch 'develop' into liezl/add-camera-group
roomrys Apr 11, 2023
fddc055
Merge branch 'develop' into liezl/add-camera-group
roomrys Apr 27, 2023
d456f29
Merge branch 'develop' into liezl/add-camera-group
roomrys Jul 6, 2023
7e2e185
Merge branch 'develop' into liezl/add-camera-group
roomrys Jul 6, 2023
39c74e2
Hack sleap-anipose into the requirements
roomrys Jul 7, 2023
b74ecfd
Merge branch 'liezl/add-camera-group' of https://github.com/talmolab/…
roomrys Jul 7, 2023
514e747
Fix typo in ci hack
roomrys Jul 7, 2023
aacdbd1
Perform install hack in same step as tests
roomrys Jul 7, 2023
afcbb15
Do a `pip uninstall` instead of `conda uninstall`
roomrys Jul 19, 2023
f76679a
OS specific uninstall commands in CI
roomrys Jul 19, 2023
9d70a86
Correct syntax for CI
roomrys Jul 19, 2023
5c0d532
Use `micromamba` instead of `conda`
roomrys Jul 19, 2023
8b29794
Use conda to install `imgaug`
roomrys Jul 19, 2023
90649c4
Use `pip freeze` to list `opencv`
roomrys Jul 19, 2023
4344118
Explicitly use xvfb in Linux tests
roomrys Jul 19, 2023
9fde0b6
Try to run tests using github actions xvfb
roomrys Jul 19, 2023
a1fc9a2
Separate pytest command from dependency changes
roomrys Jul 19, 2023
15cc927
Revert to using just pytest in CI
roomrys Jul 19, 2023
5dc74b2
Set environment variable in tests
roomrys Jul 19, 2023
fcc8c28
Add comments
roomrys Jul 19, 2023
587331f
Merge branch 'develop' of https://github.com/talmolab/sleap into liez…
Sep 25, 2023
168a10f
Sort imports
roomrys Sep 29, 2023
b8ac481
Add error message for accessing `Camcorder` attributes
roomrys Sep 29, 2023
cb3efea
Add error message for loading calibration file
roomrys Sep 29, 2023
d16516c
Change factory to default for initializing camera attribute
roomrys Sep 29, 2023
0230a97
Merge branch 'develop' of https://github.com/talmolab/sleap into liez…
roomrys Oct 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .conda/bld.bat
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ set PIP_IGNORE_INSTALLED=False
@REM https://docs.conda.io/projects/conda-build/en/stable/user-guide/wheel-files.html)
pip install --no-cache-dir -r .\requirements.txt

@REM HACK(LM): (untested) Uninstall all opencv packages and install opencv-contrib-python
for /f "tokens=1" %%a in ('conda list ^| findstr opencv') do pip uninstall %%a -y
pip install "opencv-contrib-python<4.7.0"

@REM Install sleap itself. This does not install the requirements, but will list which
@REM requirements are missing (see "install_requires") when user attempts to install.
python setup.py install --single-version-externally-managed --record=record.txt
3 changes: 3 additions & 0 deletions .conda/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export PIP_IGNORE_INSTALLED=False
# https://docs.conda.io/projects/conda-build/en/stable/user-guide/wheel-files.html)
pip install --no-cache-dir -r ./requirements.txt

# HACK(LM): (untested) Uninstall all opencv packages and install opencv-contrib-python
conda list | grep opencv | awk '{system("pip uninstall " $1 " -y")}'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using system() to execute shell commands can be risky due to potential command injection vulnerabilities. Although in this case, the risk is low because the input to system() is not user-controlled, it's still a good practice to avoid using system() when possible. Consider using a safer alternative for executing shell commands.

pip install "opencv-contrib-python<4.7.0"

# Install sleap itself. This does not install the requirements, but will list which
# requirements are missing (see "install_requires") when user attempts to install.
Expand Down
4 changes: 4 additions & 0 deletions .conda_mac/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ export PIP_IGNORE_INSTALLED=False

pip install --no-cache-dir -r requirements.txt

# HACK(LM): (untested) Uninstall all opencv packages and install opencv-contrib-python
conda list | grep opencv | awk '{system("pip uninstall " $1 " -y")}'
pip install "opencv-contrib-python<4.7.0"
Comment on lines +13 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of conda list | grep opencv | awk '{system("pip uninstall " $1 " -y")}' to uninstall all OpenCV packages is a bit risky. It assumes that all packages with 'opencv' in their name are related to OpenCV and should be uninstalled, which might not always be the case. Also, it's using pip to uninstall packages installed by conda, which could lead to inconsistencies in the environment. Consider using conda uninstall --force opencv instead to ensure only the opencv package is removed and there are no conflicts between conda and pip.

- conda list | grep opencv | awk '{system("pip uninstall " $1 " -y")}'
+ conda uninstall --force opencv


python setup.py install --single-version-externally-managed --record=record.txt
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ jobs:
bash
powershell
post-cleanup: all

# HACK(LM): Install correct version of opencv
- name: Reinstall opencv
shell: bash -l {0}
run: |
pip freeze | grep opencv | awk '{system("pip uninstall " $1 " -y")}'
pip install "opencv-contrib-python<4.7.0"


# Print environment info
- name: Print environment info
Expand All @@ -101,7 +109,7 @@ jobs:
micromamba list
pip freeze

# Test environment
# Test the code and the environment
- name: Test with pytest
shell: bash -l {0}
run: |
Expand Down
2 changes: 1 addition & 1 deletion dev_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pytest
pytest-qt>=4.0.0
pytest-cov<=3.0.0
pytest-xvfb
pytest-xvfb # For running tests without UI
ipython
sphinx
# furo
Expand Down
6 changes: 6 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ mamba create -y -n sleap -c conda-forge -c anaconda -c sleap sleap=1.3.3
mamba env create -f environment_mac.yml -n sleap
```

Followed by a terrible hack that should never reach the real docs:

```bash
conda activate sleap && conda list | grep opencv | awk '{system("conda uninstall " $1 " -y")}' && pip install "opencv-contrib-python<4.7.0"
```

This is the **recommended method for development**.

```{note}
Expand Down
8 changes: 5 additions & 3 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ dependencies:
# Packages SLEAP uses directly
- conda-forge::attrs >=21.2.0 #,<=21.4.0
- conda-forge::cattrs ==1.1.1
- conda-forge::imgaug ==0.4.0
- conda-forge::imgaug ==0.4.0 # Uses opencv-python, but sleap-anipose needs contrib
- conda-forge::jsmin
- conda-forge::jsonpickle ==1.2
- conda-forge::networkx
- anaconda::numpy >=1.19.5,<1.23.0
- conda-forge::opencv
# - conda-forge::opencv # Uses opencv-python, but sleap-anipose needs contrib
- conda-forge::pandas
- conda-forge::pip
- conda-forge::pillow #>=8.3.1,<=8.4.0
Expand Down Expand Up @@ -46,4 +46,6 @@ dependencies:

- pip:
- "--editable=.[conda_dev]"


# HACK(LM): Uninstall all opencv packages and install opencv-contrib-python
# conda activate sleap && conda list | grep opencv | awk '{system("pip uninstall " $1 " -y")}' && pip install "opencv-contrib-python<4.7.0"
4 changes: 4 additions & 0 deletions environment_no_cuda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ dependencies:

- pip:
- "--editable=.[conda_dev]"

# HACK(LM): Uninstall all opencv packages and install opencv-contrib-python
# conda activate sleap && conda list | grep opencv | awk '{system("pip uninstall " $1 " -y")} && pip install "opencv-contrib-python<4.7.0"

14 changes: 14 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,23 @@ ndx-pose
nixio>=1.5.3 # Constrain put on by @jgrewe from G-Node
qimage2ndarray # ==1.9.0
segmentation-models
sleap-anipose
tensorflow-macos==2.9.2; sys_platform == 'darwin' and platform_machine == 'arm64'
tensorflow-metal==0.5.0; sys_platform == 'darwin' and platform_machine == 'arm64'

# Conda installing results in https://github.com/h5py/h5py/issues/2037
h5py<3.2; sys_platform == 'win32' # Newer versions result in error above, linking issue in Linux
pynwb>=2.3.3 # 2.0.0 required by ndx-pose, 2.3.3 fixes importlib-metadata incompatibility

# HACK(LM): Uninstall all opencv packages and install opencv-contrib-python
# Run after installing conda environment
#
# conda activate sleap
#
# if on Windows, then
# for /f "tokens=1" %a in ('conda list ^| findstr opencv') do pip uninstall %a -y
# otherwise, run
# conda list | grep opencv | awk '{system("pip uninstall " $1 " -y")}'
#
# everybody, run
# pip install "opencv-contrib-python<4.7.0"
116 changes: 116 additions & 0 deletions sleap/io/cameras.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Module for storing information for camera groups."""

from typing import List, Optional, Union, Iterator

from attrs import define, field
from aniposelib.cameras import Camera, FisheyeCamera, CameraGroup
import numpy as np


@define
class Camcorder:
"""Wrapper for `Camera` and `FishEyeCamera` classes.

Attributes:
camera: `Camera` or `FishEyeCamera` object.
"""

camera: Optional[Union[Camera, FisheyeCamera]] = field(default=None)

def __eq__(self, other):
if not isinstance(other, Camcorder):
return NotImplemented

for attr in vars(self):
other_attr = getattr(other, attr)
if isinstance(other_attr, np.ndarray):
if not np.array_equal(getattr(self, attr), other_attr):
return False
elif getattr(self, attr) != other_attr:
return False
roomrys marked this conversation as resolved.
Show resolved Hide resolved

return True

def __getattr__(self, attr):
"""Used to grab methods from `Camera` or `FishEyeCamera` objects."""
if self.camera is None:
raise AttributeError(
f"No camera has been specified. "
f"This is likely because the `Camcorder.from_dict` method was not used to initialize this object. "
f"Please use `Camcorder.from_dict` to recreate the object."
)
return getattr(self.camera, attr)
roomrys marked this conversation as resolved.
Show resolved Hide resolved

def __repr__(self):
return f"{self.__class__.__name__}(name={self.name}, size={self.size})"

@classmethod
def from_dict(cls, d) -> "Camcorder":
"""Creates a `Camcorder` object from a dictionary.

Args:
d: Dictionary with keys for matrix, dist, size, rvec, tvec, and name.

Returns:
`Camcorder` object.
"""
if "fisheye" in d and d["fisheye"]:
cam = FisheyeCamera.from_dict(d)
else:
cam = Camera.from_dict(d)
return Camcorder(cam)
roomrys marked this conversation as resolved.
Show resolved Hide resolved


@define
class CameraCluster(CameraGroup):
"""Class for storing information for camera groups.

Attributes:
cameras: List of `Camcorder`s.
metadata: Set of metadata.
"""

cameras: List[Camcorder] = field(factory=list)
metadata: set = field(factory=set)

def __attrs_post_init__(self):
super().__init__(cameras=self.cameras, metadata=self.metadata)
roomrys marked this conversation as resolved.
Show resolved Hide resolved

def __len__(self):
return len(self.cameras)

def __getitem__(self, idx):
return self.cameras[idx]

def __iter__(self) -> Iterator[List[Camcorder]]:
return iter(self.cameras)

def __contains__(self, item):
return item in self.cameras

def __repr__(self):
message = f"{self.__class__.__name__}(len={len(self)}: "
for cam in self:
message += f"{cam.name}, "
return f"{message[:-2]})"

@classmethod
def load(cls, filename) -> "CameraCluster":
"""Loads cameras as `Camcorder`s from a calibration.toml file.

Args:
filename: Path to calibration.toml file.

Returns:
`CameraCluster` object.
"""

try:
cam_group: CameraGroup = super().load(filename)
except FileNotFoundError as e:
raise FileNotFoundError(
f"Could not find calibration file at {filename}."
) from e
roomrys marked this conversation as resolved.
Show resolved Hide resolved

cameras = [Camcorder(cam) for cam in cam_group.cameras]
return cls(cameras=cameras, metadata=cam_group.metadata)
5 changes: 2 additions & 3 deletions sleap/io/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import logging
import multiprocessing

from typing import Iterable, List, Optional, Tuple, Union, Text
from typing import Iterable, List, Optional, Tuple, Union

from sleap.util import json_loads, json_dumps

Expand Down Expand Up @@ -999,8 +999,7 @@ def get_frame(self, idx: int, grayscale: bool = None) -> np.ndarray:

@attr.s(auto_attribs=True, eq=False, order=False)
class Video:
"""
The top-level interface to any Video data used by SLEAP.
"""The top-level interface to any Video data used by SLEAP.

This class provides a common interface for various supported video data
backends. It provides the bare minimum of properties and methods that
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
from tests.fixtures.datasets import *
from tests.fixtures.videos import *
from tests.fixtures.models import *
from tests.fixtures.cameras import *
33 changes: 33 additions & 0 deletions tests/data/cameras/minimal_session/calibration.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[cam_0]
name = "back"
size = [ 1280, 1024,]
matrix = [ [ 769.8864926727645, 0.0, 639.5,], [ 0.0, 769.8864926727645, 511.5,], [ 0.0, 0.0, 1.0,],]
distortions = [ -0.2853406116327607, 0.0, 0.0, 0.0, 0.0,]
rotation = [ -0.01620434170631696, 0.00243953661952865, -0.0008482754607133058,]
translation = [ 0.11101046010648573, -5.942766688873288, -122.27936818948484,]

[cam_1]
name = "mid"
size = [ 1280, 1024,]
matrix = [ [ 759.1049091821777, 0.0, 639.5,], [ 0.0, 759.1049091821777, 511.5,], [ 0.0, 0.0, 1.0,],]
distortions = [ -0.3019598217075406, 0.0, 0.0, 0.0, 0.0,]
rotation = [ -0.5899610967415617, -1.4541149329590473, -2.6096557771132054,]
translation = [ -117.01279148208383, -335.68277970969496, 87.84524145188074,]

[cam_2]
name = "side"
size = [ 1280, 1024,]
matrix = [ [ 964.7203950924776, 0.0, 639.5,], [ 0.0, 964.7203950924776, 511.5,], [ 0.0, 0.0, 1.0,],]
distortions = [ -0.2939343017698909, 0.0, 0.0, 0.0, 0.0,]
rotation = [ 0.5133644577490065, 0.4933577839393885, 2.712950645410121,]
translation = [ -137.65379909555472, -91.75965072441964, -19.01274966036669,]

[cam_3]
name = "top"
size = [ 1280, 1024,]
matrix = [ [ 964.7203950924776, 0.0, 639.5,], [ 0.0, 964.7203950924776, 511.5,], [ 0.0, 0.0, 1.0,],]
distortions = [ -0.2939343017698909, 0.0, 0.0, 0.0, 0.0,]
rotation = [ 0.5133644577490065, 0.4933577839393885, 2.712950645410121,]
translation = [ -137.65379909555472, -91.75965072441964, -19.01274966036669,]

[metadata]
8 changes: 8 additions & 0 deletions tests/fixtures/cameras.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Camera fixtures for pytest."""

import pytest


@pytest.fixture
def min_session_calibration_toml_path():
return "tests/data/cameras/minimal_session/calibration.toml"
9 changes: 9 additions & 0 deletions tests/gui/test_app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import os

import pytest
from qtpy.QtCore import QLibraryInfo
from qtpy.QtWidgets import QApplication

from sleap.gui.app import MainWindow
from sleap.gui.commands import *

# TODO(LM): Remove when sleap-anipose, aniposelib, and imgaug use headless opencv
# https://github.com/pytest-dev/pytest-qt/issues/396#issuecomment-1060193720
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = QLibraryInfo.location(
QLibraryInfo.PluginsPath
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice find!

)


def test_app_workflow(
qtbot, centered_pair_vid, small_robot_mp4_vid, min_tracks_2node_labels: Labels
Expand Down
49 changes: 49 additions & 0 deletions tests/io/test_cameras.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Module to test functions in `sleap.io.cameras`."""

import numpy as np
import pytest
from sleap.io.cameras import Camcorder, CameraCluster


def test_camcorder(min_session_calibration_toml_path):
"""Test `Camcorder` data structure."""
calibration = min_session_calibration_toml_path
cameras = CameraCluster.load(calibration)
cam: Camcorder = cameras[0]

# Test from_dict
cam_dict = cam.get_dict()
cam2 = Camcorder.from_dict(cam_dict)

# Test __repr__
assert f"{cam.__class__.__name__}(" in repr(cam)

# Check that attributes are the same
assert np.array_equal(cam.matrix, cam2.matrix)
assert np.array_equal(cam.dist, cam2.dist)
assert np.array_equal(cam.size, cam2.size)
assert np.array_equal(cam.rvec, cam2.rvec)
assert np.array_equal(cam.tvec, cam2.tvec)
assert cam.name == cam2.name
assert cam.extra_dist == cam2.extra_dist

# Test __eq__
assert cam == cam2


def test_camera_cluster(min_session_calibration_toml_path):
"""Test `CameraCluster` data structure."""
calibration = min_session_calibration_toml_path
cameras = CameraCluster.load(calibration)

# Test __len__
assert len(cameras) == len(cameras.cameras)
assert len(cameras) == 4

# Test __getitem__, __iter__, and __contains__
for idx, cam in enumerate(cameras):
assert cam == cameras[idx]
assert cam in cameras

# Test __repr__
assert f"{cameras.__class__.__name__}(" in repr(cameras)