Skip to content

Commit 7363a0f

Browse files
authored
Allow camera_params update through update_items_metadata (#271)
1 parent 735e6ec commit 7363a0f

File tree

7 files changed

+206
-161
lines changed

7 files changed

+206
-161
lines changed

nucleus/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
Segment,
6060
SegmentationAnnotation,
6161
)
62+
from .camera_params import CameraParams
6263
from .connection import Connection
6364
from .constants import (
6465
ANNOTATION_METADATA_SCHEMA_KEY,
@@ -97,7 +98,7 @@
9798
from .data_transfer_object.dataset_details import DatasetDetails
9899
from .data_transfer_object.dataset_info import DatasetInfo
99100
from .dataset import Dataset
100-
from .dataset_item import CameraParams, DatasetItem, Quaternion
101+
from .dataset_item import DatasetItem
101102
from .deprecation_warning import deprecated
102103
from .errors import (
103104
DatasetItemRetrievalError,
@@ -126,6 +127,7 @@
126127
PolygonPrediction,
127128
SegmentationPrediction,
128129
)
130+
from .quaternion import Quaternion
129131
from .retry_strategy import RetryStrategy
130132
from .scene import Frame, LidarScene, VideoScene
131133
from .slice import Slice

nucleus/camera_params.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
from dataclasses import dataclass
2+
from enum import Enum
3+
from typing import Any, Dict
4+
5+
from .annotation import Point3D
6+
from .constants import (
7+
CAMERA_MODEL_KEY,
8+
CX_KEY,
9+
CY_KEY,
10+
FX_KEY,
11+
FY_KEY,
12+
HEADING_KEY,
13+
K1_KEY,
14+
K2_KEY,
15+
K3_KEY,
16+
K4_KEY,
17+
P1_KEY,
18+
P2_KEY,
19+
POSITION_KEY,
20+
)
21+
from .quaternion import Quaternion
22+
23+
REQUIRED_CAMERA_PARAMS_KEYS = {
24+
POSITION_KEY,
25+
HEADING_KEY,
26+
FX_KEY,
27+
FY_KEY,
28+
CX_KEY,
29+
CY_KEY,
30+
}
31+
32+
33+
class CameraModels(str, Enum):
34+
BROWN_CONRADY = "brown_conrady"
35+
FISHEYE = "fisheye"
36+
37+
def __contains__(self, item):
38+
try:
39+
self(item)
40+
except ValueError:
41+
return False
42+
return True
43+
44+
45+
@dataclass
46+
class CameraParams:
47+
"""Camera position/heading used to record the image.
48+
49+
Args:
50+
position (:class:`Point3D`): World-normalized position of the camera
51+
heading (:class:`Quaternion`): Vector4 indicating the quaternion of the
52+
camera direction; note that the z-axis of the camera frame
53+
represents the camera's optical axis. See `Heading Examples
54+
<https://docs.scale.com/reference/data-types-and-the-frame-objects#heading-examples>`_.
55+
fx (float): Focal length in x direction (in pixels).
56+
fy (float): Focal length in y direction (in pixels).
57+
cx (float): Principal point x value.
58+
cy (float): Principal point y value.
59+
"""
60+
61+
position: Point3D
62+
heading: Quaternion
63+
fx: float
64+
fy: float
65+
cx: float
66+
cy: float
67+
camera_model: str
68+
k1: float
69+
k2: float
70+
k3: float
71+
k4: float
72+
p1: float
73+
p2: float
74+
75+
def __post_init__(self):
76+
if self.camera_model is not None:
77+
if self.camera_model not in (k for k in CameraModels):
78+
raise ValueError(
79+
f'Invalid Camera Model, the supported options are "{CameraModels.BROWN_CONRADY}" and "{CameraModels.FISHEYE}"'
80+
)
81+
82+
@classmethod
83+
def from_json(cls, payload: Dict[str, Any]):
84+
"""Instantiates camera params object from schematized JSON dict payload."""
85+
keys = set(payload.keys())
86+
if not keys.issuperset(REQUIRED_CAMERA_PARAMS_KEYS):
87+
raise ValueError(
88+
f"The following fields must be present in the camera_params dictionary: {REQUIRED_CAMERA_PARAMS_KEYS}"
89+
)
90+
91+
return cls(
92+
Point3D.from_json(payload[POSITION_KEY]),
93+
Quaternion.from_json(payload[HEADING_KEY]),
94+
payload[FX_KEY],
95+
payload[FY_KEY],
96+
payload[CX_KEY],
97+
payload[CY_KEY],
98+
payload.get(CAMERA_MODEL_KEY, None),
99+
payload.get(K1_KEY, None),
100+
payload.get(K2_KEY, None),
101+
payload.get(K3_KEY, None),
102+
payload.get(K4_KEY, None),
103+
payload.get(P1_KEY, None),
104+
payload.get(P2_KEY, None),
105+
)
106+
107+
def to_payload(self) -> dict:
108+
"""Serializes camera params object to schematized JSON dict."""
109+
payload = {
110+
POSITION_KEY: self.position.to_payload(),
111+
HEADING_KEY: self.heading.to_payload(),
112+
FX_KEY: self.fx,
113+
FY_KEY: self.fy,
114+
CX_KEY: self.cx,
115+
CY_KEY: self.cy,
116+
}
117+
if self.k1:
118+
payload[K1_KEY] = self.k1
119+
if self.k2:
120+
payload[K2_KEY] = self.k2
121+
if self.k3:
122+
payload[K3_KEY] = self.k3
123+
if self.k4:
124+
payload[K4_KEY] = self.k4
125+
if self.p1:
126+
payload[P1_KEY] = self.p1
127+
if self.p2:
128+
payload[P2_KEY] = self.p2
129+
if self.camera_model:
130+
payload[CAMERA_MODEL_KEY] = self.camera_model
131+
return payload

nucleus/dataset.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1561,11 +1561,14 @@ def update_item_metadata(self, mapping: Dict[str, dict]):
15611561
The backed will join the specified mapping metadata to the exisiting metadata.
15621562
If there is a key-collision, the value given in the mapping will take precedence.
15631563
1564+
This method may also be used to udpate the `camera_params` for a particular set of items.
1565+
Just specify the key `camera_params` in the metadata for each reference_id along with all the necessary fields.
1566+
15641567
Args:
15651568
mapping: key-value pair of <reference_id>: <metadata>
15661569
15671570
Examples:
1568-
>>> mapping = {"item_ref_1": {"new_key": "foo"}, "item_ref_2": {"some_value": 123}}
1571+
>>> mapping = {"item_ref_1": {"new_key": "foo"}, "item_ref_2": {"some_value": 123, "camera_params": {...}}}
15691572
>>> dataset.update_item_metadata(mapping)
15701573
15711574
Returns:

nucleus/dataset_item.py

Lines changed: 2 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -5,174 +5,22 @@
55
from enum import Enum
66
from typing import Any, Dict, Optional, Sequence
77

8-
from .annotation import Point3D, is_local_path
8+
from .annotation import is_local_path
9+
from .camera_params import CameraParams
910
from .constants import (
1011
BACKEND_REFERENCE_ID_KEY,
11-
CAMERA_MODEL_KEY,
1212
CAMERA_PARAMS_KEY,
13-
CX_KEY,
14-
CY_KEY,
15-
FX_KEY,
16-
FY_KEY,
17-
HEADING_KEY,
1813
IMAGE_URL_KEY,
19-
K1_KEY,
20-
K2_KEY,
21-
K3_KEY,
22-
K4_KEY,
2314
METADATA_KEY,
2415
ORIGINAL_IMAGE_URL_KEY,
25-
P1_KEY,
26-
P2_KEY,
2716
POINTCLOUD_URL_KEY,
28-
POSITION_KEY,
2917
REFERENCE_ID_KEY,
3018
TYPE_KEY,
3119
UPLOAD_TO_SCALE_KEY,
3220
URL_KEY,
33-
W_KEY,
34-
X_KEY,
35-
Y_KEY,
36-
Z_KEY,
3721
)
3822

3923

40-
class CameraModels(str, Enum):
41-
BROWN_CONRADY = "brown_conrady"
42-
FISHEYE = "fisheye"
43-
44-
def __contains__(self, item):
45-
try:
46-
self(item)
47-
except ValueError:
48-
return False
49-
return True
50-
51-
52-
@dataclass
53-
class Quaternion:
54-
"""Quaternion objects are used to represent rotation.
55-
56-
We use the Hamilton/right-handed quaternion convention, where
57-
::
58-
59-
i^2 = j^2 = k^2 = ijk = -1
60-
61-
The quaternion represented by the tuple ``(x, y, z, w)`` is equal to
62-
``w + x*i + y*j + z*k``.
63-
64-
Parameters:
65-
x (float): The x value.
66-
y (float): The y value.
67-
x (float): The z value.
68-
w (float): The w value.
69-
"""
70-
71-
x: float
72-
y: float
73-
z: float
74-
w: float
75-
76-
@classmethod
77-
def from_json(cls, payload: Dict[str, float]):
78-
"""Instantiates quaternion object from schematized JSON dict payload."""
79-
return cls(
80-
payload[X_KEY], payload[Y_KEY], payload[Z_KEY], payload[W_KEY]
81-
)
82-
83-
def to_payload(self) -> dict:
84-
"""Serializes quaternion object to schematized JSON dict."""
85-
return {
86-
X_KEY: self.x,
87-
Y_KEY: self.y,
88-
Z_KEY: self.z,
89-
W_KEY: self.w,
90-
}
91-
92-
93-
@dataclass
94-
class CameraParams:
95-
"""Camera position/heading used to record the image.
96-
97-
Args:
98-
position (:class:`Point3D`): World-normalized position of the camera
99-
heading (:class:`Quaternion`): Vector4 indicating the quaternion of the
100-
camera direction; note that the z-axis of the camera frame
101-
represents the camera's optical axis. See `Heading Examples
102-
<https://docs.scale.com/reference/data-types-and-the-frame-objects#heading-examples>`_.
103-
fx (float): Focal length in x direction (in pixels).
104-
fy (float): Focal length in y direction (in pixels).
105-
cx (float): Principal point x value.
106-
cy (float): Principal point y value.
107-
"""
108-
109-
position: Point3D
110-
heading: Quaternion
111-
fx: float
112-
fy: float
113-
cx: float
114-
cy: float
115-
camera_model: str
116-
k1: float
117-
k2: float
118-
k3: float
119-
k4: float
120-
p1: float
121-
p2: float
122-
123-
def __post_init__(self):
124-
if self.camera_model is not None:
125-
if self.camera_model not in (k for k in CameraModels):
126-
raise ValueError(
127-
f'Invalid Camera Model, the supported options are "{CameraModels.BROWN_CONRADY}" and "{CameraModels.FISHEYE}"'
128-
)
129-
130-
@classmethod
131-
def from_json(cls, payload: Dict[str, Any]):
132-
"""Instantiates camera params object from schematized JSON dict payload."""
133-
return cls(
134-
Point3D.from_json(payload[POSITION_KEY]),
135-
Quaternion.from_json(payload[HEADING_KEY]),
136-
payload[FX_KEY],
137-
payload[FY_KEY],
138-
payload[CX_KEY],
139-
payload[CY_KEY],
140-
payload.get(CAMERA_MODEL_KEY, None),
141-
payload.get(K1_KEY, None),
142-
payload.get(K2_KEY, None),
143-
payload.get(K3_KEY, None),
144-
payload.get(K4_KEY, None),
145-
payload.get(P1_KEY, None),
146-
payload.get(P2_KEY, None),
147-
)
148-
149-
def to_payload(self) -> dict:
150-
"""Serializes camera params object to schematized JSON dict."""
151-
payload = {
152-
POSITION_KEY: self.position.to_payload(),
153-
HEADING_KEY: self.heading.to_payload(),
154-
FX_KEY: self.fx,
155-
FY_KEY: self.fy,
156-
CX_KEY: self.cx,
157-
CY_KEY: self.cy,
158-
}
159-
if self.k1:
160-
payload[K1_KEY] = self.k1
161-
if self.k2:
162-
payload[K2_KEY] = self.k2
163-
if self.k3:
164-
payload[K3_KEY] = self.k3
165-
if self.k4:
166-
payload[K4_KEY] = self.k4
167-
if self.p1:
168-
payload[P1_KEY] = self.p1
169-
if self.p2:
170-
payload[P2_KEY] = self.p2
171-
if self.camera_model:
172-
payload[CAMERA_MODEL_KEY] = self.camera_model
173-
return payload
174-
175-
17624
class DatasetItemType(Enum):
17725
IMAGE = "image"
17826
POINTCLOUD = "pointcloud"

nucleus/metadata_manager.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from enum import Enum
2-
from typing import TYPE_CHECKING, Dict
2+
from typing import TYPE_CHECKING, Dict, Optional
3+
4+
from .camera_params import CameraParams
5+
from .constants import CAMERA_PARAMS_KEY
36

47
if TYPE_CHECKING:
58
from . import NucleusClient
@@ -31,11 +34,24 @@ def __init__(
3134

3235
self._payload = self._format_mappings()
3336

37+
def _extract_camera_params(self, metadata: dict) -> Optional[CameraParams]:
38+
camera_params = metadata.get(CAMERA_PARAMS_KEY, None)
39+
if camera_params is None:
40+
return None
41+
return CameraParams.from_json(camera_params)
42+
3443
def _format_mappings(self):
35-
payload = []
44+
payloads = []
3645
for ref_id, meta in self.raw_mappings.items():
37-
payload.append({"reference_id": ref_id, "metadata": meta})
38-
return payload
46+
payload = {"reference_id": ref_id, "metadata": meta}
47+
48+
if self.level.value == ExportMetadataType.DATASET_ITEMS.value:
49+
camera_params = self._extract_camera_params(meta)
50+
if camera_params:
51+
payload[CAMERA_PARAMS_KEY] = camera_params.to_payload()
52+
53+
payloads.append(payload)
54+
return payloads
3955

4056
def update(self):
4157
payload = {"metadata": self._payload, "level": self.level.value}

0 commit comments

Comments
 (0)