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 11 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
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pyyaml
pillow>=8.3.1,<=8.4.0
imageio<=2.15.0
imgaug==0.4.0
sleap-anipose
scipy>=1.4.1,<=1.9.0
scikit-image
scikit-learn==1.0.*
Expand Down
103 changes: 103 additions & 0 deletions sleap/io/cameras.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Module for storing information for camera groups."""

from typing import List, Optional, Union

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(factory=None)
roomrys marked this conversation as resolved.
Show resolved Hide resolved

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."""
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):
"""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)


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

Attributes:
cameras: List of cameras.
metadata: Set of metadata.
"""

cameras: List[Camera] = 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):
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 from a calibration.toml file.

Args:
filename: Path to calibration.toml file.

Returns:
CameraCluster object.
"""
cam_group: CameraGroup = super().load(filename)
cameras = [Camcorder(cam) for cam in cam_group.cameras]
return cls(cameras=cameras, metadata=cam_group.metadata)
6 changes: 4 additions & 2 deletions sleap/io/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

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

from sleap.io.cameras import Camcorder
from sleap.util import json_loads, json_dumps

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -999,8 +1000,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 All @@ -1020,6 +1020,7 @@ class Video:
Args:
backend: A backend is an object that implements the following basic
required methods and properties
camera: A Camcorder object that describes the camera used to capture.
roomrys marked this conversation as resolved.
Show resolved Hide resolved

* Properties

Expand All @@ -1040,6 +1041,7 @@ class Video:
backend: Union[
HDF5Video, NumpyVideo, MediaVideo, ImgStoreVideo, SingleImageVideo, DummyVideo
] = attr.ib()
camera: Optional[Camcorder] = attr.ib(default=None)
roomrys marked this conversation as resolved.
Show resolved Hide resolved

# Delegate to the backend
def __getattr__(self, item):
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"
roomrys marked this conversation as resolved.
Show resolved Hide resolved
53 changes: 53 additions & 0 deletions tests/io/test_cameras.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""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."""
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 cameras."""
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)


if __name__ == "__main__":
pytest.main([f"{__file__}::test_camcorder"])
roomrys marked this conversation as resolved.
Show resolved Hide resolved