labelr 0.10.0__py3-none-any.whl → 0.11.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/apps/datasets.py +140 -9
- labelr/apps/directus.py +212 -0
- labelr/apps/google_batch.py +38 -0
- labelr/apps/label_studio.py +260 -63
- labelr/apps/typer_description.py +2 -0
- labelr/check.py +68 -7
- labelr/config.py +57 -1
- labelr/export/object_detection.py +96 -18
- labelr/main.py +16 -0
- labelr/sample/object_detection.py +42 -13
- labelr-0.11.0.dist-info/METADATA +230 -0
- {labelr-0.10.0.dist-info → labelr-0.11.0.dist-info}/RECORD +16 -14
- {labelr-0.10.0.dist-info → labelr-0.11.0.dist-info}/WHEEL +1 -1
- labelr-0.10.0.dist-info/METADATA +0 -158
- {labelr-0.10.0.dist-info → labelr-0.11.0.dist-info}/entry_points.txt +0 -0
- {labelr-0.10.0.dist-info → labelr-0.11.0.dist-info}/licenses/LICENSE +0 -0
- {labelr-0.10.0.dist-info → labelr-0.11.0.dist-info}/top_level.txt +0 -0
labelr/apps/label_studio.py
CHANGED
|
@@ -7,25 +7,39 @@ from typing import Annotated, Optional
|
|
|
7
7
|
import typer
|
|
8
8
|
from openfoodfacts.utils import get_logger
|
|
9
9
|
|
|
10
|
-
from
|
|
10
|
+
from . import typer_description
|
|
11
|
+
from ..config import config
|
|
11
12
|
|
|
12
13
|
app = typer.Typer()
|
|
13
14
|
|
|
14
15
|
logger = get_logger(__name__)
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
def check_label_studio_api_key(api_key: str | None):
|
|
19
|
+
if not api_key:
|
|
20
|
+
raise typer.BadParameter(
|
|
21
|
+
"Label Studio API key not provided. Please provide it with the "
|
|
22
|
+
"--api-key option or set the LABELR_LABEL_STUDIO_API_KEY environment variable."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
17
26
|
@app.command()
|
|
18
27
|
def create(
|
|
19
|
-
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
20
28
|
title: Annotated[str, typer.Option(help="Project title")],
|
|
21
29
|
config_file: Annotated[
|
|
22
30
|
Path, typer.Option(help="Path to label config file", file_okay=True)
|
|
23
31
|
],
|
|
24
|
-
|
|
32
|
+
api_key: Annotated[
|
|
33
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_API_KEY)
|
|
34
|
+
] = config.label_studio_api_key,
|
|
35
|
+
label_studio_url: Annotated[
|
|
36
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_URL)
|
|
37
|
+
] = config.label_studio_url,
|
|
25
38
|
):
|
|
26
39
|
"""Create a new Label Studio project."""
|
|
27
40
|
from label_studio_sdk.client import LabelStudio
|
|
28
41
|
|
|
42
|
+
check_label_studio_api_key(api_key)
|
|
29
43
|
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
30
44
|
label_config = config_file.read_text()
|
|
31
45
|
|
|
@@ -35,7 +49,6 @@ def create(
|
|
|
35
49
|
|
|
36
50
|
@app.command()
|
|
37
51
|
def import_data(
|
|
38
|
-
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
39
52
|
project_id: Annotated[int, typer.Option(help="Label Studio Project ID")],
|
|
40
53
|
dataset_path: Annotated[
|
|
41
54
|
Path,
|
|
@@ -43,8 +56,15 @@ def import_data(
|
|
|
43
56
|
help="Path to the Label Studio dataset JSONL file", file_okay=True
|
|
44
57
|
),
|
|
45
58
|
],
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
api_key: Annotated[
|
|
60
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_API_KEY)
|
|
61
|
+
] = config.label_studio_api_key,
|
|
62
|
+
label_studio_url: Annotated[
|
|
63
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_URL)
|
|
64
|
+
] = config.label_studio_url,
|
|
65
|
+
batch_size: Annotated[
|
|
66
|
+
int, typer.Option(help="Number of tasks to import as a single batch")
|
|
67
|
+
] = 25,
|
|
48
68
|
):
|
|
49
69
|
"""Import tasks from a dataset file to a Label Studio project.
|
|
50
70
|
|
|
@@ -56,6 +76,7 @@ def import_data(
|
|
|
56
76
|
import tqdm
|
|
57
77
|
from label_studio_sdk.client import LabelStudio
|
|
58
78
|
|
|
79
|
+
check_label_studio_api_key(api_key)
|
|
59
80
|
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
60
81
|
|
|
61
82
|
with dataset_path.open("rt") as f:
|
|
@@ -67,12 +88,17 @@ def import_data(
|
|
|
67
88
|
|
|
68
89
|
@app.command()
|
|
69
90
|
def update_prediction(
|
|
70
|
-
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
71
91
|
project_id: Annotated[int, typer.Option(help="Label Studio project ID")],
|
|
72
|
-
|
|
92
|
+
api_key: Annotated[
|
|
93
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_API_KEY)
|
|
94
|
+
] = config.label_studio_api_key,
|
|
95
|
+
label_studio_url: Annotated[
|
|
96
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_URL)
|
|
97
|
+
] = config.label_studio_url,
|
|
73
98
|
):
|
|
74
99
|
from label_studio_sdk.client import LabelStudio
|
|
75
100
|
|
|
101
|
+
check_label_studio_api_key(api_key)
|
|
76
102
|
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
77
103
|
|
|
78
104
|
for task in ls.tasks.list(project=project_id, fields="all"):
|
|
@@ -91,16 +117,7 @@ def add_split(
|
|
|
91
117
|
train_split: Annotated[
|
|
92
118
|
float, typer.Option(help="fraction of samples to add in train split")
|
|
93
119
|
],
|
|
94
|
-
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
95
120
|
project_id: Annotated[int, typer.Option(help="Label Studio project ID")],
|
|
96
|
-
split_name: Annotated[
|
|
97
|
-
Optional[str],
|
|
98
|
-
typer.Option(
|
|
99
|
-
help="name of the split associated "
|
|
100
|
-
"with the task ID file. If --task-id-file is not provided, "
|
|
101
|
-
"this field is ignored."
|
|
102
|
-
),
|
|
103
|
-
] = None,
|
|
104
121
|
train_split_name: Annotated[
|
|
105
122
|
str,
|
|
106
123
|
typer.Option(help="name of the train split"),
|
|
@@ -110,13 +127,33 @@ def add_split(
|
|
|
110
127
|
typer.Option(help="name of the validation split"),
|
|
111
128
|
] = "val",
|
|
112
129
|
task_id_file: Annotated[
|
|
113
|
-
|
|
130
|
+
Path | None,
|
|
114
131
|
typer.Option(help="path of a text file containing IDs of samples"),
|
|
115
132
|
] = None,
|
|
133
|
+
split_name: Annotated[
|
|
134
|
+
str | None,
|
|
135
|
+
typer.Option(
|
|
136
|
+
help="name of the split associated "
|
|
137
|
+
"with the task ID file. If --task-id-file is not provided, "
|
|
138
|
+
"this field is ignored."
|
|
139
|
+
),
|
|
140
|
+
] = None,
|
|
116
141
|
overwrite: Annotated[
|
|
117
142
|
bool, typer.Option(help="overwrite existing split field")
|
|
118
143
|
] = False,
|
|
119
|
-
|
|
144
|
+
view_id: Annotated[
|
|
145
|
+
int | None,
|
|
146
|
+
typer.Option(
|
|
147
|
+
help="ID of the Label Studio view, if any. This option is useful "
|
|
148
|
+
"to filter the task to process."
|
|
149
|
+
),
|
|
150
|
+
] = None,
|
|
151
|
+
api_key: Annotated[
|
|
152
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_API_KEY)
|
|
153
|
+
] = config.label_studio_api_key,
|
|
154
|
+
label_studio_url: Annotated[
|
|
155
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_URL)
|
|
156
|
+
] = config.label_studio_url,
|
|
120
157
|
):
|
|
121
158
|
"""Update the split field of tasks in a Label Studio project.
|
|
122
159
|
|
|
@@ -124,7 +161,7 @@ def add_split(
|
|
|
124
161
|
|
|
125
162
|
If `--task-id-file` is provided, it should contain a list of task IDs,
|
|
126
163
|
one per line. The split field of these tasks will be updated to the value
|
|
127
|
-
of `--split-name`.
|
|
164
|
+
of `--split-name`. The `--train-split` value is ignored in this case.
|
|
128
165
|
|
|
129
166
|
If `--task-id-file` is not provided, the split field of all tasks in the
|
|
130
167
|
project will be updated based on the `train_split` probability.
|
|
@@ -133,12 +170,16 @@ def add_split(
|
|
|
133
170
|
|
|
134
171
|
In both cases, tasks with a non-null split field are not updated unless
|
|
135
172
|
the `--overwrite` flag is provided.
|
|
173
|
+
|
|
174
|
+
The `--view-id` option can be used to only assign the split on a subset
|
|
175
|
+
of the tasks.
|
|
136
176
|
"""
|
|
137
177
|
import random
|
|
138
178
|
|
|
139
179
|
from label_studio_sdk import Task
|
|
140
180
|
from label_studio_sdk.client import LabelStudio
|
|
141
181
|
|
|
182
|
+
check_label_studio_api_key(api_key)
|
|
142
183
|
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
143
184
|
|
|
144
185
|
task_ids = None
|
|
@@ -149,14 +190,17 @@ def add_split(
|
|
|
149
190
|
)
|
|
150
191
|
task_ids = task_id_file.read_text().strip().split("\n")
|
|
151
192
|
|
|
152
|
-
for task in ls.tasks.list(project=project_id, fields="all"):
|
|
193
|
+
for task in ls.tasks.list(project=project_id, fields="all", view=view_id):
|
|
153
194
|
task: Task
|
|
154
195
|
task_id = task.id
|
|
155
196
|
|
|
156
197
|
split = task.data.get("split")
|
|
157
198
|
if split is None or overwrite:
|
|
158
|
-
if task_ids
|
|
159
|
-
|
|
199
|
+
if task_ids:
|
|
200
|
+
if str(task_id) in task_ids:
|
|
201
|
+
split = split_name
|
|
202
|
+
else:
|
|
203
|
+
continue
|
|
160
204
|
else:
|
|
161
205
|
split = (
|
|
162
206
|
train_split_name
|
|
@@ -170,12 +214,16 @@ def add_split(
|
|
|
170
214
|
|
|
171
215
|
@app.command()
|
|
172
216
|
def annotate_from_prediction(
|
|
173
|
-
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
174
217
|
project_id: Annotated[int, typer.Option(help="Label Studio project ID")],
|
|
218
|
+
api_key: Annotated[
|
|
219
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_API_KEY)
|
|
220
|
+
] = config.label_studio_api_key,
|
|
175
221
|
updated_by: Annotated[
|
|
176
222
|
Optional[int], typer.Option(help="User ID to declare as annotator")
|
|
177
223
|
] = None,
|
|
178
|
-
label_studio_url:
|
|
224
|
+
label_studio_url: Annotated[
|
|
225
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_URL)
|
|
226
|
+
] = config.label_studio_url,
|
|
179
227
|
):
|
|
180
228
|
"""Create annotations for all tasks from predictions.
|
|
181
229
|
|
|
@@ -186,6 +234,7 @@ def annotate_from_prediction(
|
|
|
186
234
|
from label_studio_sdk.client import LabelStudio
|
|
187
235
|
from label_studio_sdk.types.task import Task
|
|
188
236
|
|
|
237
|
+
check_label_studio_api_key(api_key)
|
|
189
238
|
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
190
239
|
|
|
191
240
|
task: Task
|
|
@@ -203,42 +252,55 @@ def annotate_from_prediction(
|
|
|
203
252
|
)
|
|
204
253
|
|
|
205
254
|
|
|
206
|
-
class PredictorBackend(enum.
|
|
207
|
-
ultralytics =
|
|
208
|
-
|
|
255
|
+
class PredictorBackend(enum.StrEnum):
|
|
256
|
+
ultralytics = enum.auto()
|
|
257
|
+
ultralytics_sam3 = enum.auto()
|
|
258
|
+
robotoff = enum.auto()
|
|
209
259
|
|
|
210
260
|
|
|
211
261
|
@app.command()
|
|
212
262
|
def add_prediction(
|
|
213
|
-
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
214
263
|
project_id: Annotated[int, typer.Option(help="Label Studio Project ID")],
|
|
264
|
+
api_key: Annotated[
|
|
265
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_API_KEY)
|
|
266
|
+
] = config.label_studio_api_key,
|
|
215
267
|
view_id: Annotated[
|
|
216
|
-
|
|
268
|
+
int | None,
|
|
217
269
|
typer.Option(
|
|
218
270
|
help="Label Studio View ID to filter tasks. If not provided, all tasks in the "
|
|
219
271
|
"project are processed."
|
|
220
272
|
),
|
|
221
273
|
] = None,
|
|
222
274
|
model_name: Annotated[
|
|
223
|
-
str,
|
|
275
|
+
str | None,
|
|
224
276
|
typer.Option(
|
|
225
277
|
help="Name of the object detection model to run (for Robotoff server) or "
|
|
226
|
-
"of the Ultralytics zero-shot model to run."
|
|
278
|
+
"of the Ultralytics zero-shot model to run. If using Ultralytics backend "
|
|
279
|
+
"and no model name is provided, the default is yolov8x-worldv2.pt. "
|
|
280
|
+
"If using ultralytics_sam3 backend, the model name is ignored."
|
|
281
|
+
),
|
|
282
|
+
] = None,
|
|
283
|
+
skip_existing: Annotated[
|
|
284
|
+
bool,
|
|
285
|
+
typer.Option(
|
|
286
|
+
help="Skip tasks that already have predictions",
|
|
227
287
|
),
|
|
228
|
-
] =
|
|
288
|
+
] = True,
|
|
229
289
|
server_url: Annotated[
|
|
230
|
-
|
|
231
|
-
typer.Option(
|
|
290
|
+
str | None,
|
|
291
|
+
typer.Option(
|
|
292
|
+
help="The Robotoff URL if the backend is robotoff. If the backend is "
|
|
293
|
+
"different than robotoff, this option is ignored."
|
|
294
|
+
),
|
|
232
295
|
] = "https://robotoff.openfoodfacts.org",
|
|
233
296
|
backend: Annotated[
|
|
234
297
|
PredictorBackend,
|
|
235
298
|
typer.Option(
|
|
236
|
-
help="
|
|
237
|
-
"the prediction or Robotoff server."
|
|
299
|
+
help="The prediction backend, possible options are: `ultralytics`, `ultralytics_sam3` and `robotoff`"
|
|
238
300
|
),
|
|
239
301
|
] = PredictorBackend.ultralytics,
|
|
240
302
|
labels: Annotated[
|
|
241
|
-
|
|
303
|
+
list[str] | None,
|
|
242
304
|
typer.Option(
|
|
243
305
|
help="List of class labels to use for Yolo model. If you're using Yolo-World or other "
|
|
244
306
|
"zero-shot models, this is the list of label names that are going to be provided to the "
|
|
@@ -247,15 +309,20 @@ def add_prediction(
|
|
|
247
309
|
),
|
|
248
310
|
] = None,
|
|
249
311
|
label_mapping: Annotated[
|
|
250
|
-
|
|
251
|
-
typer.Option(
|
|
312
|
+
str | None,
|
|
313
|
+
typer.Option(
|
|
314
|
+
help='Mapping of model labels to class names, as a JSON string. Example: \'{"price tag": "price-tag"}\''
|
|
315
|
+
),
|
|
252
316
|
] = None,
|
|
253
|
-
label_studio_url:
|
|
317
|
+
label_studio_url: Annotated[
|
|
318
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_URL)
|
|
319
|
+
] = config.label_studio_url,
|
|
254
320
|
threshold: Annotated[
|
|
255
|
-
|
|
321
|
+
float | None,
|
|
256
322
|
typer.Option(
|
|
257
323
|
help="Confidence threshold for selecting bounding boxes. The default is 0.3 "
|
|
258
|
-
"for robotoff backend
|
|
324
|
+
"for robotoff backend, 0.1 for ultralytics backend and 0.25 for "
|
|
325
|
+
"ultralytics_sam3 backend."
|
|
259
326
|
),
|
|
260
327
|
] = None,
|
|
261
328
|
max_det: Annotated[int, typer.Option(help="Maximum numbers of detections")] = 300,
|
|
@@ -270,14 +337,24 @@ def add_prediction(
|
|
|
270
337
|
typer.Option(help="Raise an error if image download fails"),
|
|
271
338
|
] = True,
|
|
272
339
|
model_version: Annotated[
|
|
273
|
-
|
|
274
|
-
typer.Option(
|
|
340
|
+
str | None,
|
|
341
|
+
typer.Option(
|
|
342
|
+
help="Set the model version field of the prediction sent to Label Studio. "
|
|
343
|
+
"This is used to track which model generated the prediction."
|
|
344
|
+
),
|
|
345
|
+
] = None,
|
|
346
|
+
imgsz: Annotated[
|
|
347
|
+
int | None,
|
|
348
|
+
typer.Option(
|
|
349
|
+
help="Image size to use for Ultralytics models. If not provided, "
|
|
350
|
+
"the default size of the model is used."
|
|
351
|
+
),
|
|
275
352
|
] = None,
|
|
276
353
|
):
|
|
277
|
-
"""Add predictions as pre-annotations to Label Studio tasks
|
|
278
|
-
for an object detection model running on Triton Inference Server."""
|
|
354
|
+
"""Add predictions as pre-annotations to Label Studio tasks."""
|
|
279
355
|
|
|
280
356
|
import tqdm
|
|
357
|
+
from huggingface_hub import hf_hub_download
|
|
281
358
|
from label_studio_sdk.client import LabelStudio
|
|
282
359
|
from openfoodfacts.utils import get_image_from_url, http_session
|
|
283
360
|
from PIL import Image
|
|
@@ -287,6 +364,8 @@ def add_prediction(
|
|
|
287
364
|
format_annotation_results_from_ultralytics,
|
|
288
365
|
)
|
|
289
366
|
|
|
367
|
+
check_label_studio_api_key(api_key)
|
|
368
|
+
|
|
290
369
|
label_mapping_dict = None
|
|
291
370
|
if label_mapping:
|
|
292
371
|
label_mapping_dict = json.loads(label_mapping)
|
|
@@ -305,7 +384,10 @@ def add_prediction(
|
|
|
305
384
|
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
306
385
|
|
|
307
386
|
if backend == PredictorBackend.ultralytics:
|
|
308
|
-
from ultralytics import YOLO
|
|
387
|
+
from ultralytics import YOLO, YOLOWorld
|
|
388
|
+
|
|
389
|
+
if model_name is None:
|
|
390
|
+
model_name = "yolov8x-worldv2.pt"
|
|
309
391
|
|
|
310
392
|
if labels is None:
|
|
311
393
|
raise typer.BadParameter("Labels are required for Ultralytics backend")
|
|
@@ -315,9 +397,33 @@ def add_prediction(
|
|
|
315
397
|
|
|
316
398
|
model = YOLO(model_name)
|
|
317
399
|
if hasattr(model, "set_classes"):
|
|
400
|
+
model = typing.cast(YOLOWorld, model)
|
|
318
401
|
model.set_classes(labels)
|
|
319
402
|
else:
|
|
320
403
|
logger.warning("The model does not support setting classes directly.")
|
|
404
|
+
elif backend == PredictorBackend.ultralytics_sam3:
|
|
405
|
+
from ultralytics.models.sam import SAM3SemanticPredictor
|
|
406
|
+
|
|
407
|
+
if threshold is None:
|
|
408
|
+
threshold = 0.25
|
|
409
|
+
|
|
410
|
+
# SAM3 cannot be downloaded directly using to to a gated access. Use a
|
|
411
|
+
# proxy repo.
|
|
412
|
+
model_path = hf_hub_download(
|
|
413
|
+
"1038lab/sam3",
|
|
414
|
+
filename="sam3.pt",
|
|
415
|
+
revision="f055b060a4de0a040891ba2ebac9c5cb3c1c0132",
|
|
416
|
+
)
|
|
417
|
+
overrides = dict(
|
|
418
|
+
task="segment",
|
|
419
|
+
mode="predict",
|
|
420
|
+
model=model_path,
|
|
421
|
+
save=False,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
if imgsz is not None:
|
|
425
|
+
overrides["imgsz"] = imgsz
|
|
426
|
+
model = SAM3SemanticPredictor(overrides=overrides)
|
|
321
427
|
elif backend == PredictorBackend.robotoff:
|
|
322
428
|
if server_url is None:
|
|
323
429
|
raise typer.BadParameter("--server-url is required for Robotoff backend")
|
|
@@ -331,22 +437,32 @@ def add_prediction(
|
|
|
331
437
|
for task in tqdm.tqdm(
|
|
332
438
|
ls.tasks.list(project=project_id, view=view_id), desc="tasks"
|
|
333
439
|
):
|
|
334
|
-
if task.total_predictions
|
|
440
|
+
if not (skip_existing and task.total_predictions > 0):
|
|
335
441
|
image_url = task.data["image_url"]
|
|
336
442
|
image = typing.cast(
|
|
337
443
|
Image.Image,
|
|
338
444
|
get_image_from_url(image_url, error_raise=error_raise),
|
|
339
445
|
)
|
|
446
|
+
min_score = None
|
|
340
447
|
if backend == PredictorBackend.ultralytics:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
448
|
+
predict_kwargs = {
|
|
449
|
+
"conf": threshold,
|
|
450
|
+
"max_det": max_det,
|
|
451
|
+
}
|
|
452
|
+
if imgsz is not None:
|
|
453
|
+
predict_kwargs["imgsz"] = imgsz
|
|
454
|
+
results = model.predict(image, **predict_kwargs)[0]
|
|
346
455
|
labels = typing.cast(list[str], labels)
|
|
347
456
|
label_studio_result = format_annotation_results_from_ultralytics(
|
|
348
457
|
results, labels, label_mapping_dict
|
|
349
458
|
)
|
|
459
|
+
elif backend == PredictorBackend.ultralytics_sam3:
|
|
460
|
+
model.set_image(image)
|
|
461
|
+
results = model(text=labels)[0]
|
|
462
|
+
label_studio_result = format_annotation_results_from_ultralytics(
|
|
463
|
+
results, labels, label_mapping_dict
|
|
464
|
+
)
|
|
465
|
+
min_score = min(results.boxes.conf.tolist(), default=None)
|
|
350
466
|
elif backend == PredictorBackend.robotoff:
|
|
351
467
|
r = http_session.get(
|
|
352
468
|
f"{server_url}/api/v1/images/predict",
|
|
@@ -372,7 +488,9 @@ def add_prediction(
|
|
|
372
488
|
task=task.id,
|
|
373
489
|
result=label_studio_result,
|
|
374
490
|
model_version=model_version,
|
|
491
|
+
score=min_score,
|
|
375
492
|
)
|
|
493
|
+
logger.info("Prediction added for task: %s", task.id)
|
|
376
494
|
|
|
377
495
|
|
|
378
496
|
@app.command()
|
|
@@ -449,28 +567,61 @@ def create_config_file(
|
|
|
449
567
|
@app.command()
|
|
450
568
|
def check_dataset(
|
|
451
569
|
project_id: Annotated[int, typer.Option(help="Label Studio Project ID")],
|
|
570
|
+
view_id: Annotated[int, typer.Option(help="Label Studio View ID, if any.")] = None,
|
|
452
571
|
api_key: Annotated[
|
|
453
|
-
|
|
454
|
-
] =
|
|
455
|
-
label_studio_url:
|
|
572
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_API_KEY)
|
|
573
|
+
] = config.label_studio_api_key,
|
|
574
|
+
label_studio_url: Annotated[
|
|
575
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_URL)
|
|
576
|
+
] = config.label_studio_url,
|
|
577
|
+
delete_missing_images: Annotated[
|
|
578
|
+
bool,
|
|
579
|
+
typer.Option(help="Delete tasks with missing images from the dataset"),
|
|
580
|
+
] = False,
|
|
581
|
+
delete_duplicate_images: Annotated[
|
|
582
|
+
bool, typer.Option(help="Delete duplicate images from the dataset")
|
|
583
|
+
] = False,
|
|
456
584
|
):
|
|
457
|
-
"""
|
|
585
|
+
"""Perform sanity checks of a Label Studio dataset.
|
|
586
|
+
|
|
587
|
+
This function checks for:
|
|
588
|
+
- Tasks with missing images (404)
|
|
589
|
+
- Duplicate images based on perceptual hash (pHash)
|
|
590
|
+
- Tasks with multiple annotations
|
|
591
|
+
|
|
592
|
+
This function doesn't perform any modifications to the dataset, except
|
|
593
|
+
optionally deleting tasks with missing images if --delete-missing-images
|
|
594
|
+
is provided and tasks with duplicate images if --delete-duplicate-images
|
|
595
|
+
is provided.
|
|
596
|
+
"""
|
|
458
597
|
from label_studio_sdk.client import LabelStudio
|
|
459
598
|
|
|
460
599
|
from ..check import check_ls_dataset
|
|
461
600
|
|
|
601
|
+
check_label_studio_api_key(api_key)
|
|
462
602
|
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
463
|
-
check_ls_dataset(
|
|
603
|
+
check_ls_dataset(
|
|
604
|
+
ls=ls,
|
|
605
|
+
project_id=project_id,
|
|
606
|
+
view_id=view_id,
|
|
607
|
+
delete_missing_images=delete_missing_images,
|
|
608
|
+
delete_duplicate_images=delete_duplicate_images,
|
|
609
|
+
)
|
|
464
610
|
|
|
465
611
|
|
|
466
612
|
@app.command()
|
|
467
613
|
def list_users(
|
|
468
|
-
api_key: Annotated[
|
|
469
|
-
|
|
614
|
+
api_key: Annotated[
|
|
615
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_API_KEY)
|
|
616
|
+
] = config.label_studio_api_key,
|
|
617
|
+
label_studio_url: Annotated[
|
|
618
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_URL)
|
|
619
|
+
] = config.label_studio_url,
|
|
470
620
|
):
|
|
471
621
|
"""List all users in Label Studio."""
|
|
472
622
|
from label_studio_sdk.client import LabelStudio
|
|
473
623
|
|
|
624
|
+
check_label_studio_api_key(api_key)
|
|
474
625
|
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
475
626
|
|
|
476
627
|
for user in ls.users.list():
|
|
@@ -480,11 +631,57 @@ def list_users(
|
|
|
480
631
|
@app.command()
|
|
481
632
|
def delete_user(
|
|
482
633
|
user_id: int,
|
|
483
|
-
api_key: Annotated[
|
|
484
|
-
|
|
634
|
+
api_key: Annotated[
|
|
635
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_API_KEY)
|
|
636
|
+
] = config.label_studio_api_key,
|
|
637
|
+
label_studio_url: Annotated[
|
|
638
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_URL)
|
|
639
|
+
] = config.label_studio_url,
|
|
485
640
|
):
|
|
486
641
|
"""Delete a user from Label Studio."""
|
|
487
642
|
from label_studio_sdk.client import LabelStudio
|
|
488
643
|
|
|
644
|
+
check_label_studio_api_key(api_key)
|
|
489
645
|
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
490
646
|
ls.users.delete(user_id)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@app.command()
|
|
650
|
+
def dump_dataset(
|
|
651
|
+
project_id: Annotated[int, typer.Option(help="Label Studio Project ID")],
|
|
652
|
+
output_file: Annotated[
|
|
653
|
+
Path, typer.Option(help="Path of the output file", writable=True)
|
|
654
|
+
],
|
|
655
|
+
api_key: Annotated[
|
|
656
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_API_KEY)
|
|
657
|
+
] = config.label_studio_api_key,
|
|
658
|
+
view_id: Annotated[
|
|
659
|
+
int | None,
|
|
660
|
+
typer.Option(
|
|
661
|
+
help="ID of the Label Studio view, if any. This option is useful "
|
|
662
|
+
"to filter the tasks to dump."
|
|
663
|
+
),
|
|
664
|
+
] = None,
|
|
665
|
+
label_studio_url: Annotated[
|
|
666
|
+
str, typer.Option(help=typer_description.LABEL_STUDIO_URL)
|
|
667
|
+
] = config.label_studio_url,
|
|
668
|
+
):
|
|
669
|
+
"""Dump all the tasks of a dataset in a JSONL file.
|
|
670
|
+
|
|
671
|
+
All fields of the tasks are exported. A subset of the tasks can be
|
|
672
|
+
selected by filtering tasks based on a view (=tab) using the `--view-id`
|
|
673
|
+
option.
|
|
674
|
+
"""
|
|
675
|
+
import orjson
|
|
676
|
+
import tqdm
|
|
677
|
+
from label_studio_sdk.client import LabelStudio
|
|
678
|
+
|
|
679
|
+
check_label_studio_api_key(api_key)
|
|
680
|
+
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
681
|
+
|
|
682
|
+
with output_file.open("wb") as f:
|
|
683
|
+
for task in tqdm.tqdm(
|
|
684
|
+
ls.tasks.list(project=project_id, view=view_id), desc="tasks"
|
|
685
|
+
):
|
|
686
|
+
content = orjson.dumps(task.dict())
|
|
687
|
+
f.write(content + b"\n")
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
LABEL_STUDIO_API_KEY = """API Key to authenticate to the Label Studio server. Can also be set with the LABELR_LABEL_STUDIO_API_KEY environment variable."""
|
|
2
|
+
LABEL_STUDIO_URL = """URL of the Label Studio server. Can also be set with the LABELR_LABEL_STUDIO_URL environment variable."""
|