labelr 0.1.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- labelr/annotate.py +16 -15
- labelr/apps/datasets.py +3 -1
- labelr/apps/projects.py +115 -34
- labelr/export.py +5 -7
- labelr/main.py +29 -225
- labelr/project_config.py +45 -0
- labelr/sample.py +39 -5
- {labelr-0.1.0.dist-info → labelr-0.3.0.dist-info}/METADATA +24 -44
- labelr-0.3.0.dist-info/RECORD +20 -0
- {labelr-0.1.0.dist-info → labelr-0.3.0.dist-info}/WHEEL +1 -1
- labelr/triton/object_detection.py +0 -241
- labelr-0.1.0.dist-info/RECORD +0 -20
- {labelr-0.1.0.dist-info → labelr-0.3.0.dist-info}/entry_points.txt +0 -0
- {labelr-0.1.0.dist-info → labelr-0.3.0.dist-info/licenses}/LICENSE +0 -0
- {labelr-0.1.0.dist-info → labelr-0.3.0.dist-info}/top_level.txt +0 -0
labelr/annotate.py
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
import random
|
|
2
2
|
import string
|
|
3
3
|
|
|
4
|
+
from openfoodfacts.types import JSONType
|
|
4
5
|
from openfoodfacts.utils import get_logger
|
|
5
6
|
|
|
6
|
-
try:
|
|
7
|
-
from ultralytics.engine.results import Results
|
|
8
|
-
except ImportError:
|
|
9
|
-
pass
|
|
10
|
-
|
|
11
|
-
from labelr.triton.object_detection import ObjectDetectionResult
|
|
12
|
-
|
|
13
7
|
logger = get_logger(__name__)
|
|
14
8
|
|
|
15
9
|
|
|
16
|
-
def
|
|
17
|
-
objects: list[
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
def format_annotation_results_from_robotoff(
|
|
11
|
+
objects: list[JSONType],
|
|
12
|
+
image_width: int,
|
|
13
|
+
image_height: int,
|
|
14
|
+
label_mapping: dict[str, str] | None = None,
|
|
15
|
+
) -> list[JSONType]:
|
|
16
|
+
"""Format annotation results from Robotoff prediction endpoint into
|
|
20
17
|
Label Studio format."""
|
|
21
18
|
annotation_results = []
|
|
22
19
|
for object_ in objects:
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
bounding_box = object_["bounding_box"]
|
|
21
|
+
label_name = object_["label"]
|
|
22
|
+
|
|
23
|
+
if label_mapping:
|
|
24
|
+
label_name = label_mapping.get(label_name, label_name)
|
|
25
|
+
|
|
25
26
|
# These are relative coordinates (between 0.0 and 1.0)
|
|
26
|
-
y_min, x_min, y_max, x_max =
|
|
27
|
+
y_min, x_min, y_max, x_max = bounding_box
|
|
27
28
|
# Make sure the coordinates are within the image boundaries,
|
|
28
29
|
# and convert them to percentages
|
|
29
30
|
y_min = min(max(0, y_min), 1.0) * 100
|
|
@@ -51,7 +52,7 @@ def format_annotation_results_from_triton(
|
|
|
51
52
|
"y": y,
|
|
52
53
|
"width": width,
|
|
53
54
|
"height": height,
|
|
54
|
-
"rectanglelabels": [
|
|
55
|
+
"rectanglelabels": [label_name],
|
|
55
56
|
},
|
|
56
57
|
},
|
|
57
58
|
)
|
labelr/apps/datasets.py
CHANGED
|
@@ -132,7 +132,9 @@ def export(
|
|
|
132
132
|
api_key: Annotated[Optional[str], typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
133
133
|
repo_id: Annotated[
|
|
134
134
|
Optional[str],
|
|
135
|
-
typer.Option(
|
|
135
|
+
typer.Option(
|
|
136
|
+
help="Hugging Face Datasets repository ID to convert (only if --from or --to is `hf`)"
|
|
137
|
+
),
|
|
136
138
|
] = None,
|
|
137
139
|
label_names: Annotated[
|
|
138
140
|
Optional[str],
|
labelr/apps/projects.py
CHANGED
|
@@ -9,7 +9,7 @@ from openfoodfacts.utils import get_logger
|
|
|
9
9
|
from PIL import Image
|
|
10
10
|
|
|
11
11
|
from ..annotate import (
|
|
12
|
-
|
|
12
|
+
format_annotation_results_from_robotoff,
|
|
13
13
|
format_annotation_results_from_ultralytics,
|
|
14
14
|
)
|
|
15
15
|
from ..config import LABEL_STUDIO_DEFAULT_URL
|
|
@@ -90,16 +90,48 @@ def add_split(
|
|
|
90
90
|
train_split: Annotated[
|
|
91
91
|
float, typer.Option(help="fraction of samples to add in train split")
|
|
92
92
|
],
|
|
93
|
+
split_name: Annotated[
|
|
94
|
+
Optional[str],
|
|
95
|
+
typer.Option(
|
|
96
|
+
help="name of the split associated "
|
|
97
|
+
"with the task ID file. If --task-id-file is not provided, "
|
|
98
|
+
"this field is ignored."
|
|
99
|
+
),
|
|
100
|
+
],
|
|
93
101
|
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
94
102
|
project_id: Annotated[int, typer.Option(help="Label Studio project ID")],
|
|
103
|
+
train_split_name: Annotated[
|
|
104
|
+
str,
|
|
105
|
+
typer.Option(help="name of the train split"),
|
|
106
|
+
] = "train",
|
|
107
|
+
val_split_name: Annotated[
|
|
108
|
+
str,
|
|
109
|
+
typer.Option(help="name of the validation split"),
|
|
110
|
+
] = "val",
|
|
111
|
+
task_id_file: Annotated[
|
|
112
|
+
Optional[Path],
|
|
113
|
+
typer.Option(help="path of a text file containing IDs of samples"),
|
|
114
|
+
] = None,
|
|
115
|
+
overwrite: Annotated[
|
|
116
|
+
bool, typer.Option(help="overwrite existing split field")
|
|
117
|
+
] = False,
|
|
95
118
|
label_studio_url: str = LABEL_STUDIO_DEFAULT_URL,
|
|
96
119
|
):
|
|
97
120
|
"""Update the split field of tasks in a Label Studio project.
|
|
98
121
|
|
|
122
|
+
The behavior of this command depends on the `--task-id-file` option.
|
|
123
|
+
|
|
124
|
+
If `--task-id-file` is provided, it should contain a list of task IDs,
|
|
125
|
+
one per line. The split field of these tasks will be updated to the value
|
|
126
|
+
of `--split-name`.
|
|
127
|
+
|
|
128
|
+
If `--task-id-file` is not provided, the split field of all tasks in the
|
|
129
|
+
project will be updated based on the `train_split` probability.
|
|
99
130
|
The split field is set to "train" with probability `train_split`, and "val"
|
|
100
|
-
otherwise.
|
|
101
|
-
|
|
102
|
-
are not updated
|
|
131
|
+
otherwise.
|
|
132
|
+
|
|
133
|
+
In both cases, tasks with a non-null split field are not updated unless
|
|
134
|
+
the `--overwrite` flag is provided.
|
|
103
135
|
"""
|
|
104
136
|
import random
|
|
105
137
|
|
|
@@ -108,11 +140,29 @@ def add_split(
|
|
|
108
140
|
|
|
109
141
|
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
110
142
|
|
|
143
|
+
task_ids = None
|
|
144
|
+
if task_id_file is not None:
|
|
145
|
+
if split_name is None or split_name not in (train_split_name, val_split_name):
|
|
146
|
+
raise typer.BadParameter(
|
|
147
|
+
"--split-name is required when using --task-id-file"
|
|
148
|
+
)
|
|
149
|
+
task_ids = task_id_file.read_text().strip().split("\n")
|
|
150
|
+
|
|
111
151
|
for task in ls.tasks.list(project=project_id, fields="all"):
|
|
112
152
|
task: Task
|
|
153
|
+
task_id = task.id
|
|
154
|
+
|
|
113
155
|
split = task.data.get("split")
|
|
114
|
-
if split is None:
|
|
115
|
-
|
|
156
|
+
if split is None or overwrite:
|
|
157
|
+
if task_ids and str(task_id) in task_ids:
|
|
158
|
+
split = split_name
|
|
159
|
+
else:
|
|
160
|
+
split = (
|
|
161
|
+
train_split_name
|
|
162
|
+
if random.random() < train_split
|
|
163
|
+
else val_split_name
|
|
164
|
+
)
|
|
165
|
+
|
|
116
166
|
logger.info("Updating task: %s, split: %s", task.id, split)
|
|
117
167
|
ls.tasks.update(task.id, data={**task.data, "split": split})
|
|
118
168
|
|
|
@@ -153,30 +203,37 @@ def annotate_from_prediction(
|
|
|
153
203
|
|
|
154
204
|
|
|
155
205
|
class PredictorBackend(enum.Enum):
|
|
156
|
-
triton = "triton"
|
|
157
206
|
ultralytics = "ultralytics"
|
|
207
|
+
robotoff = "robotoff"
|
|
158
208
|
|
|
159
209
|
|
|
160
210
|
@app.command()
|
|
161
211
|
def add_prediction(
|
|
162
212
|
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
163
213
|
project_id: Annotated[int, typer.Option(help="Label Studio Project ID")],
|
|
214
|
+
view_id: Annotated[
|
|
215
|
+
Optional[int],
|
|
216
|
+
typer.Option(
|
|
217
|
+
help="Label Studio View ID to filter tasks. If not provided, all tasks in the "
|
|
218
|
+
"project are processed."
|
|
219
|
+
),
|
|
220
|
+
] = None,
|
|
164
221
|
model_name: Annotated[
|
|
165
222
|
str,
|
|
166
223
|
typer.Option(
|
|
167
|
-
help="Name of the object detection model to run (for
|
|
224
|
+
help="Name of the object detection model to run (for Robotoff server) or "
|
|
168
225
|
"of the Ultralytics zero-shot model to run."
|
|
169
226
|
),
|
|
170
227
|
] = "yolov8x-worldv2.pt",
|
|
171
|
-
|
|
228
|
+
server_url: Annotated[
|
|
172
229
|
Optional[str],
|
|
173
|
-
typer.Option(help="
|
|
174
|
-
] =
|
|
230
|
+
typer.Option(help="The Robotoff URL if the backend is robotoff"),
|
|
231
|
+
] = "https://robotoff.openfoodfacts.org",
|
|
175
232
|
backend: Annotated[
|
|
176
233
|
PredictorBackend,
|
|
177
234
|
typer.Option(
|
|
178
|
-
help="Prediction backend: either use
|
|
179
|
-
"the prediction or
|
|
235
|
+
help="Prediction backend: either use Ultralytics to perform "
|
|
236
|
+
"the prediction or Robotoff server."
|
|
180
237
|
),
|
|
181
238
|
] = PredictorBackend.ultralytics,
|
|
182
239
|
labels: Annotated[
|
|
@@ -196,8 +253,8 @@ def add_prediction(
|
|
|
196
253
|
threshold: Annotated[
|
|
197
254
|
Optional[float],
|
|
198
255
|
typer.Option(
|
|
199
|
-
help="Confidence threshold for selecting bounding boxes. The default is 0.
|
|
200
|
-
"for
|
|
256
|
+
help="Confidence threshold for selecting bounding boxes. The default is 0.3 "
|
|
257
|
+
"for robotoff backend and 0.1 for ultralytics backend."
|
|
201
258
|
),
|
|
202
259
|
] = None,
|
|
203
260
|
max_det: Annotated[int, typer.Option(help="Maximum numbers of detections")] = 300,
|
|
@@ -221,9 +278,7 @@ def add_prediction(
|
|
|
221
278
|
|
|
222
279
|
import tqdm
|
|
223
280
|
from label_studio_sdk.client import LabelStudio
|
|
224
|
-
from openfoodfacts.utils import get_image_from_url
|
|
225
|
-
|
|
226
|
-
from labelr.triton.object_detection import ObjectDetectionModelRegistry
|
|
281
|
+
from openfoodfacts.utils import get_image_from_url, http_session
|
|
227
282
|
|
|
228
283
|
label_mapping_dict = None
|
|
229
284
|
if label_mapping:
|
|
@@ -242,8 +297,6 @@ def add_prediction(
|
|
|
242
297
|
)
|
|
243
298
|
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
244
299
|
|
|
245
|
-
model: ObjectDetectionModelRegistry | "YOLO"
|
|
246
|
-
|
|
247
300
|
if backend == PredictorBackend.ultralytics:
|
|
248
301
|
from ultralytics import YOLO
|
|
249
302
|
|
|
@@ -258,18 +311,19 @@ def add_prediction(
|
|
|
258
311
|
model.set_classes(labels)
|
|
259
312
|
else:
|
|
260
313
|
logger.warning("The model does not support setting classes directly.")
|
|
261
|
-
elif backend == PredictorBackend.
|
|
262
|
-
if
|
|
263
|
-
raise typer.BadParameter("
|
|
314
|
+
elif backend == PredictorBackend.robotoff:
|
|
315
|
+
if server_url is None:
|
|
316
|
+
raise typer.BadParameter("--server-url is required for Robotoff backend")
|
|
264
317
|
|
|
265
318
|
if threshold is None:
|
|
266
|
-
threshold = 0.
|
|
267
|
-
|
|
268
|
-
model = ObjectDetectionModelRegistry.load(model_name)
|
|
319
|
+
threshold = 0.1
|
|
320
|
+
server_url = server_url.rstrip("/")
|
|
269
321
|
else:
|
|
270
322
|
raise typer.BadParameter(f"Unsupported backend: {backend}")
|
|
271
323
|
|
|
272
|
-
for task in tqdm.tqdm(
|
|
324
|
+
for task in tqdm.tqdm(
|
|
325
|
+
ls.tasks.list(project=project_id, view=view_id), desc="tasks"
|
|
326
|
+
):
|
|
273
327
|
if task.total_predictions == 0:
|
|
274
328
|
image_url = task.data["image_url"]
|
|
275
329
|
image = typing.cast(
|
|
@@ -286,12 +340,22 @@ def add_prediction(
|
|
|
286
340
|
label_studio_result = format_annotation_results_from_ultralytics(
|
|
287
341
|
results, labels, label_mapping_dict
|
|
288
342
|
)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
343
|
+
elif backend == PredictorBackend.robotoff:
|
|
344
|
+
r = http_session.get(
|
|
345
|
+
f"{server_url}/api/v1/images/predict",
|
|
346
|
+
params={
|
|
347
|
+
"models": model_name,
|
|
348
|
+
"output_image": 0,
|
|
349
|
+
"image_url": image_url,
|
|
350
|
+
},
|
|
351
|
+
)
|
|
352
|
+
r.raise_for_status()
|
|
353
|
+
response = r.json()
|
|
354
|
+
label_studio_result = format_annotation_results_from_robotoff(
|
|
355
|
+
response["predictions"][model_name],
|
|
356
|
+
image.width,
|
|
357
|
+
image.height,
|
|
358
|
+
label_mapping_dict,
|
|
295
359
|
)
|
|
296
360
|
if dry_run:
|
|
297
361
|
logger.info("image_url: %s", image_url)
|
|
@@ -339,7 +403,7 @@ def create_dataset_file(
|
|
|
339
403
|
extra_meta["barcode"] = barcode
|
|
340
404
|
off_image_id = Path(extract_source_from_url(url)).stem
|
|
341
405
|
extra_meta["off_image_id"] = off_image_id
|
|
342
|
-
image_id = f"{barcode}
|
|
406
|
+
image_id = f"{barcode}_{off_image_id}"
|
|
343
407
|
|
|
344
408
|
image = get_image_from_url(url, error_raise=False)
|
|
345
409
|
|
|
@@ -351,3 +415,20 @@ def create_dataset_file(
|
|
|
351
415
|
image_id, url, image.width, image.height, extra_meta
|
|
352
416
|
)
|
|
353
417
|
f.write(json.dumps(label_studio_sample) + "\n")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@app.command()
|
|
421
|
+
def create_config_file(
|
|
422
|
+
output_file: Annotated[
|
|
423
|
+
Path, typer.Option(help="Path to the output label config file", exists=False)
|
|
424
|
+
],
|
|
425
|
+
labels: Annotated[
|
|
426
|
+
list[str], typer.Option(help="List of class labels to use for the model")
|
|
427
|
+
],
|
|
428
|
+
):
|
|
429
|
+
"""Create a Label Studio label config file for object detection tasks."""
|
|
430
|
+
from labelr.project_config import create_object_detection_label_config
|
|
431
|
+
|
|
432
|
+
config = create_object_detection_label_config(labels)
|
|
433
|
+
output_file.write_text(config)
|
|
434
|
+
logger.info("Label config file created: %s", output_file)
|
labelr/export.py
CHANGED
|
@@ -164,16 +164,14 @@ def export_from_ls_to_ultralytics(
|
|
|
164
164
|
|
|
165
165
|
if has_valid_annotation:
|
|
166
166
|
download_output = download_image(
|
|
167
|
-
image_url,
|
|
167
|
+
image_url, return_struct=True, error_raise=error_raise
|
|
168
168
|
)
|
|
169
169
|
if download_output is None:
|
|
170
170
|
logger.error("Failed to download image: %s", image_url)
|
|
171
171
|
continue
|
|
172
172
|
|
|
173
|
-
_, image_bytes = typing.cast(tuple[Image.Image, bytes], download_output)
|
|
174
|
-
|
|
175
173
|
with (images_dir / split / f"{image_id}.jpg").open("wb") as f:
|
|
176
|
-
f.write(image_bytes)
|
|
174
|
+
f.write(download_output.image_bytes)
|
|
177
175
|
|
|
178
176
|
with (output_dir / "data.yaml").open("w") as f:
|
|
179
177
|
f.write("path: data\n")
|
|
@@ -215,14 +213,14 @@ def export_from_hf_to_ultralytics(
|
|
|
215
213
|
|
|
216
214
|
if download_images:
|
|
217
215
|
download_output = download_image(
|
|
218
|
-
image_url,
|
|
216
|
+
image_url, return_struct=True, error_raise=error_raise
|
|
219
217
|
)
|
|
220
218
|
if download_output is None:
|
|
221
219
|
logger.error("Failed to download image: %s", image_url)
|
|
222
220
|
continue
|
|
223
|
-
|
|
221
|
+
|
|
224
222
|
with (split_images_dir / f"{image_id}.jpg").open("wb") as f:
|
|
225
|
-
f.write(image_bytes)
|
|
223
|
+
f.write(download_output.image_bytes)
|
|
226
224
|
else:
|
|
227
225
|
image = sample["image"]
|
|
228
226
|
image.save(split_images_dir / f"{image_id}.jpg")
|
labelr/main.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Annotated
|
|
1
|
+
from typing import Annotated
|
|
2
2
|
|
|
3
3
|
import typer
|
|
4
4
|
from openfoodfacts.utils import get_logger
|
|
@@ -6,7 +6,6 @@ from openfoodfacts.utils import get_logger
|
|
|
6
6
|
from labelr.apps import datasets as dataset_app
|
|
7
7
|
from labelr.apps import projects as project_app
|
|
8
8
|
from labelr.apps import users as user_app
|
|
9
|
-
from labelr.config import LABEL_STUDIO_DEFAULT_URL
|
|
10
9
|
|
|
11
10
|
app = typer.Typer(pretty_exceptions_show_locals=False)
|
|
12
11
|
|
|
@@ -14,243 +13,48 @@ logger = get_logger()
|
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
@app.command()
|
|
17
|
-
def
|
|
16
|
+
def predict(
|
|
18
17
|
model_name: Annotated[
|
|
19
18
|
str, typer.Option(help="Name of the object detection model to run")
|
|
20
19
|
],
|
|
20
|
+
label_names: Annotated[list[str], typer.Argument(help="List of label names")],
|
|
21
21
|
image_url: Annotated[str, typer.Option(help="URL of the image to process")],
|
|
22
22
|
triton_uri: Annotated[
|
|
23
23
|
str, typer.Option(help="URI (host+port) of the Triton Inference Server")
|
|
24
24
|
],
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
model = ObjectDetectionModelRegistry.get(model_name)
|
|
32
|
-
image = get_image_from_url(image_url)
|
|
33
|
-
output = model.detect_from_image(image, triton_uri=triton_uri)
|
|
34
|
-
results = output.select(threshold=threshold)
|
|
35
|
-
|
|
36
|
-
for result in results:
|
|
37
|
-
typer.echo(result)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# Temporary scripts
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@app.command()
|
|
44
|
-
def skip_rotated_images(
|
|
45
|
-
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
46
|
-
project_id: Annotated[int, typer.Option(help="Label Studio project ID")],
|
|
47
|
-
updated_by: Annotated[
|
|
48
|
-
Optional[int], typer.Option(help="User ID to declare as annotator")
|
|
49
|
-
] = None,
|
|
50
|
-
label_studio_url: str = LABEL_STUDIO_DEFAULT_URL,
|
|
51
|
-
):
|
|
52
|
-
import requests
|
|
53
|
-
import tqdm
|
|
54
|
-
from label_studio_sdk.client import LabelStudio
|
|
55
|
-
from label_studio_sdk.types.task import Task
|
|
56
|
-
from openfoodfacts.ocr import OCRResult
|
|
57
|
-
|
|
58
|
-
session = requests.Session()
|
|
59
|
-
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
60
|
-
|
|
61
|
-
task: Task
|
|
62
|
-
for task in tqdm.tqdm(
|
|
63
|
-
ls.tasks.list(project=project_id, fields="all"), desc="tasks"
|
|
64
|
-
):
|
|
65
|
-
if any(annotation["was_cancelled"] for annotation in task.annotations):
|
|
66
|
-
continue
|
|
67
|
-
|
|
68
|
-
assert task.total_annotations == 1, (
|
|
69
|
-
"Task has multiple annotations (%s)" % task.id
|
|
70
|
-
)
|
|
71
|
-
task_id = task.id
|
|
72
|
-
|
|
73
|
-
annotation = task.annotations[0]
|
|
74
|
-
annotation_id = annotation["id"]
|
|
75
|
-
|
|
76
|
-
ocr_url = task.data["image_url"].replace(".jpg", ".json")
|
|
77
|
-
ocr_result = OCRResult.from_url(ocr_url, session=session, error_raise=False)
|
|
78
|
-
|
|
79
|
-
if ocr_result is None:
|
|
80
|
-
logger.warning("No OCR result for task: %s", task_id)
|
|
81
|
-
continue
|
|
82
|
-
|
|
83
|
-
orientation_result = ocr_result.get_orientation()
|
|
84
|
-
|
|
85
|
-
if orientation_result is None:
|
|
86
|
-
# logger.info("No orientation for task: %s", task_id)
|
|
87
|
-
continue
|
|
88
|
-
|
|
89
|
-
orientation = orientation_result.orientation.name
|
|
90
|
-
if orientation != "up":
|
|
91
|
-
logger.info(
|
|
92
|
-
"Skipping rotated image for task: %s (orientation: %s)",
|
|
93
|
-
task_id,
|
|
94
|
-
orientation,
|
|
95
|
-
)
|
|
96
|
-
ls.annotations.update(
|
|
97
|
-
id=annotation_id,
|
|
98
|
-
was_cancelled=True,
|
|
99
|
-
updated_by=updated_by,
|
|
100
|
-
)
|
|
101
|
-
elif orientation == "up":
|
|
102
|
-
logger.debug("Keeping annotation for task: %s", task_id)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@app.command()
|
|
106
|
-
def fix_label(
|
|
107
|
-
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
108
|
-
project_id: Annotated[int, typer.Option(help="Label Studio project ID")],
|
|
109
|
-
label_studio_url: str = LABEL_STUDIO_DEFAULT_URL,
|
|
110
|
-
):
|
|
111
|
-
import tqdm
|
|
112
|
-
from label_studio_sdk.client import LabelStudio
|
|
113
|
-
from label_studio_sdk.types.task import Task
|
|
114
|
-
|
|
115
|
-
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
116
|
-
|
|
117
|
-
task: Task
|
|
118
|
-
for task in tqdm.tqdm(
|
|
119
|
-
ls.tasks.list(project=project_id, fields="all"), desc="tasks"
|
|
120
|
-
):
|
|
121
|
-
for prediction in task.predictions:
|
|
122
|
-
updated = False
|
|
123
|
-
if "result" in prediction:
|
|
124
|
-
for result in prediction["result"]:
|
|
125
|
-
value = result["value"]
|
|
126
|
-
if "rectanglelabels" in value and value["rectanglelabels"] != [
|
|
127
|
-
"price-tag"
|
|
128
|
-
]:
|
|
129
|
-
value["rectanglelabels"] = ["price-tag"]
|
|
130
|
-
updated = True
|
|
131
|
-
|
|
132
|
-
if updated:
|
|
133
|
-
print(f"Updating prediction {prediction['id']}, task {task.id}")
|
|
134
|
-
ls.predictions.update(prediction["id"], result=prediction["result"])
|
|
135
|
-
|
|
136
|
-
for annotation in task.annotations:
|
|
137
|
-
updated = False
|
|
138
|
-
if "result" in annotation:
|
|
139
|
-
for result in annotation["result"]:
|
|
140
|
-
value = result["value"]
|
|
141
|
-
if "rectanglelabels" in value and value["rectanglelabels"] != [
|
|
142
|
-
"price-tag"
|
|
143
|
-
]:
|
|
144
|
-
value["rectanglelabels"] = ["price-tag"]
|
|
145
|
-
updated = True
|
|
146
|
-
|
|
147
|
-
if updated:
|
|
148
|
-
print(f"Updating annotation {annotation['id']}, task {task.id}")
|
|
149
|
-
ls.annotations.update(annotation["id"], result=annotation["result"])
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
@app.command()
|
|
153
|
-
def select_price_tag_images(
|
|
154
|
-
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
155
|
-
project_id: Annotated[int, typer.Option(help="Label Studio project ID")],
|
|
156
|
-
label_studio_url: str = LABEL_STUDIO_DEFAULT_URL,
|
|
157
|
-
):
|
|
158
|
-
import typing
|
|
159
|
-
from pathlib import Path
|
|
160
|
-
from typing import Any
|
|
161
|
-
from urllib.parse import urlparse
|
|
162
|
-
|
|
163
|
-
import requests
|
|
164
|
-
import tqdm
|
|
165
|
-
from label_studio_sdk.client import LabelStudio
|
|
166
|
-
from label_studio_sdk.types.task import Task
|
|
167
|
-
|
|
168
|
-
session = requests.Session()
|
|
169
|
-
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
170
|
-
|
|
171
|
-
proof_paths = (Path(__file__).parent / "proof.txt").read_text().splitlines()
|
|
172
|
-
task: Task
|
|
173
|
-
for task in tqdm.tqdm(
|
|
174
|
-
ls.tasks.list(project=project_id, include="data,id"), desc="tasks"
|
|
175
|
-
):
|
|
176
|
-
data = typing.cast(dict[str, Any], task.data)
|
|
177
|
-
|
|
178
|
-
if "is_raw_product_shelf" in data:
|
|
179
|
-
continue
|
|
180
|
-
image_url = data["image_url"]
|
|
181
|
-
file_path = urlparse(image_url).path.replace("/img/", "")
|
|
182
|
-
r = session.get(
|
|
183
|
-
f"https://robotoff.openfoodfacts.org/api/v1/images/predict?image_url={image_url}&models=price_proof_classification",
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
if r.status_code != 200:
|
|
187
|
-
print(
|
|
188
|
-
f"Failed to get prediction for {image_url}, error: {r.text} (status: {r.status_code})"
|
|
189
|
-
)
|
|
190
|
-
continue
|
|
191
|
-
|
|
192
|
-
prediction = r.json()["predictions"]["price_proof_classification"][0]["label"]
|
|
193
|
-
|
|
194
|
-
is_raw_preduct_shelf = False
|
|
195
|
-
if prediction in ("PRICE_TAG", "SHELF"):
|
|
196
|
-
is_raw_preduct_shelf = file_path in proof_paths
|
|
197
|
-
|
|
198
|
-
ls.tasks.update(
|
|
199
|
-
task.id,
|
|
200
|
-
data={
|
|
201
|
-
**data,
|
|
202
|
-
"is_raw_product_shelf": "true" if is_raw_preduct_shelf else "false",
|
|
203
|
-
},
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
@app.command()
|
|
208
|
-
def add_predicted_category(
|
|
209
|
-
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
210
|
-
project_id: Annotated[int, typer.Option(help="Label Studio project ID")],
|
|
211
|
-
label_studio_url: str = LABEL_STUDIO_DEFAULT_URL,
|
|
25
|
+
image_size: Annotated[
|
|
26
|
+
int, typer.Option(help="Size of the image the model expects")
|
|
27
|
+
] = 640,
|
|
28
|
+
threshold: Annotated[float, typer.Option(help="Detection threshold")] = 0.5,
|
|
29
|
+
triton_model_version: str = "1",
|
|
212
30
|
):
|
|
31
|
+
"""Predict objects in an image using an object detection model served by
|
|
32
|
+
Triton."""
|
|
213
33
|
import typing
|
|
214
|
-
from typing import Any
|
|
215
|
-
|
|
216
|
-
import requests
|
|
217
|
-
import tqdm
|
|
218
|
-
from label_studio_sdk.client import LabelStudio
|
|
219
|
-
from label_studio_sdk.types.task import Task
|
|
220
|
-
|
|
221
|
-
session = requests.Session()
|
|
222
|
-
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
223
34
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
):
|
|
228
|
-
data = typing.cast(dict[str, Any], task.data)
|
|
35
|
+
from openfoodfacts.ml.object_detection import ObjectDetector
|
|
36
|
+
from openfoodfacts.utils import get_image_from_url
|
|
37
|
+
from PIL import Image
|
|
229
38
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
f"https://robotoff.openfoodfacts.org/api/v1/images/predict?image_url={image_url}&models=price_proof_classification",
|
|
235
|
-
)
|
|
39
|
+
model = ObjectDetector(
|
|
40
|
+
model_name=model_name, label_names=label_names, image_size=image_size
|
|
41
|
+
)
|
|
42
|
+
image = typing.cast(Image.Image | None, get_image_from_url(image_url))
|
|
236
43
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
)
|
|
241
|
-
continue
|
|
44
|
+
if image is None:
|
|
45
|
+
logger.error("Failed to download image from URL: %s", image_url)
|
|
46
|
+
raise typer.Abort()
|
|
242
47
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
48
|
+
output = model.detect_from_image(
|
|
49
|
+
image,
|
|
50
|
+
triton_uri=triton_uri,
|
|
51
|
+
model_version=triton_model_version,
|
|
52
|
+
threshold=threshold,
|
|
53
|
+
)
|
|
54
|
+
results = output.to_list()
|
|
246
55
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
data={
|
|
250
|
-
**data,
|
|
251
|
-
"predicted_category": predicted_category,
|
|
252
|
-
},
|
|
253
|
-
)
|
|
56
|
+
for result in results:
|
|
57
|
+
typer.echo(result)
|
|
254
58
|
|
|
255
59
|
|
|
256
60
|
app.add_typer(user_app.app, name="users", help="Manage Label Studio users")
|
labelr/project_config.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
COLORS = [
|
|
2
|
+
"blue",
|
|
3
|
+
"green",
|
|
4
|
+
"yellow",
|
|
5
|
+
"red",
|
|
6
|
+
"purple",
|
|
7
|
+
"orange",
|
|
8
|
+
"pink",
|
|
9
|
+
"brown",
|
|
10
|
+
"gray",
|
|
11
|
+
"black",
|
|
12
|
+
"white",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_object_detection_label_config(labels_names: list[str]) -> str:
|
|
17
|
+
"""Create a Label Studio label configuration for object detection tasks.
|
|
18
|
+
|
|
19
|
+
The format is the following:
|
|
20
|
+
```xml
|
|
21
|
+
<View>
|
|
22
|
+
<Image name="image" value="$image_url"/>
|
|
23
|
+
<RectangleLabels name="label" toName="image">
|
|
24
|
+
<Label value="nutrition-table" background="green"/>
|
|
25
|
+
<Label value="nutrition-table-small" background="blue"/>
|
|
26
|
+
<Label value="nutrition-table-small-energy" background="yellow"/>
|
|
27
|
+
<Label value="nutrition-table-text" background="red"/>
|
|
28
|
+
</RectangleLabels>
|
|
29
|
+
</View>
|
|
30
|
+
```
|
|
31
|
+
"""
|
|
32
|
+
if len(labels_names) > len(COLORS):
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"Too many labels ({len(labels_names)}) for the available colors ({len(COLORS)})."
|
|
35
|
+
)
|
|
36
|
+
labels_xml = "\n".join(
|
|
37
|
+
f' <Label value="{label}" background="{color}"/>'
|
|
38
|
+
for label, color in zip(labels_names, COLORS[: len(labels_names)])
|
|
39
|
+
)
|
|
40
|
+
return f"""<View>
|
|
41
|
+
<Image name="image" value="$image_url"/>
|
|
42
|
+
<RectangleLabels name="label" toName="image">
|
|
43
|
+
{labels_xml}
|
|
44
|
+
</RectangleLabels>
|
|
45
|
+
</View>"""
|
labelr/sample.py
CHANGED
|
@@ -3,7 +3,9 @@ import random
|
|
|
3
3
|
import string
|
|
4
4
|
|
|
5
5
|
import datasets
|
|
6
|
-
from openfoodfacts
|
|
6
|
+
from openfoodfacts import Flavor
|
|
7
|
+
from openfoodfacts.barcode import normalize_barcode
|
|
8
|
+
from openfoodfacts.images import download_image, generate_image_url
|
|
7
9
|
|
|
8
10
|
logger = logging.getLogger(__name__)
|
|
9
11
|
|
|
@@ -62,17 +64,49 @@ def format_object_detection_sample_from_hf(hf_sample: dict, split: str) -> dict:
|
|
|
62
64
|
annotation_results = format_annotation_results_from_hf(
|
|
63
65
|
objects, image_width, image_height
|
|
64
66
|
)
|
|
67
|
+
image_id = hf_sample["image_id"]
|
|
68
|
+
image_url = hf_meta["image_url"]
|
|
69
|
+
meta_kwargs = {}
|
|
70
|
+
|
|
71
|
+
if "off_image_id" in hf_meta:
|
|
72
|
+
# If `off_image_id` is present, we assume this is an Open Food Facts
|
|
73
|
+
# dataset sample.
|
|
74
|
+
# We normalize the barcode, and generate a new image URL
|
|
75
|
+
# to make sure that:
|
|
76
|
+
# - the image URL is valid with correct path
|
|
77
|
+
# - we use the images subdomain everywhere
|
|
78
|
+
off_image_id = hf_meta["off_image_id"]
|
|
79
|
+
meta_kwargs["off_image_id"] = off_image_id
|
|
80
|
+
barcode = normalize_barcode(hf_meta["barcode"])
|
|
81
|
+
meta_kwargs["barcode"] = barcode
|
|
82
|
+
image_id = f"{barcode}_{off_image_id}"
|
|
83
|
+
|
|
84
|
+
if ".openfoodfacts." in image_url:
|
|
85
|
+
flavor = Flavor.off
|
|
86
|
+
elif ".openbeautyfacts." in image_url:
|
|
87
|
+
flavor = Flavor.obf
|
|
88
|
+
elif ".openpetfoodfacts." in image_url:
|
|
89
|
+
flavor = Flavor.opf
|
|
90
|
+
elif ".openproductsfacts." in image_url:
|
|
91
|
+
flavor = Flavor.opf
|
|
92
|
+
else:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"Unknown Open Food Facts flavor for image URL: {image_url}"
|
|
95
|
+
)
|
|
96
|
+
image_url = generate_image_url(
|
|
97
|
+
code=barcode, image_id=off_image_id, flavor=flavor
|
|
98
|
+
)
|
|
99
|
+
|
|
65
100
|
return {
|
|
66
101
|
"data": {
|
|
67
|
-
"image_id":
|
|
68
|
-
"image_url":
|
|
102
|
+
"image_id": image_id,
|
|
103
|
+
"image_url": image_url,
|
|
69
104
|
"batch": "null",
|
|
70
105
|
"split": split,
|
|
71
106
|
"meta": {
|
|
72
107
|
"width": image_width,
|
|
73
108
|
"height": image_height,
|
|
74
|
-
|
|
75
|
-
"off_image_id": hf_meta["off_image_id"],
|
|
109
|
+
**meta_kwargs,
|
|
76
110
|
},
|
|
77
111
|
},
|
|
78
112
|
"predictions": [{"result": annotation_results}],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: labelr
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A command-line tool to manage labeling tasks with Label Studio.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
License-File: LICENSE
|
|
@@ -9,13 +9,11 @@ Requires-Dist: datasets>=3.2.0
|
|
|
9
9
|
Requires-Dist: imagehash>=4.3.1
|
|
10
10
|
Requires-Dist: label-studio-sdk>=1.0.8
|
|
11
11
|
Requires-Dist: more-itertools>=10.5.0
|
|
12
|
-
Requires-Dist: openfoodfacts>=2.
|
|
13
|
-
Requires-Dist: protobuf>=5.29.1
|
|
12
|
+
Requires-Dist: openfoodfacts>=2.9.0
|
|
14
13
|
Requires-Dist: typer>=0.15.1
|
|
15
14
|
Provides-Extra: ultralytics
|
|
16
15
|
Requires-Dist: ultralytics>=8.3.49; extra == "ultralytics"
|
|
17
|
-
|
|
18
|
-
Requires-Dist: tritonclient>=2.52.0; extra == "triton"
|
|
16
|
+
Dynamic: license-file
|
|
19
17
|
|
|
20
18
|
# Labelr
|
|
21
19
|
|
|
@@ -36,50 +34,22 @@ It currently allows to:
|
|
|
36
34
|
## Installation
|
|
37
35
|
|
|
38
36
|
Python 3.10 or higher is required to run this CLI.
|
|
39
|
-
You need to install the CLI manually for now, there is no project published on Pypi.
|
|
40
|
-
To do so:
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
### Pip
|
|
45
|
-
|
|
46
|
-
Create the virtualenv:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
python3 -m venv labelr
|
|
50
|
-
source labelr/bin/activate
|
|
51
|
-
```
|
|
52
|
-
### Conda
|
|
38
|
+
To install the CLI, simply run:
|
|
53
39
|
|
|
54
|
-
With conda:
|
|
55
40
|
```bash
|
|
56
|
-
|
|
57
|
-
conda activate labelr
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Then, clone the repository and install the requirements:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
git clone git@github.com:openfoodfacts/openfoodfacts-ai.git
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
pip install -r requirements.txt
|
|
41
|
+
pip install labelr
|
|
68
42
|
```
|
|
43
|
+
We recommend to install the CLI in a virtual environment. You can either use pip or conda for that.
|
|
69
44
|
|
|
70
|
-
|
|
45
|
+
There are two optional dependencies that you can install to use the CLI:
|
|
46
|
+
- `ultralytics`: pre-annotate object detection datasets with an ultralytics model (yolo, yolo-world)
|
|
47
|
+
- `triton`: pre-annotate object detection datasets using a model served by a Triton inference server
|
|
71
48
|
|
|
72
|
-
|
|
73
|
-
alias labelr='${VIRTUALENV_DIR}/bin/python3 ${PROJECT_PATH}/main.py'
|
|
74
|
-
```
|
|
75
|
-
or if you are using conda:
|
|
76
|
-
```bash
|
|
77
|
-
alias labelr='${CONDA_PREFIX}/bin/python3 ${PROJECT_PATH}/main.py'
|
|
78
|
-
```
|
|
49
|
+
To install the optional dependencies, you can run:
|
|
79
50
|
|
|
80
|
-
with `${VIRTUALENV_DIR}` the path to the virtual environment where you installed the CLI and `${PROJECT_PATH}` the path to the root of the project, for example:
|
|
81
51
|
```bash
|
|
82
|
-
|
|
52
|
+
pip install labelr[ultralytics,triton]
|
|
83
53
|
```
|
|
84
54
|
|
|
85
55
|
## Usage
|
|
@@ -94,7 +64,17 @@ For all the commands that interact with Label Studio, you need to provide an API
|
|
|
94
64
|
|
|
95
65
|
#### Create a project
|
|
96
66
|
|
|
97
|
-
Once you have a Label Studio instance running, you can create a project
|
|
67
|
+
Once you have a Label Studio instance running, you can create a project easily. First, you need to create a configuration file for the project. The configuration file is an XML file that defines the labeling interface and the labels to use for the project. You can find an example of a configuration file in the [Label Studio documentation](https://labelstud.io/guide/setup).
|
|
68
|
+
|
|
69
|
+
For an object detection task, a command allows you to create the configuration file automatically:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
labelr projects create-config --labels 'label1' --labels 'label2' --output-file label_config.xml
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
where `label1` and `label2` are the labels you want to use for the object detection task, and `label_config.xml` is the output file that will contain the configuration.
|
|
76
|
+
|
|
77
|
+
Then, you can create a project on Label Studio with the following command:
|
|
98
78
|
|
|
99
79
|
```bash
|
|
100
80
|
labelr projects create --title my_project --api-key API_KEY --config-file label_config.xml
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
labelr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
labelr/__main__.py,sha256=G4e95-IfhI-lOmkOBP6kQ8wl1x_Fl7dZlLOYr90K83c,66
|
|
3
|
+
labelr/annotate.py,sha256=3fJ9FYbcozcOoKuhNtzPHV8sSnp-45FsNnMc8UeBHGU,3503
|
|
4
|
+
labelr/check.py,sha256=3wK6mE0UsKvoBNm0_lyWhCMq7gxkv5r50pvO70damXY,2476
|
|
5
|
+
labelr/config.py,sha256=3RXF_NdkSuHvfVMGMlYmjlw45fU77zQkLX7gmZq7NxM,64
|
|
6
|
+
labelr/export.py,sha256=MuU7M0H1THg3FcA6IEYPKFb58nIakNCCpcItQSSwNzM,10070
|
|
7
|
+
labelr/main.py,sha256=gQ8I287mpLy3HIUWqZUyoLAfPwkphwOIzut7hEbH8tY,2135
|
|
8
|
+
labelr/project_config.py,sha256=CIHEcgSOfXb53naHWEBkTDm2V9m3abAu8C54VSzHjAs,1260
|
|
9
|
+
labelr/sample.py,sha256=WPWKJbyFDp1T-pmd1DfCpz2LWUApGJ71MvnMYkHeORU,7164
|
|
10
|
+
labelr/types.py,sha256=CahqnkLnGj23Jg0X9nftK7Jiorq50WYQqR8u9Ln4E-k,281
|
|
11
|
+
labelr/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
labelr/apps/datasets.py,sha256=OihvNIaqvny9QH64kq3oUrITRppGPz6WlJUSeObS3kE,7991
|
|
13
|
+
labelr/apps/projects.py,sha256=mF25efdNsNaOyMJindi60EHdKP6kR_7L6KFBEbqMlqM,15146
|
|
14
|
+
labelr/apps/users.py,sha256=twQSlpHxE0hrYkgrJpEFbK8lYfWnpJr8vyfLHLtdAUU,909
|
|
15
|
+
labelr-0.3.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
16
|
+
labelr-0.3.0.dist-info/METADATA,sha256=bxgaoKo6fCFfTtFI2YSjb_cSDbURB_JDsMA_5PV09gs,6583
|
|
17
|
+
labelr-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
18
|
+
labelr-0.3.0.dist-info/entry_points.txt,sha256=OACukVeR_2z54i8yQuWqqk_jdEHlyTwmTFOFBmxPp1k,43
|
|
19
|
+
labelr-0.3.0.dist-info/top_level.txt,sha256=bjZo50aGZhXIcZYpYOX4sdAQcamxh8nwfEh7A9RD_Ag,7
|
|
20
|
+
labelr-0.3.0.dist-info/RECORD,,
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import dataclasses
|
|
2
|
-
import functools
|
|
3
|
-
import logging
|
|
4
|
-
import time
|
|
5
|
-
from typing import Any, Optional
|
|
6
|
-
|
|
7
|
-
import numpy as np
|
|
8
|
-
from PIL import Image
|
|
9
|
-
|
|
10
|
-
try:
|
|
11
|
-
import grpc
|
|
12
|
-
from tritonclient.grpc import service_pb2, service_pb2_grpc
|
|
13
|
-
from tritonclient.grpc.service_pb2_grpc import GRPCInferenceServiceStub
|
|
14
|
-
except ImportError:
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
JSONType = dict[str, Any]
|
|
21
|
-
|
|
22
|
-
OBJECT_DETECTION_MODEL_VERSION = {
|
|
23
|
-
"nutriscore": "tf-nutriscore-1.0",
|
|
24
|
-
"nutrition_table": "tf-nutrition-table-1.0",
|
|
25
|
-
"universal_logo_detector": "tf-universal-logo-detector-1.0",
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
LABELS = {
|
|
29
|
-
"nutriscore": [
|
|
30
|
-
"NULL",
|
|
31
|
-
"nutriscore-a",
|
|
32
|
-
"nutriscore-b",
|
|
33
|
-
"nutriscore-c",
|
|
34
|
-
"nutriscore-d",
|
|
35
|
-
"nutriscore-e",
|
|
36
|
-
],
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
OBJECT_DETECTION_IMAGE_MAX_SIZE = (1024, 1024)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@functools.cache
|
|
43
|
-
def get_triton_inference_stub(
|
|
44
|
-
triton_uri: str,
|
|
45
|
-
) -> "GRPCInferenceServiceStub":
|
|
46
|
-
"""Return a gRPC stub for Triton Inference Server.
|
|
47
|
-
|
|
48
|
-
:param triton_uri: URI of the Triton Inference Server
|
|
49
|
-
:return: gRPC stub for Triton Inference Server
|
|
50
|
-
"""
|
|
51
|
-
triton_uri = triton_uri
|
|
52
|
-
channel = grpc.insecure_channel(triton_uri)
|
|
53
|
-
return service_pb2_grpc.GRPCInferenceServiceStub(channel)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def convert_image_to_array(image: Image.Image) -> np.ndarray:
|
|
57
|
-
"""Convert a PIL Image into a numpy array.
|
|
58
|
-
|
|
59
|
-
The image is converted to RGB if needed before generating the array.
|
|
60
|
-
|
|
61
|
-
:param image: the input image
|
|
62
|
-
:return: the generated numpy array of shape (width, height, 3)
|
|
63
|
-
"""
|
|
64
|
-
if image.mode != "RGB":
|
|
65
|
-
image = image.convert("RGB")
|
|
66
|
-
|
|
67
|
-
(im_width, im_height) = image.size
|
|
68
|
-
|
|
69
|
-
return np.array(image.getdata()).reshape((im_height, im_width, 3)).astype(np.uint8)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@dataclasses.dataclass
|
|
73
|
-
class ObjectDetectionResult:
|
|
74
|
-
bounding_box: tuple[int, int, int, int]
|
|
75
|
-
score: float
|
|
76
|
-
label: str
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@dataclasses.dataclass
|
|
80
|
-
class ObjectDetectionRawResult:
|
|
81
|
-
num_detections: int
|
|
82
|
-
detection_boxes: np.ndarray
|
|
83
|
-
detection_scores: np.ndarray
|
|
84
|
-
detection_classes: np.ndarray
|
|
85
|
-
label_names: list[str]
|
|
86
|
-
detection_masks: Optional[np.ndarray] = None
|
|
87
|
-
boxed_image: Optional[Image.Image] = None
|
|
88
|
-
|
|
89
|
-
def select(self, threshold: Optional[float] = None) -> list[ObjectDetectionResult]:
|
|
90
|
-
if threshold is None:
|
|
91
|
-
threshold = 0.5
|
|
92
|
-
|
|
93
|
-
box_masks = self.detection_scores > threshold
|
|
94
|
-
selected_boxes = self.detection_boxes[box_masks]
|
|
95
|
-
selected_scores = self.detection_scores[box_masks]
|
|
96
|
-
selected_classes = self.detection_classes[box_masks]
|
|
97
|
-
|
|
98
|
-
results = []
|
|
99
|
-
for bounding_box, score, label in zip(
|
|
100
|
-
selected_boxes, selected_scores, selected_classes
|
|
101
|
-
):
|
|
102
|
-
label_int = int(label)
|
|
103
|
-
label_str = self.label_names[label_int]
|
|
104
|
-
if label_str is not None:
|
|
105
|
-
result = ObjectDetectionResult(
|
|
106
|
-
bounding_box=tuple(bounding_box.tolist()), # type: ignore
|
|
107
|
-
score=float(score),
|
|
108
|
-
label=label_str,
|
|
109
|
-
)
|
|
110
|
-
results.append(result)
|
|
111
|
-
|
|
112
|
-
return results
|
|
113
|
-
|
|
114
|
-
def to_json(self, threshold: Optional[float] = None) -> list[JSONType]:
|
|
115
|
-
return [dataclasses.asdict(r) for r in self.select(threshold)]
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def resize_image(image: Image.Image, max_size: tuple[int, int]) -> Image.Image:
|
|
119
|
-
width, height = image.size
|
|
120
|
-
max_width, max_height = max_size
|
|
121
|
-
|
|
122
|
-
if width > max_width or height > max_height:
|
|
123
|
-
new_image = image.copy()
|
|
124
|
-
new_image.thumbnail((max_width, max_height))
|
|
125
|
-
return new_image
|
|
126
|
-
|
|
127
|
-
return image
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
class RemoteModel:
|
|
131
|
-
def __init__(self, name: str, label_names: list[str]):
|
|
132
|
-
self.name: str = name
|
|
133
|
-
self.label_names = label_names
|
|
134
|
-
|
|
135
|
-
def detect_from_image(
|
|
136
|
-
self,
|
|
137
|
-
image: Image.Image,
|
|
138
|
-
triton_uri: str,
|
|
139
|
-
) -> ObjectDetectionRawResult:
|
|
140
|
-
"""Run object detection model on an image.
|
|
141
|
-
|
|
142
|
-
:param image: the input Pillow image
|
|
143
|
-
:param triton_uri: URI of the Triton Inference Server.
|
|
144
|
-
:return: the detection result
|
|
145
|
-
"""
|
|
146
|
-
resized_image = resize_image(image, OBJECT_DETECTION_IMAGE_MAX_SIZE)
|
|
147
|
-
image_array = convert_image_to_array(resized_image)
|
|
148
|
-
grpc_stub = get_triton_inference_stub(triton_uri)
|
|
149
|
-
request = service_pb2.ModelInferRequest()
|
|
150
|
-
request.model_name = self.name
|
|
151
|
-
|
|
152
|
-
image_input = service_pb2.ModelInferRequest().InferInputTensor()
|
|
153
|
-
image_input.name = "inputs"
|
|
154
|
-
image_input.datatype = "UINT8"
|
|
155
|
-
image_input.shape.extend([1, image_array.shape[0], image_array.shape[1], 3])
|
|
156
|
-
request.inputs.extend([image_input])
|
|
157
|
-
|
|
158
|
-
for output_name in (
|
|
159
|
-
"num_detections",
|
|
160
|
-
"detection_classes",
|
|
161
|
-
"detection_scores",
|
|
162
|
-
"detection_boxes",
|
|
163
|
-
):
|
|
164
|
-
output = service_pb2.ModelInferRequest().InferRequestedOutputTensor()
|
|
165
|
-
output.name = output_name
|
|
166
|
-
request.outputs.extend([output])
|
|
167
|
-
|
|
168
|
-
request.raw_input_contents.extend([image_array.tobytes()])
|
|
169
|
-
start_time = time.monotonic()
|
|
170
|
-
response = grpc_stub.ModelInfer(request)
|
|
171
|
-
logger.debug(
|
|
172
|
-
"Inference time for %s: %s", self.name, time.monotonic() - start_time
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
if len(response.outputs) != 4:
|
|
176
|
-
raise Exception(f"expected 4 output, got {len(response.outputs)}")
|
|
177
|
-
|
|
178
|
-
if len(response.raw_output_contents) != 4:
|
|
179
|
-
raise Exception(
|
|
180
|
-
f"expected 4 raw output content, got {len(response.raw_output_contents)}"
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
output_index = {output.name: i for i, output in enumerate(response.outputs)}
|
|
184
|
-
num_detections = (
|
|
185
|
-
np.frombuffer(
|
|
186
|
-
response.raw_output_contents[output_index["num_detections"]],
|
|
187
|
-
dtype=np.float32,
|
|
188
|
-
)
|
|
189
|
-
.reshape((1, 1))
|
|
190
|
-
.astype(int)[0][0] # type: ignore
|
|
191
|
-
)
|
|
192
|
-
detection_scores = np.frombuffer(
|
|
193
|
-
response.raw_output_contents[output_index["detection_scores"]],
|
|
194
|
-
dtype=np.float32,
|
|
195
|
-
).reshape((1, -1))[0]
|
|
196
|
-
detection_classes = (
|
|
197
|
-
np.frombuffer(
|
|
198
|
-
response.raw_output_contents[output_index["detection_classes"]],
|
|
199
|
-
dtype=np.float32,
|
|
200
|
-
)
|
|
201
|
-
.reshape((1, -1))
|
|
202
|
-
.astype(int) # type: ignore
|
|
203
|
-
)[0]
|
|
204
|
-
detection_boxes = np.frombuffer(
|
|
205
|
-
response.raw_output_contents[output_index["detection_boxes"]],
|
|
206
|
-
dtype=np.float32,
|
|
207
|
-
).reshape((1, -1, 4))[0]
|
|
208
|
-
|
|
209
|
-
result = ObjectDetectionRawResult(
|
|
210
|
-
num_detections=num_detections,
|
|
211
|
-
detection_classes=detection_classes,
|
|
212
|
-
detection_boxes=detection_boxes,
|
|
213
|
-
detection_scores=detection_scores,
|
|
214
|
-
detection_masks=None,
|
|
215
|
-
label_names=self.label_names,
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
return result
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
class ObjectDetectionModelRegistry:
|
|
222
|
-
models: dict[str, RemoteModel] = {}
|
|
223
|
-
_loaded = False
|
|
224
|
-
|
|
225
|
-
@classmethod
|
|
226
|
-
def get_available_models(cls) -> list[str]:
|
|
227
|
-
cls.load_all()
|
|
228
|
-
return list(cls.models.keys())
|
|
229
|
-
|
|
230
|
-
@classmethod
|
|
231
|
-
def load(cls, name: str) -> RemoteModel:
|
|
232
|
-
label_names = LABELS[name]
|
|
233
|
-
model = RemoteModel(name, label_names)
|
|
234
|
-
cls.models[name] = model
|
|
235
|
-
return model
|
|
236
|
-
|
|
237
|
-
@classmethod
|
|
238
|
-
def get(cls, name: str) -> RemoteModel:
|
|
239
|
-
if name not in cls.models:
|
|
240
|
-
cls.load(name)
|
|
241
|
-
return cls.models[name]
|
labelr-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
labelr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
labelr/__main__.py,sha256=G4e95-IfhI-lOmkOBP6kQ8wl1x_Fl7dZlLOYr90K83c,66
|
|
3
|
-
labelr/annotate.py,sha256=8O9SO2thevo_Aa6etIUxCz2xJVXB4MwuSHj4jxz8sqQ,3441
|
|
4
|
-
labelr/check.py,sha256=3wK6mE0UsKvoBNm0_lyWhCMq7gxkv5r50pvO70damXY,2476
|
|
5
|
-
labelr/config.py,sha256=3RXF_NdkSuHvfVMGMlYmjlw45fU77zQkLX7gmZq7NxM,64
|
|
6
|
-
labelr/export.py,sha256=tcOmVnOdJidWfNouNWoQ4OJgHMbbG-bLFHkId9huiS0,10170
|
|
7
|
-
labelr/main.py,sha256=1_cZoJLBMpUV-lnaKb1XaVff4XxWjpIUZbSNQh44tPE,8715
|
|
8
|
-
labelr/sample.py,sha256=cpzvgZWVU6GzwD35tqGKEFVKAgqQbSHlWW6IL9FG15Q,5918
|
|
9
|
-
labelr/types.py,sha256=CahqnkLnGj23Jg0X9nftK7Jiorq50WYQqR8u9Ln4E-k,281
|
|
10
|
-
labelr/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
labelr/apps/datasets.py,sha256=DXU8XZx0iEHDI5SvUeI8atCKSUmj9YJwO6xTgMZDgEI,7936
|
|
12
|
-
labelr/apps/projects.py,sha256=HpulSciBVTk1sSR1uXjtHytny9t-rN8wiaQ5llNBX6Y,12420
|
|
13
|
-
labelr/apps/users.py,sha256=twQSlpHxE0hrYkgrJpEFbK8lYfWnpJr8vyfLHLtdAUU,909
|
|
14
|
-
labelr/triton/object_detection.py,sha256=QKUOWiYFH72omyZH4SdbA56JDiVA_e_N8YCSQarkzWQ,7409
|
|
15
|
-
labelr-0.1.0.dist-info/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
16
|
-
labelr-0.1.0.dist-info/METADATA,sha256=tBsu8c-LehNqjPNiCG3XjRLboQNeq2RSy9JZiv4v9Dc,6528
|
|
17
|
-
labelr-0.1.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
18
|
-
labelr-0.1.0.dist-info/entry_points.txt,sha256=OACukVeR_2z54i8yQuWqqk_jdEHlyTwmTFOFBmxPp1k,43
|
|
19
|
-
labelr-0.1.0.dist-info/top_level.txt,sha256=bjZo50aGZhXIcZYpYOX4sdAQcamxh8nwfEh7A9RD_Ag,7
|
|
20
|
-
labelr-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|