-
Notifications
You must be signed in to change notification settings - Fork 101
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
base: develop
Are you sure you want to change the base?
Changes from 42 commits
ad65e1f
30707be
cd5c1c3
e34a028
c60d69e
9bf2cff
c206944
e1fc405
57c0a90
3ac82de
12d8ae9
f6ef0cf
212fa4c
492a78b
3f1b31d
21c3fb3
7951ae6
baa7420
fddc055
d456f29
7e2e185
39c74e2
b74ecfd
514e747
aacdbd1
afcbb15
f76679a
9d70a86
5c0d532
8b29794
90649c4
4344118
9fde0b6
a1fc9a2
15cc927
5dc74b2
fcc8c28
587331f
168a10f
b8ac481
cb3efea
d16516c
0230a97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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")}'
+ conda uninstall --force opencv |
||
|
||
python setup.py install --single-version-externally-managed --record=record.txt |
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) |
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] |
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" |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
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) |
There was a problem hiding this comment.
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 tosystem()
is not user-controlled, it's still a good practice to avoid usingsystem()
when possible. Consider using a safer alternative for executing shell commands.