labelr 0.1.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/__init__.py +0 -0
- labelr/__main__.py +4 -0
- labelr/annotate.py +107 -0
- labelr/apps/__init__.py +0 -0
- labelr/apps/datasets.py +227 -0
- labelr/apps/projects.py +353 -0
- labelr/apps/users.py +36 -0
- labelr/check.py +86 -0
- labelr/config.py +1 -0
- labelr/export.py +270 -0
- labelr/main.py +269 -0
- labelr/sample.py +186 -0
- labelr/triton/object_detection.py +241 -0
- labelr/types.py +16 -0
- labelr-0.1.0.dist-info/LICENSE +661 -0
- labelr-0.1.0.dist-info/METADATA +160 -0
- labelr-0.1.0.dist-info/RECORD +20 -0
- labelr-0.1.0.dist-info/WHEEL +5 -0
- labelr-0.1.0.dist-info/entry_points.txt +2 -0
- labelr-0.1.0.dist-info/top_level.txt +1 -0
labelr/apps/users.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from ..config import LABEL_STUDIO_DEFAULT_URL
|
|
6
|
+
|
|
7
|
+
app = typer.Typer()
|
|
8
|
+
|
|
9
|
+
# Label Studio user management
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command()
|
|
13
|
+
def list(
|
|
14
|
+
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
15
|
+
label_studio_url: str = LABEL_STUDIO_DEFAULT_URL,
|
|
16
|
+
):
|
|
17
|
+
"""List all users in Label Studio."""
|
|
18
|
+
from label_studio_sdk.client import LabelStudio
|
|
19
|
+
|
|
20
|
+
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
21
|
+
|
|
22
|
+
for user in ls.users.list():
|
|
23
|
+
print(f"{user.id:02d}: {user.email}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command()
|
|
27
|
+
def delete(
|
|
28
|
+
user_id: int,
|
|
29
|
+
api_key: Annotated[str, typer.Option(envvar="LABEL_STUDIO_API_KEY")],
|
|
30
|
+
label_studio_url: str = LABEL_STUDIO_DEFAULT_URL,
|
|
31
|
+
):
|
|
32
|
+
"""Delete a user from Label Studio."""
|
|
33
|
+
from label_studio_sdk.client import LabelStudio
|
|
34
|
+
|
|
35
|
+
ls = LabelStudio(base_url=label_studio_url, api_key=api_key)
|
|
36
|
+
ls.users.delete(user_id)
|
labelr/check.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import imagehash
|
|
5
|
+
import tqdm
|
|
6
|
+
from label_studio_sdk.client import LabelStudio
|
|
7
|
+
from openfoodfacts.utils import get_image_from_url, get_logger
|
|
8
|
+
from PIL import Image
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_ls_dataset(ls: LabelStudio, project_id: int):
|
|
14
|
+
skipped = 0
|
|
15
|
+
not_annotated = 0
|
|
16
|
+
annotated = 0
|
|
17
|
+
hash_map = defaultdict(list)
|
|
18
|
+
for task in tqdm.tqdm(
|
|
19
|
+
ls.tasks.list(project=project_id, fields="all"), desc="tasks"
|
|
20
|
+
):
|
|
21
|
+
annotations = task.annotations
|
|
22
|
+
|
|
23
|
+
if len(annotations) == 0:
|
|
24
|
+
not_annotated += 1
|
|
25
|
+
continue
|
|
26
|
+
elif len(annotations) > 1:
|
|
27
|
+
logger.warning("Task has multiple annotations: %s", task.id)
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
annotation = annotations[0]
|
|
31
|
+
|
|
32
|
+
if annotation["was_cancelled"]:
|
|
33
|
+
skipped += 1
|
|
34
|
+
|
|
35
|
+
annotated += 1
|
|
36
|
+
image_url = task.data["image_url"]
|
|
37
|
+
image = get_image_from_url(image_url)
|
|
38
|
+
image_hash = str(imagehash.phash(image))
|
|
39
|
+
hash_map[image_hash].append(task.id)
|
|
40
|
+
|
|
41
|
+
for image_hash, task_ids in hash_map.items():
|
|
42
|
+
if len(task_ids) > 1:
|
|
43
|
+
logger.warning("Duplicate images: %s", task_ids)
|
|
44
|
+
|
|
45
|
+
logger.info(
|
|
46
|
+
"Tasks - annotated: %d, skipped: %d, not annotated: %d",
|
|
47
|
+
annotated,
|
|
48
|
+
skipped,
|
|
49
|
+
not_annotated,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_local_dataset(dataset_dir: Path, remove: bool = False):
|
|
54
|
+
hash_map = defaultdict(list)
|
|
55
|
+
for path in tqdm.tqdm(dataset_dir.glob("**/*.jpg"), desc="images"):
|
|
56
|
+
if path.is_file() and path.suffix in [
|
|
57
|
+
".jpg",
|
|
58
|
+
".jpeg",
|
|
59
|
+
".png",
|
|
60
|
+
".webp",
|
|
61
|
+
".bmp",
|
|
62
|
+
".tiff",
|
|
63
|
+
".gif",
|
|
64
|
+
]:
|
|
65
|
+
image = Image.open(path)
|
|
66
|
+
image_hash = str(imagehash.phash(image))
|
|
67
|
+
logger.debug("Image hash: %s", image_hash)
|
|
68
|
+
hash_map[image_hash].append(path)
|
|
69
|
+
|
|
70
|
+
duplicated = 0
|
|
71
|
+
to_remove = []
|
|
72
|
+
for image_hash, image_paths in hash_map.items():
|
|
73
|
+
if len(image_paths) > 1:
|
|
74
|
+
logger.warning(
|
|
75
|
+
"Duplicate images: %s",
|
|
76
|
+
[str(x.relative_to(dataset_dir)) for x in image_paths],
|
|
77
|
+
)
|
|
78
|
+
duplicated += 1
|
|
79
|
+
to_remove.append(image_paths[0])
|
|
80
|
+
|
|
81
|
+
logger.info("Total duplicated groups: %d", duplicated)
|
|
82
|
+
|
|
83
|
+
if remove and to_remove:
|
|
84
|
+
for path in to_remove:
|
|
85
|
+
logger.info("Removing: %s", str(path))
|
|
86
|
+
path.unlink()
|
labelr/config.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
LABEL_STUDIO_DEFAULT_URL = "https://annotate.openfoodfacts.org"
|
labelr/export.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
import pickle
|
|
4
|
+
import random
|
|
5
|
+
import tempfile
|
|
6
|
+
import typing
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import datasets
|
|
10
|
+
import tqdm
|
|
11
|
+
from label_studio_sdk.client import LabelStudio
|
|
12
|
+
from openfoodfacts.images import download_image
|
|
13
|
+
from PIL import Image
|
|
14
|
+
|
|
15
|
+
from labelr.sample import HF_DS_FEATURES, format_object_detection_sample_to_hf
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _pickle_sample_generator(dir: Path):
|
|
21
|
+
"""Generator that yields samples from pickles in a directory."""
|
|
22
|
+
for pkl in dir.glob("*.pkl"):
|
|
23
|
+
with open(pkl, "rb") as f:
|
|
24
|
+
yield pickle.load(f)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def export_from_ls_to_hf(
|
|
28
|
+
ls: LabelStudio,
|
|
29
|
+
repo_id: str,
|
|
30
|
+
category_names: list[str],
|
|
31
|
+
project_id: int,
|
|
32
|
+
):
|
|
33
|
+
logger.info("Project ID: %d, category names: %s", project_id, category_names)
|
|
34
|
+
|
|
35
|
+
for split in ["train", "val"]:
|
|
36
|
+
logger.info("Processing split: %s", split)
|
|
37
|
+
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmp_dir_str:
|
|
39
|
+
tmp_dir = Path(tmp_dir_str)
|
|
40
|
+
logger.info("Saving samples to temporary directory: %s", tmp_dir)
|
|
41
|
+
for i, task in tqdm.tqdm(
|
|
42
|
+
enumerate(ls.tasks.list(project=project_id, fields="all")),
|
|
43
|
+
desc="tasks",
|
|
44
|
+
):
|
|
45
|
+
if task.data["split"] != split:
|
|
46
|
+
continue
|
|
47
|
+
sample = format_object_detection_sample_to_hf(
|
|
48
|
+
task.data, task.annotations, category_names
|
|
49
|
+
)
|
|
50
|
+
if sample is not None:
|
|
51
|
+
# Save output as pickle
|
|
52
|
+
with open(tmp_dir / f"{split}_{i:05}.pkl", "wb") as f:
|
|
53
|
+
pickle.dump(sample, f)
|
|
54
|
+
|
|
55
|
+
hf_ds = datasets.Dataset.from_generator(
|
|
56
|
+
functools.partial(_pickle_sample_generator, tmp_dir),
|
|
57
|
+
features=HF_DS_FEATURES,
|
|
58
|
+
)
|
|
59
|
+
hf_ds.push_to_hub(repo_id, split=split)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def export_from_ls_to_ultralytics(
|
|
63
|
+
ls: LabelStudio,
|
|
64
|
+
output_dir: Path,
|
|
65
|
+
category_names: list[str],
|
|
66
|
+
project_id: int,
|
|
67
|
+
train_ratio: float = 0.8,
|
|
68
|
+
error_raise: bool = True,
|
|
69
|
+
):
|
|
70
|
+
"""Export annotations from a Label Studio project to the Ultralytics
|
|
71
|
+
format.
|
|
72
|
+
|
|
73
|
+
The Label Studio project should be an object detection project with a
|
|
74
|
+
single rectanglelabels annotation result per task.
|
|
75
|
+
"""
|
|
76
|
+
logger.info("Project ID: %d, category names: %s", project_id, category_names)
|
|
77
|
+
|
|
78
|
+
data_dir = output_dir / "data"
|
|
79
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
split_warning_displayed = False
|
|
81
|
+
|
|
82
|
+
# NOTE: before, all images were sent to val, the last split
|
|
83
|
+
label_dir = data_dir / "labels"
|
|
84
|
+
images_dir = data_dir / "images"
|
|
85
|
+
for split in ["train", "val"]:
|
|
86
|
+
(label_dir / split).mkdir(parents=True, exist_ok=True)
|
|
87
|
+
(images_dir / split).mkdir(parents=True, exist_ok=True)
|
|
88
|
+
|
|
89
|
+
for task in tqdm.tqdm(
|
|
90
|
+
ls.tasks.list(project=project_id, fields="all"),
|
|
91
|
+
desc="tasks",
|
|
92
|
+
):
|
|
93
|
+
split = task.data.get("split")
|
|
94
|
+
|
|
95
|
+
if split is None:
|
|
96
|
+
if not split_warning_displayed:
|
|
97
|
+
logger.warning(
|
|
98
|
+
"Split information not found, assigning randomly. "
|
|
99
|
+
"To avoid this, set the `split` field in the task data."
|
|
100
|
+
)
|
|
101
|
+
split_warning_displayed = True
|
|
102
|
+
split = "train" if random.random() < train_ratio else "val"
|
|
103
|
+
|
|
104
|
+
elif split not in ["train", "val"]:
|
|
105
|
+
raise ValueError("Invalid split name: %s", split)
|
|
106
|
+
|
|
107
|
+
if len(task.annotations) > 1:
|
|
108
|
+
logger.warning("More than one annotation found, skipping")
|
|
109
|
+
continue
|
|
110
|
+
elif len(task.annotations) == 0:
|
|
111
|
+
logger.debug("No annotation found, skipping")
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
annotation = task.annotations[0]
|
|
115
|
+
if annotation["was_cancelled"] is True:
|
|
116
|
+
logger.debug("Annotation was cancelled, skipping")
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
if "image_id" not in task.data:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
"`image_id` field not found in task data. "
|
|
122
|
+
"Make sure the task data contains the `image_id` "
|
|
123
|
+
"field, which should be a unique identifier for the image."
|
|
124
|
+
)
|
|
125
|
+
if "image_url" not in task.data:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
"`image_url` field not found in task data. "
|
|
128
|
+
"Make sure the task data contains the `image_url` "
|
|
129
|
+
"field, which should be the URL of the image."
|
|
130
|
+
)
|
|
131
|
+
image_id = task.data["image_id"]
|
|
132
|
+
image_url = task.data["image_url"]
|
|
133
|
+
|
|
134
|
+
has_valid_annotation = False
|
|
135
|
+
with (label_dir / split / f"{image_id}.txt").open("w") as f:
|
|
136
|
+
if not any(
|
|
137
|
+
annotation_result["type"] == "rectanglelabels"
|
|
138
|
+
for annotation_result in annotation["result"]
|
|
139
|
+
):
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
for annotation_result in annotation["result"]:
|
|
143
|
+
if annotation_result["type"] == "rectanglelabels":
|
|
144
|
+
value = annotation_result["value"]
|
|
145
|
+
x_min = value["x"] / 100
|
|
146
|
+
y_min = value["y"] / 100
|
|
147
|
+
width = value["width"] / 100
|
|
148
|
+
height = value["height"] / 100
|
|
149
|
+
category_name = value["rectanglelabels"][0]
|
|
150
|
+
category_id = category_names.index(category_name)
|
|
151
|
+
|
|
152
|
+
# Save the labels in the Ultralytics format:
|
|
153
|
+
# - one label per line
|
|
154
|
+
# - each line is a list of 5 elements:
|
|
155
|
+
# - category_id
|
|
156
|
+
# - x_center
|
|
157
|
+
# - y_center
|
|
158
|
+
# - width
|
|
159
|
+
# - height
|
|
160
|
+
x_center = x_min + width / 2
|
|
161
|
+
y_center = y_min + height / 2
|
|
162
|
+
f.write(f"{category_id} {x_center} {y_center} {width} {height}\n")
|
|
163
|
+
has_valid_annotation = True
|
|
164
|
+
|
|
165
|
+
if has_valid_annotation:
|
|
166
|
+
download_output = download_image(
|
|
167
|
+
image_url, return_bytes=True, error_raise=error_raise
|
|
168
|
+
)
|
|
169
|
+
if download_output is None:
|
|
170
|
+
logger.error("Failed to download image: %s", image_url)
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
_, image_bytes = typing.cast(tuple[Image.Image, bytes], download_output)
|
|
174
|
+
|
|
175
|
+
with (images_dir / split / f"{image_id}.jpg").open("wb") as f:
|
|
176
|
+
f.write(image_bytes)
|
|
177
|
+
|
|
178
|
+
with (output_dir / "data.yaml").open("w") as f:
|
|
179
|
+
f.write("path: data\n")
|
|
180
|
+
f.write("train: images/train\n")
|
|
181
|
+
f.write("val: images/val\n")
|
|
182
|
+
f.write("test:\n")
|
|
183
|
+
f.write("names:\n")
|
|
184
|
+
for i, category_name in enumerate(category_names):
|
|
185
|
+
f.write(f" {i}: {category_name}\n")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def export_from_hf_to_ultralytics(
|
|
189
|
+
repo_id: str,
|
|
190
|
+
output_dir: Path,
|
|
191
|
+
download_images: bool = True,
|
|
192
|
+
error_raise: bool = True,
|
|
193
|
+
):
|
|
194
|
+
"""Export annotations from a Hugging Face dataset project to the
|
|
195
|
+
Ultralytics format.
|
|
196
|
+
|
|
197
|
+
The Label Studio project should be an object detection project with a
|
|
198
|
+
single rectanglelabels annotation result per task.
|
|
199
|
+
"""
|
|
200
|
+
logger.info("Repo ID: %s", repo_id)
|
|
201
|
+
ds = datasets.load_dataset(repo_id)
|
|
202
|
+
data_dir = output_dir / "data"
|
|
203
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
category_id_to_name = {}
|
|
205
|
+
|
|
206
|
+
for split in ["train", "val"]:
|
|
207
|
+
split_labels_dir = data_dir / "labels" / split
|
|
208
|
+
split_labels_dir.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
split_images_dir = data_dir / "images" / split
|
|
210
|
+
split_images_dir.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
|
|
212
|
+
for sample in tqdm.tqdm(ds[split], desc="samples"):
|
|
213
|
+
image_id = sample["image_id"]
|
|
214
|
+
image_url = sample["meta"]["image_url"]
|
|
215
|
+
|
|
216
|
+
if download_images:
|
|
217
|
+
download_output = download_image(
|
|
218
|
+
image_url, return_bytes=True, error_raise=error_raise
|
|
219
|
+
)
|
|
220
|
+
if download_output is None:
|
|
221
|
+
logger.error("Failed to download image: %s", image_url)
|
|
222
|
+
continue
|
|
223
|
+
_, image_bytes = download_output
|
|
224
|
+
with (split_images_dir / f"{image_id}.jpg").open("wb") as f:
|
|
225
|
+
f.write(image_bytes)
|
|
226
|
+
else:
|
|
227
|
+
image = sample["image"]
|
|
228
|
+
image.save(split_images_dir / f"{image_id}.jpg")
|
|
229
|
+
|
|
230
|
+
objects = sample["objects"]
|
|
231
|
+
bboxes = objects["bbox"]
|
|
232
|
+
category_ids = objects["category_id"]
|
|
233
|
+
category_names = objects["category_name"]
|
|
234
|
+
|
|
235
|
+
with (split_labels_dir / f"{image_id}.txt").open("w") as f:
|
|
236
|
+
for bbox, category_id, category_name in zip(
|
|
237
|
+
bboxes, category_ids, category_names
|
|
238
|
+
):
|
|
239
|
+
if category_id not in category_id_to_name:
|
|
240
|
+
category_id_to_name[category_id] = category_name
|
|
241
|
+
y_min, x_min, y_max, x_max = bbox
|
|
242
|
+
y_min = min(max(y_min, 0.0), 1.0)
|
|
243
|
+
x_min = min(max(x_min, 0.0), 1.0)
|
|
244
|
+
y_max = min(max(y_max, 0.0), 1.0)
|
|
245
|
+
x_max = min(max(x_max, 0.0), 1.0)
|
|
246
|
+
width = x_max - x_min
|
|
247
|
+
height = y_max - y_min
|
|
248
|
+
# Save the labels in the Ultralytics format:
|
|
249
|
+
# - one label per line
|
|
250
|
+
# - each line is a list of 5 elements:
|
|
251
|
+
# - category_id
|
|
252
|
+
# - x_center
|
|
253
|
+
# - y_center
|
|
254
|
+
# - width
|
|
255
|
+
# - height
|
|
256
|
+
x_center = x_min + width / 2
|
|
257
|
+
y_center = y_min + height / 2
|
|
258
|
+
f.write(f"{category_id} {x_center} {y_center} {width} {height}\n")
|
|
259
|
+
|
|
260
|
+
category_names = [
|
|
261
|
+
x[1] for x in sorted(category_id_to_name.items(), key=lambda x: x[0])
|
|
262
|
+
]
|
|
263
|
+
with (output_dir / "data.yaml").open("w") as f:
|
|
264
|
+
f.write("path: data\n")
|
|
265
|
+
f.write("train: images/train\n")
|
|
266
|
+
f.write("val: images/val\n")
|
|
267
|
+
f.write("test:\n")
|
|
268
|
+
f.write("names:\n")
|
|
269
|
+
for i, category_name in enumerate(category_names):
|
|
270
|
+
f.write(f" {i}: {category_name}\n")
|
labelr/main.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from typing import Annotated, Optional
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from openfoodfacts.utils import get_logger
|
|
5
|
+
|
|
6
|
+
from labelr.apps import datasets as dataset_app
|
|
7
|
+
from labelr.apps import projects as project_app
|
|
8
|
+
from labelr.apps import users as user_app
|
|
9
|
+
from labelr.config import LABEL_STUDIO_DEFAULT_URL
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(pretty_exceptions_show_locals=False)
|
|
12
|
+
|
|
13
|
+
logger = get_logger()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command()
|
|
17
|
+
def predict_object(
|
|
18
|
+
model_name: Annotated[
|
|
19
|
+
str, typer.Option(help="Name of the object detection model to run")
|
|
20
|
+
],
|
|
21
|
+
image_url: Annotated[str, typer.Option(help="URL of the image to process")],
|
|
22
|
+
triton_uri: Annotated[
|
|
23
|
+
str, typer.Option(help="URI (host+port) of the Triton Inference Server")
|
|
24
|
+
],
|
|
25
|
+
threshold: float = 0.5,
|
|
26
|
+
):
|
|
27
|
+
from openfoodfacts.utils import get_image_from_url
|
|
28
|
+
|
|
29
|
+
from labelr.triton.object_detection import ObjectDetectionModelRegistry
|
|
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,
|
|
212
|
+
):
|
|
213
|
+
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
|
+
|
|
224
|
+
task: Task
|
|
225
|
+
for task in tqdm.tqdm(
|
|
226
|
+
ls.tasks.list(project=project_id, include="data,id"), desc="tasks"
|
|
227
|
+
):
|
|
228
|
+
data = typing.cast(dict[str, Any], task.data)
|
|
229
|
+
|
|
230
|
+
if "predicted_category" in data:
|
|
231
|
+
continue
|
|
232
|
+
image_url = data["image_url"]
|
|
233
|
+
r = session.get(
|
|
234
|
+
f"https://robotoff.openfoodfacts.org/api/v1/images/predict?image_url={image_url}&models=price_proof_classification",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if r.status_code != 200:
|
|
238
|
+
print(
|
|
239
|
+
f"Failed to get prediction for {image_url}, error: {r.text} (status: {r.status_code})"
|
|
240
|
+
)
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
predicted_category = r.json()["predictions"]["price_proof_classification"][0][
|
|
244
|
+
"label"
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
ls.tasks.update(
|
|
248
|
+
task.id,
|
|
249
|
+
data={
|
|
250
|
+
**data,
|
|
251
|
+
"predicted_category": predicted_category,
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
app.add_typer(user_app.app, name="users", help="Manage Label Studio users")
|
|
257
|
+
app.add_typer(
|
|
258
|
+
project_app.app,
|
|
259
|
+
name="projects",
|
|
260
|
+
help="Manage Label Studio projects (create, import data, etc.)",
|
|
261
|
+
)
|
|
262
|
+
app.add_typer(
|
|
263
|
+
dataset_app.app,
|
|
264
|
+
name="datasets",
|
|
265
|
+
help="Manage datasets (convert, export, check, etc.)",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
app()
|