-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathschemas.py
More file actions
343 lines (290 loc) · 10.7 KB
/
Copy pathschemas.py
File metadata and controls
343 lines (290 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# Can these be imported from the OpenAPI spec yaml?
import datetime
import pathlib
import PIL.Image
import pydantic
from trapdata.common.logs import logger
from trapdata.ml.utils import get_image
class BoundingBox(pydantic.BaseModel):
x1: float
y1: float
x2: float
y2: float
@classmethod
def from_coords(cls, coords: list[float]):
return cls(x1=coords[0], y1=coords[1], x2=coords[2], y2=coords[3])
def to_string(self):
return f"{self.x1},{self.y1},{self.x2},{self.y2}"
def to_path(self):
return "-".join([str(int(x)) for x in [self.x1, self.y1, self.x2, self.y2]])
def to_tuple(self):
return (self.x1, self.y1, self.x2, self.y2)
class SourceImage(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="ignore", arbitrary_types_allowed=True)
id: str
url: str | None = None
b64: str | None = None
filepath: str | pathlib.Path | None = None
_pil: PIL.Image.Image | None = None
width: int | None = None
height: int | None = None
timestamp: datetime.datetime | None = None
# Validate that there is at least one of the following fields
@pydantic.model_validator(mode="after")
def validate_source(self):
if not any([self.url, self.b64, self.filepath, self._pil]):
raise ValueError(
"At least one of the following fields must be provided: "
"url, b64, filepath, pil"
)
return self
def open(self, raise_exception=False) -> PIL.Image.Image | None:
if not self._pil:
logger.warn(f"Opening image {self.id} for the first time")
self._pil = get_image(
url=self.url,
b64=self.b64,
filepath=self.filepath,
raise_exception=raise_exception,
)
else:
logger.info(f"Using already loaded image {self.id}")
if self._pil:
self.width, self.height = self._pil.size
return self._pil
class AlgorithmReference(pydantic.BaseModel):
name: str
key: str
class ClassificationResponse(pydantic.BaseModel):
classification: str
labels: list[str] | None = pydantic.Field(
default=None,
description=(
"A list of all possible labels for the model, in the correct order. "
"Omitted if the model has too many labels to include for each "
"classification in the response. Use the category map from the algorithm "
"to get the full list of labels and metadata."
),
repr=False, # Too long to display in the repr
)
scores: list[float] = pydantic.Field(
default_factory=list,
description=(
"The calibrated probabilities for each class label, most commonly "
"the softmax output."
),
repr=False, # Too long to display in the repr
)
logits: list[float] = pydantic.Field(
default_factory=list,
description=(
"The raw logits output by the model, before any calibration or "
"normalization."
),
repr=False, # Too long to display in the repr
)
inference_time: float | None = None
algorithm: AlgorithmReference
terminal: bool = True
timestamp: datetime.datetime
class DetectionResponse(pydantic.BaseModel):
source_image_id: str
bbox: BoundingBox
inference_time: float | None = None
algorithm: AlgorithmReference
timestamp: datetime.datetime
crop_image_url: str | None = None
classifications: list[ClassificationResponse] = []
rotation: float | None = pydantic.Field(
default=None,
description=(
"Rotation angle in degrees (cv2.minAreaRect convention), when "
"the detector produces oriented bounding boxes. FUTURE: "
"downstream classifiers may use this to crop a straightened "
"patch instead of the axis-aligned envelope. See "
"`docs/superpowers/specs/2026-04-14-mothbot-detection-pipeline-design.md` "
"for the proposed RotatedBoundingBox schema upgrade."
),
)
class SourceImageRequest(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="ignore")
# @TODO bring over new SourceImage & b64 validation from the lepsAI repo
id: str = pydantic.Field(
description=(
"Unique identifier for the source image. This is returned in the response."
),
examples=["e124f3b4"],
)
url: str = pydantic.Field(
description="URL to the source image to be processed.",
examples=[
"https://static.dev.insectai.org/ami-trapdata/"
"vermont/RawImages/LUNA/2022/movement/2022_06_23/20220623050407-00-235.jpg"
],
)
# b64: str | None = None
class SourceImageResponse(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="ignore")
id: str
url: str
class AlgorithmCategoryMapResponse(pydantic.BaseModel):
data: list[dict] = pydantic.Field(
default_factory=dict,
description=(
"Complete data for each label, such as id, gbif_key, explicit index, "
"source, etc."
),
examples=[
[
{"label": "Moth", "index": 0, "gbif_key": 1234},
{"label": "Not a moth", "index": 1, "gbif_key": 5678},
]
],
repr=False, # Too long to display in the repr
)
labels: list[str] = pydantic.Field(
default_factory=list,
description=(
"A simple list of string labels, in the correct index order used by "
"the model."
),
examples=[["Moth", "Not a moth"]],
repr=False, # Too long to display in the repr
)
version: str | None = pydantic.Field(
default=None,
description=(
"The version of the category map. Can be a descriptive string or a "
"version number."
),
examples=["LepNet2021-with-2023-mods"],
)
description: str | None = pydantic.Field(
default=None,
description=(
"A description of the category map used to train. e.g. source, "
"purpose and modifications."
),
examples=[
"LepNet2021 with Schmidt 2023 corrections. Limited to species with > "
"1000 observations."
],
)
uri: str | None = pydantic.Field(
default=None,
description="A URI to the category map file, could be a public web URL or object store path.",
)
class AlgorithmConfigResponse(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="ignore")
name: str
key: str = pydantic.Field(
description=(
"A unique key for an algorithm to lookup the category map (class list) "
"and other metadata."
),
)
description: str | None = None
task_type: str | None = pydantic.Field(
default=None,
description=(
"The type of task the model is trained for. e.g. 'detection', "
"'classification', 'embedding', etc."
),
examples=["detection", "classification", "segmentation", "embedding"],
)
version: int = pydantic.Field(
default=1,
description=(
"A sortable version number for the model. Increment this number when "
"the model is updated."
),
)
version_name: str | None = pydantic.Field(
default=None,
description="A complete version name e.g. '2021-01-01', 'LepNet2021'.",
)
uri: str | None = pydantic.Field(
default=None,
description="A URI to the weights or model details, could be a public web URL or object store path.",
)
category_map: AlgorithmCategoryMapResponse | None = None
class PipelineConfigRequest(pydantic.BaseModel):
"""
Configuration for the processing pipeline.
"""
example_config_param: int | None = pydantic.Field(
default=None,
description="Example of a configuration parameter for a pipeline.",
examples=[3],
)
class PipelineRequest(pydantic.BaseModel):
model_config = pydantic.ConfigDict(use_enum_values=True)
pipeline: str = pydantic.Field(
description=(
"The pipeline to use for processing the source images, specified by key"
),
examples=["vermont_quebec_moths_2023"],
)
source_images: list[SourceImageRequest] = pydantic.Field(
description="A list of source image URLs to process.",
)
config: PipelineConfigRequest = pydantic.Field(
default=PipelineConfigRequest(),
examples=[PipelineConfigRequest(example_config_param=3)],
)
class PipelineResultsResponse(pydantic.BaseModel):
model_config = pydantic.ConfigDict(use_enum_values=True)
pipeline: str = pydantic.Field(
description="The pipeline used for processing, specified by key."
)
algorithms: dict[str, AlgorithmConfigResponse] = pydantic.Field(
default_factory=dict,
description=(
"A dictionary of all algorithms used in the pipeline, including their "
"class list and other metadata, keyed by the algorithm key."
"DEPRECATED: Use the algorithms list in PipelineConfigResponse instead."
),
deprecated=True,
)
total_time: float
source_images: list[SourceImageResponse]
detections: list[DetectionResponse]
config: PipelineConfigRequest = PipelineConfigRequest()
class PipelineStageParam(pydantic.BaseModel):
"""A configurable parameter of a stage of a pipeline."""
name: str
key: str
category: str = "default"
class PipelineStage(pydantic.BaseModel):
"""A configurable stage of a pipeline."""
key: str
name: str
params: list[PipelineStageParam] = []
description: str | None = None
class PipelineConfigResponse(pydantic.BaseModel):
"""Details about a pipeline, its algorithms and category maps."""
name: str
slug: str
version: int
description: str | None = None
algorithms: list[AlgorithmConfigResponse] = []
stages: list[PipelineStage] = []
class ProcessingServiceInfoResponse(pydantic.BaseModel):
"""Information about the processing service."""
name: str = pydantic.Field(examples=["Mila Research Lab - Moth AI Services"])
description: str | None = pydantic.Field(
default=None,
examples=[
"Algorithms developed by the Mila Research Lab for analysis of moth images."
],
)
pipelines: list[PipelineConfigResponse] = pydantic.Field(
default=list,
examples=[
[
PipelineConfigResponse(
name="Random Pipeline", slug="random", version=1, algorithms=[]
),
]
],
)