From f61a1914cdbc51d323c15ba51d0608ee2866bdec Mon Sep 17 00:00:00 2001 From: roomrys <38435167+roomrys@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:59:39 -0800 Subject: [PATCH 1/5] Docstring clarification and string spelling --- sleap_io/model/camera.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/sleap_io/model/camera.py b/sleap_io/model/camera.py index 530939f3..a440fe17 100644 --- a/sleap_io/model/camera.py +++ b/sleap_io/model/camera.py @@ -36,26 +36,26 @@ class RecordingSession: _camera_by_video: dict[Video, Camera] = field(factory=dict) def get_video(self, camera: Camera) -> Video | None: - """Get `Video` associated with `Camera`. + """Get `Video` associated with `camera`. Args: - camera: Camera to get video + camera: `Camera` to get `Video` Returns: - Video associated with camera or None if not found + `Video` associated with `camera` or None if not found """ return self._video_by_camera.get(camera, None) def add_video(self, video: Video, camera: Camera): - """Add `Video` to `RecordingSession` and mapping to `Camera`. + """Add `video` to `RecordingSession` and mapping to `camera`. Args: video: `Video` object to add to `RecordingSession`. - camera: `Camera` object to associate with `Video`. + camera: `Camera` object to associate with `video`. Raises: - ValueError: If `Camera` is not in associated `CameraGroup`. - ValueError: If `Video` is not a `Video` object. + ValueError: If `camera` is not in associated `CameraGroup`. + ValueError: If `video` is not a `Video` object. """ # Raise ValueError if camera is not in associated camera group self.camera_group.cameras.index(camera) @@ -73,13 +73,13 @@ def add_video(self, video: Video, camera: Camera): self._camera_by_video[video] = camera def remove_video(self, video: Video): - """Remove `Video` from `RecordingSession` and mapping to `Camera`. + """Remove `video` from `RecordingSession` and mapping to `Camera`. Args: video: `Video` object to remove from `RecordingSession`. Raises: - ValueError: If `Video` is not in associated `RecordingSession`. + ValueError: If `video` is not in associated `RecordingSession`. """ # Remove video from camera mapping camera = self._camera_by_video.pop(video) @@ -96,7 +96,7 @@ class Camera: matrix: Intrinsic camera matrix of size (3, 3) and type float64. dist: Radial-tangential distortion coefficients [k_1, k_2, p_1, p_2, k_3] of size (5,) and type float64. - size: Image size of camera in pixels of size (2,) and type int. + size: Image size (width, height) of camera in pixels of size (2,) and type int. rvec: Rotation vector in unnormalized axis-angle representation of size (3,) and type float64. tvec: Translation vector of size (3,) and type float64. @@ -161,7 +161,7 @@ def _validate_shape(self, attribute: attrs.Attribute, value): if np.shape(value) != expected_shape: raise ValueError( f"{attribute.name} must be a {expected_type} of size {expected_shape}, " - f"but recieved shape: {np.shape(value)} and type: {type(value)} for " + f"but received shape: {np.shape(value)} and type: {type(value)} for " f"value: {value}" ) From 7f526c5990780a4d0e80ff892acce5503f3582c0 Mon Sep 17 00:00:00 2001 From: roomrys <38435167+roomrys@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:02:02 -0800 Subject: [PATCH 2/5] Retain points shape when project --- sleap_io/model/camera.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/sleap_io/model/camera.py b/sleap_io/model/camera.py index a440fe17..30051d41 100644 --- a/sleap_io/model/camera.py +++ b/sleap_io/model/camera.py @@ -255,12 +255,25 @@ def project(self, points: np.ndarray) -> np.ndarray: """Project 3D points to 2D using camera matrix and distortion coefficients. Args: - points: 3D points to project of shape (N, 3) or (N, 1, 3). + points: 3D points to project of shape (..., 3) where "..." is any number of + dimensions (including 0). Returns: - Projected 2D points of shape (N, 1, 2). + Projected 2D points of shape (..., 2) where "..." is the same as the input + "..." dimensions. """ - points = points.reshape(-1, 1, 3) + # Validate points in + points_shape = points.shape + try: + if points_shape[-1] != 3: + raise ValueError + points = points.reshape(-1, 1, 3) + except Exception as e: + raise ValueError( + "Expected points to be an array of 3D points of shape (..., 3) where " + "'...' is any number of non-zero dimensions, but received shape " + f"{points_shape}.\n\n{e}" + ) out, _ = cv2.projectPoints( points, self.rvec, @@ -268,7 +281,7 @@ def project(self, points: np.ndarray) -> np.ndarray: self.matrix, self.dist, ) - return out + return out.reshape(*points_shape[:-1], 2) def get_video(self, session: RecordingSession) -> Video | None: """Get video associated with recording session. From 6530e1f5ff5557373960fe8acfd0e744c34bfbf1 Mon Sep 17 00:00:00 2001 From: roomrys <38435167+roomrys@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:02:43 -0800 Subject: [PATCH 3/5] Test points shape is retained on project --- tests/model/test_camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/model/test_camera.py b/tests/model/test_camera.py index 243e672d..58feb5c4 100644 --- a/tests/model/test_camera.py +++ b/tests/model/test_camera.py @@ -225,11 +225,11 @@ def test_camera_project(): points = np.random.rand(10, 3) projected_points = camera.project(points) - assert projected_points.shape == (points.shape[0], 1, 2) + assert projected_points.shape == (*points.shape[:-1], 2) points = np.random.rand(10, 1, 3) projected_points = camera.project(points) - assert projected_points.shape == (points.shape[0], 1, 2) + assert projected_points.shape == (*points.shape[:-1], 2) def test_camera_get_video(): From 0d77864ef48a54892ecab8954c687234e40e1998 Mon Sep 17 00:00:00 2001 From: roomrys <38435167+roomrys@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:03:37 -0800 Subject: [PATCH 4/5] Add to/from dict methods --- sleap_io/model/camera.py | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/sleap_io/model/camera.py b/sleap_io/model/camera.py index 30051d41..c8c56987 100644 --- a/sleap_io/model/camera.py +++ b/sleap_io/model/camera.py @@ -294,6 +294,63 @@ def get_video(self, session: RecordingSession) -> Video | None: """ return session.get_video(camera=self) + def to_dict(self) -> dict: + """Convert `Camera` to dictionary. + + Returns: + Dictionary containing camera information with the following keys: + - name: Camera name. + - size: Image size (width, height) of camera in pixels of size (2,) and + type int. + - matrix: Intrinsic camera matrix of size (3, 3) and type float64. + - distortions: Radial-tangential distortion coefficients + [k_1, k_2, p_1, p_2, k_3] of size (5,) and type float64. + - rotation: Rotation vector in unnormalized axis-angle representation of + size (3,) and type float64. + - translation: Translation vector of size (3,) and type float64. + """ + camera_dict = { + "name": self.name, + "size": list(self.size), + "matrix": self.matrix.tolist(), + "distortions": self.dist.tolist(), + "rotation": self.rvec.tolist(), + "translation": self.tvec.tolist(), + } + + return camera_dict + + @classmethod + def from_dict(cls, camera_dict: dict) -> Camera: + """Create `Camera` from dictionary. + + Args: + camera_dict: Dictionary containing camera information with the following + keys: + - name: Camera name. + - size: Image size (width, height) of camera in pixels of size (2,) and + type int. + - matrix: Intrinsic camera matrix of size (3, 3) and type float64. + - distortions: Radial-tangential distortion coefficients + [k_1, k_2, p_1, p_2, k_3] of size (5,) and type float64. + - rotation: Rotation vector in unnormalized axis-angle representation of + size (3,) and type float64. + - translation: Translation vector of size (3,) and type float64. + + Returns: + `Camera` object created from dictionary. + """ + camera = cls( + name=camera_dict["name"], + size=camera_dict["size"], + matrix=camera_dict["matrix"], + dist=camera_dict["distortions"], + rvec=camera_dict["rotation"], + tvec=camera_dict["translation"], + ) + + return camera + # TODO: Remove this when we implement triangulation without aniposelib def __getattr__(self, name: str): """Get attribute by name. From 0edce969b76c8b9c95804ae619fdc2a746fd00b9 Mon Sep 17 00:00:00 2001 From: roomrys <38435167+roomrys@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:03:54 -0800 Subject: [PATCH 5/5] Test to/from dict methods --- tests/model/test_camera.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/model/test_camera.py b/tests/model/test_camera.py index 58feb5c4..d68a194c 100644 --- a/tests/model/test_camera.py +++ b/tests/model/test_camera.py @@ -197,6 +197,42 @@ def test_camera_extrinsic_matrix(): np.testing.assert_array_equal(camera.tvec, tvec) +def test_camera_from_dict_to_dict(): + """Test camera from_dict method.""" + + # Define camera dictionary + name = "back" + size = [1280, 1024] + matrix = [ + [762.513822135494, 0.0, 639.5], + [0.0, 762.513822135494, 511.5], + [0.0, 0.0, 1.0], + ] + distortions = [-0.2868458380166852, 0.0, 0.0, 0.0, 0.0] + rotation = [0.3571857188780474, 0.8879473292757126, 1.6832001677006176] + translation = [-555.4577842902744, -294.43494957092884, -190.82196458369515] + camera_dict = { + "name": name, + "size": size, + "matrix": matrix, + "distortions": distortions, + "rotation": rotation, + "translation": translation, + } + + # Test camera from_dict + camera = Camera.from_dict(camera_dict) + assert camera.name == "back" + assert camera.size == tuple(size) + np.testing.assert_array_almost_equal(camera.matrix, np.array(matrix)) + np.testing.assert_array_almost_equal(camera.dist, np.array(distortions)) + np.testing.assert_array_almost_equal(camera.rvec, np.array(rotation)) + np.testing.assert_array_almost_equal(camera.tvec, np.array(translation)) + + # Test camera to_dict + assert camera.to_dict() == camera_dict + + def test_camera_undistort_points(): """Test camera undistort points method.""" camera = Camera(