label-studio-sdk 1.0.12__py3-none-any.whl → 1.0.14__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.

Potentially problematic release.


This version of label-studio-sdk might be problematic. Click here for more details.

@@ -3,6 +3,7 @@ import io
3
3
  import logging
4
4
  import os
5
5
  import shutil
6
+ import base64
6
7
  from contextlib import contextmanager
7
8
  from tempfile import mkdtemp
8
9
  from urllib.parse import urlparse
@@ -201,7 +202,7 @@ def download_and_cache(
201
202
 
202
203
  # local storage: /data/local-files?d=dir/1.jpg => 1.jpg
203
204
  if is_local_storage_file:
204
- url_filename = os.path.basename(url.split('?d=')[1])
205
+ url_filename = os.path.basename(url.split("?d=")[1])
205
206
  # cloud storage: s3://bucket/1.jpg => 1.jpg
206
207
  elif is_cloud_storage_file:
207
208
  url_filename = os.path.basename(url)
@@ -213,7 +214,11 @@ def download_and_cache(
213
214
  filepath = os.path.join(cache_dir, url_hash + "__" + url_filename)
214
215
 
215
216
  if not os.path.exists(filepath):
216
- logger.info("Download {url} to {filepath}. download_resources: {download_resources}".format(url=url, filepath=filepath, download_resources=download_resources))
217
+ logger.info(
218
+ "Download {url} to {filepath}. download_resources: {download_resources}".format(
219
+ url=url, filepath=filepath, download_resources=download_resources
220
+ )
221
+ )
217
222
  if download_resources:
218
223
  headers = {
219
224
  # avoid requests.exceptions.HTTPError: 403 Client Error: Forbidden. Please comply with the User-Agent policy:
@@ -256,3 +261,123 @@ def get_all_files_from_dir(d):
256
261
  if os.path.isfile(filepath):
257
262
  out.append(filepath)
258
263
  return out
264
+
265
+
266
+ def get_base64_content(
267
+ url,
268
+ hostname=None,
269
+ access_token=None,
270
+ task_id=None,
271
+ ):
272
+ """This helper function is used to download a file and return its base64 representation without saving to filesystem.
273
+
274
+ :param url: File URL to download, it can be a uploaded file, local storage, cloud storage file or just http(s) url
275
+ :param hostname: Label Studio Hostname, it will be used for uploaded files, local storage files and cloud storage files
276
+ if not provided, it will be taken from LABEL_STUDIO_URL env variable
277
+ :param access_token: Label Studio access token, it will be used for uploaded files, local storage files and cloud storage files
278
+ if not provided, it will be taken from LABEL_STUDIO_API_KEY env variable
279
+ :param task_id: Label Studio Task ID, required for cloud storage files
280
+ because the URL will be rebuilt to `{hostname}/tasks/{task_id}/presign/?fileuri={url}`
281
+
282
+ :return: base64 encoded file content
283
+ """
284
+ # get environment variables
285
+ hostname = (
286
+ hostname
287
+ or os.getenv("LABEL_STUDIO_URL", "")
288
+ or os.getenv("LABEL_STUDIO_HOST", "")
289
+ )
290
+ access_token = (
291
+ access_token
292
+ or os.getenv("LABEL_STUDIO_API_KEY", "")
293
+ or os.getenv("LABEL_STUDIO_ACCESS_TOKEN", "")
294
+ )
295
+ if "localhost" in hostname:
296
+ logger.warning(
297
+ f"Using `localhost` ({hostname}) in LABEL_STUDIO_URL, "
298
+ f"`localhost` is not accessible inside of docker containers. "
299
+ f"You can check your IP with utilities like `ifconfig` and set it as LABEL_STUDIO_URL."
300
+ )
301
+ if hostname and not (
302
+ hostname.startswith("http://") or hostname.startswith("https://")
303
+ ):
304
+ raise ValueError(
305
+ f"Invalid hostname in LABEL_STUDIO_URL: {hostname}. "
306
+ "Please provide full URL starting with protocol (http:// or https://)."
307
+ )
308
+
309
+ # fix file upload url
310
+ if url.startswith("upload") or url.startswith("/upload"):
311
+ url = "/data" + ("" if url.startswith("/") else "/") + url
312
+
313
+ is_uploaded_file = url.startswith("/data/upload")
314
+ is_local_storage_file = url.startswith("/data/") and "?d=" in url
315
+ is_cloud_storage_file = (
316
+ url.startswith("s3:") or url.startswith("gs:") or url.startswith("azure-blob:")
317
+ )
318
+
319
+ # Local storage file: try to load locally
320
+ if is_local_storage_file:
321
+ filepath = url.split("?d=")[1]
322
+ filepath = safe_build_path(LOCAL_FILES_DOCUMENT_ROOT, filepath)
323
+ if os.path.exists(filepath):
324
+ logger.debug(
325
+ f"Local Storage file path exists locally, read content directly: {filepath}"
326
+ )
327
+ with open(filepath, "rb") as f:
328
+ return base64.b64encode(f.read()).decode("utf-8")
329
+
330
+ # Upload or Local Storage file
331
+ if is_uploaded_file or is_local_storage_file or is_cloud_storage_file:
332
+ # hostname check
333
+ if not hostname:
334
+ raise FileNotFoundError(
335
+ f"Can't resolve url, hostname not provided: {url}. "
336
+ "You can set LABEL_STUDIO_URL environment variable to use it as a hostname."
337
+ )
338
+ # uploaded and local storage file
339
+ elif is_uploaded_file or is_local_storage_file:
340
+ url = concat_urls(hostname, url)
341
+ logger.info("Resolving url using hostname [" + hostname + "]: " + url)
342
+ # s3, gs, azure-blob file
343
+ elif is_cloud_storage_file:
344
+ if task_id is None:
345
+ raise Exception(
346
+ "Label Studio Task ID is required for cloud storage files"
347
+ )
348
+ url = concat_urls(hostname, f"/tasks/{task_id}/presign/?fileuri={url}")
349
+ logger.info(
350
+ "Cloud storage file: Resolving url using hostname ["
351
+ + hostname
352
+ + "]: "
353
+ + url
354
+ )
355
+
356
+ # check access token
357
+ if not access_token:
358
+ raise FileNotFoundError(
359
+ "To access uploaded and local storage files you have to "
360
+ "set LABEL_STUDIO_API_KEY environment variable."
361
+ )
362
+
363
+ # Download the content but don't save to filesystem
364
+ headers = {
365
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36"
366
+ }
367
+
368
+ # check if url matches hostname - then uses access token to this Label Studio instance
369
+ parsed_url = urlparse(url)
370
+ if access_token and hostname and parsed_url.netloc == urlparse(hostname).netloc:
371
+ headers["Authorization"] = "Token " + access_token
372
+ logger.debug("Authorization token is used for get_base64_content")
373
+
374
+ try:
375
+ r = requests.get(url, headers=headers, verify=VERIFY_SSL)
376
+ r.raise_for_status()
377
+ return base64.b64encode(r.content).decode("utf-8")
378
+ except requests.exceptions.SSLError as e:
379
+ logger.error(
380
+ f"SSL error during requests.get('{url}'): {e}\n"
381
+ f"Try to set VERIFY_SSL=False in environment variables to bypass SSL verification."
382
+ )
383
+ raise e
@@ -11,13 +11,14 @@ from datetime import datetime
11
11
  from enum import Enum
12
12
  from glob import glob
13
13
  from shutil import copy2
14
- from typing import Optional
14
+ from typing import Optional, List, Tuple
15
15
 
16
16
  import ijson
17
17
  import ujson as json
18
18
  from PIL import Image
19
19
  from label_studio_sdk.converter import brush
20
20
  from label_studio_sdk.converter.audio import convert_to_asr_json_manifest
21
+ from label_studio_sdk.converter.keypoints import process_keypoints_for_coco, build_kp_order, update_categories_for_keypoints, keypoints_in_label_config, get_yolo_categories_for_keypoints
21
22
  from label_studio_sdk.converter.exports import csv2
22
23
  from label_studio_sdk.converter.utils import (
23
24
  parse_config,
@@ -34,6 +35,7 @@ from label_studio_sdk.converter.utils import (
34
35
  convert_annotation_to_yolo_obb,
35
36
  )
36
37
  from label_studio_sdk._extensions.label_studio_tools.core.utils.io import get_local_path
38
+ from label_studio_sdk.converter.exports.yolo import process_and_save_yolo_annotations
37
39
 
38
40
  logger = logging.getLogger(__name__)
39
41
 
@@ -109,13 +111,13 @@ class Converter(object):
109
111
  "description": "Popular machine learning format used by the COCO dataset for object detection and image "
110
112
  "segmentation tasks with polygons and rectangles.",
111
113
  "link": "https://labelstud.io/guide/export.html#COCO",
112
- "tags": ["image segmentation", "object detection"],
114
+ "tags": ["image segmentation", "object detection", "keypoints"],
113
115
  },
114
116
  Format.COCO_WITH_IMAGES: {
115
117
  "title": "COCO with Images",
116
118
  "description": "COCO format with images downloaded.",
117
119
  "link": "https://labelstud.io/guide/export.html#COCO",
118
- "tags": ["image segmentation", "object detection"],
120
+ "tags": ["image segmentation", "object detection", "keypoints"],
119
121
  },
120
122
  Format.VOC: {
121
123
  "title": "Pascal VOC XML",
@@ -128,13 +130,13 @@ class Converter(object):
128
130
  "description": "Popular TXT format is created for each image file. Each txt file contains annotations for "
129
131
  "the corresponding image file, that is object class, object coordinates, height & width.",
130
132
  "link": "https://labelstud.io/guide/export.html#YOLO",
131
- "tags": ["image segmentation", "object detection"],
133
+ "tags": ["image segmentation", "object detection", "keypoints"],
132
134
  },
133
135
  Format.YOLO_WITH_IMAGES: {
134
136
  "title": "YOLO with Images",
135
137
  "description": "YOLO format with images downloaded.",
136
138
  "link": "https://labelstud.io/guide/export.html#YOLO",
137
- "tags": ["image segmentation", "object detection"],
139
+ "tags": ["image segmentation", "object detection", "keypoints"],
138
140
  },
139
141
  Format.YOLO_OBB: {
140
142
  "title": "YOLOv8 OBB",
@@ -205,6 +207,7 @@ class Converter(object):
205
207
  self._schema = None
206
208
  self.access_token = access_token
207
209
  self.hostname = hostname
210
+ self.is_keypoints = None
208
211
 
209
212
  if isinstance(config, dict):
210
213
  self._schema = config
@@ -376,11 +379,14 @@ class Converter(object):
376
379
  and (
377
380
  "RectangleLabels" in output_tag_types
378
381
  or "PolygonLabels" in output_tag_types
382
+ or "KeyPointLabels" in output_tag_types
379
383
  )
380
384
  or "Rectangle" in output_tag_types
381
385
  and "Labels" in output_tag_types
382
386
  or "PolygonLabels" in output_tag_types
383
387
  and "Labels" in output_tag_types
388
+ or "KeyPointLabels" in output_tag_types
389
+ and "Labels" in output_tag_types
384
390
  ):
385
391
  all_formats.remove(Format.COCO.name)
386
392
  all_formats.remove(Format.COCO_WITH_IMAGES.name)
@@ -522,6 +528,9 @@ class Converter(object):
522
528
  if "original_height" in r:
523
529
  v["original_height"] = r["original_height"]
524
530
  outputs[r["from_name"]].append(v)
531
+ if self.is_keypoints:
532
+ v['id'] = r.get('id')
533
+ v['parentID'] = r.get('parentID')
525
534
 
526
535
  data = Converter.get_data(task, outputs, annotation)
527
536
  if "agreement" in task:
@@ -638,6 +647,7 @@ class Converter(object):
638
647
  os.makedirs(output_image_dir, exist_ok=True)
639
648
  images, categories, annotations = [], [], []
640
649
  categories, category_name_to_id = self._get_labels()
650
+ categories, category_name_to_id = update_categories_for_keypoints(categories, category_name_to_id, self._schema)
641
651
  data_key = self._data_keys[0]
642
652
  item_iterator = (
643
653
  self.iter_from_dir(input_data)
@@ -703,9 +713,10 @@ class Converter(object):
703
713
  logger.debug(f'Empty bboxes for {item["output"]}')
704
714
  continue
705
715
 
716
+ keypoint_labels = []
706
717
  for label in labels:
707
718
  category_name = None
708
- for key in ["rectanglelabels", "polygonlabels", "labels"]:
719
+ for key in ["rectanglelabels", "polygonlabels", "keypointlabels", "labels"]:
709
720
  if key in label and len(label[key]) > 0:
710
721
  category_name = label[key][0]
711
722
  break
@@ -775,11 +786,22 @@ class Converter(object):
775
786
  "area": get_polygon_area(x, y),
776
787
  }
777
788
  )
789
+ elif "keypointlabels" in label:
790
+ keypoint_labels.append(label)
778
791
  else:
779
792
  raise ValueError("Unknown label type")
780
793
 
781
794
  if os.getenv("LABEL_STUDIO_FORCE_ANNOTATOR_EXPORT"):
782
795
  annotations[-1].update({"annotator": get_annotator(item)})
796
+ if keypoint_labels:
797
+ kp_order = build_kp_order(self._schema)
798
+ annotations.append(process_keypoints_for_coco(
799
+ keypoint_labels,
800
+ kp_order,
801
+ annotation_id=len(annotations),
802
+ image_id=image_id,
803
+ category_name_to_id=category_name_to_id,
804
+ ))
783
805
 
784
806
  with io.open(output_file, mode="w", encoding="utf8") as fout:
785
807
  json.dump(
@@ -846,7 +868,14 @@ class Converter(object):
846
868
  else:
847
869
  output_label_dir = os.path.join(output_dir, "labels")
848
870
  os.makedirs(output_label_dir, exist_ok=True)
849
- categories, category_name_to_id = self._get_labels()
871
+ is_keypoints = keypoints_in_label_config(self._schema)
872
+
873
+ if is_keypoints:
874
+ # we use this attribute to add id and parentID to annotation data
875
+ self.is_keypoints = True
876
+ categories, category_name_to_id = get_yolo_categories_for_keypoints(self._schema)
877
+ else:
878
+ categories, category_name_to_id = self._get_labels()
850
879
  data_key = self._data_keys[0]
851
880
  item_iterator = (
852
881
  self.iter_from_dir(input_data)
@@ -923,82 +952,7 @@ class Converter(object):
923
952
  pass
924
953
  continue
925
954
 
926
- annotations = []
927
- for label in labels:
928
- category_name = None
929
- category_names = [] # considering multi-label
930
- for key in ["rectanglelabels", "polygonlabels", "labels"]:
931
- if key in label and len(label[key]) > 0:
932
- # change to save multi-label
933
- for category_name in label[key]:
934
- category_names.append(category_name)
935
-
936
- if len(category_names) == 0:
937
- logger.debug(
938
- "Unknown label type or labels are empty: " + str(label)
939
- )
940
- continue
941
-
942
- for category_name in category_names:
943
- if category_name not in category_name_to_id:
944
- category_id = len(categories)
945
- category_name_to_id[category_name] = category_id
946
- categories.append({"id": category_id, "name": category_name})
947
- category_id = category_name_to_id[category_name]
948
-
949
- if (
950
- "rectanglelabels" in label
951
- or "rectangle" in label
952
- or "labels" in label
953
- ):
954
- # yolo obb
955
- if is_obb:
956
- obb_annotation = convert_annotation_to_yolo_obb(label)
957
- if obb_annotation is None:
958
- continue
959
-
960
- top_left, top_right, bottom_right, bottom_left = (
961
- obb_annotation
962
- )
963
- x1, y1 = top_left
964
- x2, y2 = top_right
965
- x3, y3 = bottom_right
966
- x4, y4 = bottom_left
967
- annotations.append(
968
- [category_id, x1, y1, x2, y2, x3, y3, x4, y4]
969
- )
970
-
971
- # simple yolo
972
- else:
973
- annotation = convert_annotation_to_yolo(label)
974
- if annotation is None:
975
- continue
976
-
977
- (
978
- x,
979
- y,
980
- w,
981
- h,
982
- ) = annotation
983
- annotations.append([category_id, x, y, w, h])
984
-
985
- elif "polygonlabels" in label or "polygon" in label:
986
- if not ('points' in label):
987
- continue
988
- points_abs = [(x / 100, y / 100) for x, y in label["points"]]
989
- annotations.append(
990
- [category_id]
991
- + [coord for point in points_abs for coord in point]
992
- )
993
- else:
994
- raise ValueError(f"Unknown label type {label}")
995
- with open(label_path, "w") as f:
996
- for annotation in annotations:
997
- for idx, l in enumerate(annotation):
998
- if idx == len(annotation) - 1:
999
- f.write(f"{l}\n")
1000
- else:
1001
- f.write(f"{l} ")
955
+ categories, category_name_to_id = process_and_save_yolo_annotations(labels, label_path, category_name_to_id, categories, is_obb, is_keypoints, self._schema)
1002
956
  with open(class_file, "w", encoding="utf8") as f:
1003
957
  for c in categories:
1004
958
  f.write(c["name"] + "\n")
@@ -0,0 +1,149 @@
1
+ import logging
2
+ from label_studio_sdk.converter.utils import convert_annotation_to_yolo, convert_annotation_to_yolo_obb
3
+ from label_studio_sdk.converter.keypoints import build_kp_order
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ def process_keypoints_for_yolo(labels, label_path,
8
+ category_name_to_id, categories,
9
+ is_obb, kp_order):
10
+ class_map = {c['name']: c['id'] for c in categories}
11
+
12
+ rectangles = {}
13
+ for item in labels:
14
+ if item['type'].lower() == 'rectanglelabels':
15
+ bbox_id = item['id']
16
+ cls_name = item['rectanglelabels'][0]
17
+ cls_idx = class_map.get(cls_name)
18
+ if cls_idx is None:
19
+ continue
20
+
21
+ x = item['x'] / 100.0
22
+ y = item['y'] / 100.0
23
+ width = item['width'] / 100.0
24
+ height = item['height'] / 100.0
25
+ x_c = x + width / 2.0
26
+ y_c = y + height / 2.0
27
+
28
+ rectangles[bbox_id] = {
29
+ 'class_idx': cls_idx,
30
+ 'x_center': x_c,
31
+ 'y_center': y_c,
32
+ 'width': width,
33
+ 'height': height,
34
+ 'kp_dict': {}
35
+ }
36
+
37
+ for item in labels:
38
+ if item['type'].lower() == 'keypointlabels':
39
+ parent_id = item.get('parentID')
40
+ if parent_id not in rectangles:
41
+ continue
42
+ label_name = item['keypointlabels'][0]
43
+ kp_x = item['x'] / 100.0
44
+ kp_y = item['y'] / 100.0
45
+ rectangles[parent_id]['kp_dict'][label_name] = (kp_x, kp_y, 2) # 2 = visible
46
+
47
+ lines = []
48
+ for rect in rectangles.values():
49
+ base = [
50
+ rect['class_idx'],
51
+ rect['x_center'],
52
+ rect['y_center'],
53
+ rect['width'],
54
+ rect['height']
55
+ ]
56
+ keypoints = []
57
+ for k in kp_order:
58
+ keypoints.extend(rect['kp_dict'].get(k, (0.0, 0.0, 0)))
59
+ line = ' '.join(map(str, base + keypoints))
60
+ lines.append(line)
61
+
62
+ with open(label_path, 'w', encoding='utf-8') as f:
63
+ f.write('\n'.join(lines))
64
+
65
+
66
+ def process_and_save_yolo_annotations(labels, label_path, category_name_to_id, categories, is_obb, is_keypoints, label_config):
67
+ if is_keypoints:
68
+ kp_order = build_kp_order(label_config)
69
+ process_keypoints_for_yolo(labels, label_path, category_name_to_id, categories, is_obb, kp_order)
70
+ return categories, category_name_to_id
71
+
72
+ annotations = []
73
+ for label in labels:
74
+ category_name = None
75
+ category_names = [] # considering multi-label
76
+ for key in ["rectanglelabels", "polygonlabels", "labels"]:
77
+ if key in label and len(label[key]) > 0:
78
+ # change to save multi-label
79
+ for category_name in label[key]:
80
+ category_names.append(category_name)
81
+
82
+ if len(category_names) == 0:
83
+ logger.debug(
84
+ "Unknown label type or labels are empty: " + str(label)
85
+ )
86
+ continue
87
+
88
+ for category_name in category_names:
89
+ if category_name not in category_name_to_id:
90
+ category_id = len(categories)
91
+ category_name_to_id[category_name] = category_id
92
+ categories.append({"id": category_id, "name": category_name})
93
+ category_id = category_name_to_id[category_name]
94
+
95
+ if (
96
+ "rectanglelabels" in label
97
+ or "rectangle" in label
98
+ or "labels" in label
99
+ ):
100
+ # yolo obb
101
+ if is_obb:
102
+ obb_annotation = convert_annotation_to_yolo_obb(label)
103
+ if obb_annotation is None:
104
+ continue
105
+
106
+ top_left, top_right, bottom_right, bottom_left = (
107
+ obb_annotation
108
+ )
109
+ x1, y1 = top_left
110
+ x2, y2 = top_right
111
+ x3, y3 = bottom_right
112
+ x4, y4 = bottom_left
113
+ annotations.append(
114
+ [category_id, x1, y1, x2, y2, x3, y3, x4, y4]
115
+ )
116
+
117
+ # simple yolo
118
+ else:
119
+ annotation = convert_annotation_to_yolo(label)
120
+ if annotation is None:
121
+ continue
122
+
123
+ (
124
+ x,
125
+ y,
126
+ w,
127
+ h,
128
+ ) = annotation
129
+ annotations.append([category_id, x, y, w, h])
130
+
131
+ elif "polygonlabels" in label or "polygon" in label:
132
+ if not ('points' in label):
133
+ continue
134
+ points_abs = [(x / 100, y / 100) for x, y in label["points"]]
135
+ annotations.append(
136
+ [category_id]
137
+ + [coord for point in points_abs for coord in point]
138
+ )
139
+ else:
140
+ raise ValueError(f"Unknown label type {label}")
141
+ with open(label_path, "w") as f:
142
+ for annotation in annotations:
143
+ for idx, l in enumerate(annotation):
144
+ if idx == len(annotation) - 1:
145
+ f.write(f"{l}\n")
146
+ else:
147
+ f.write(f"{l} ")
148
+
149
+ return categories, category_name_to_id
@@ -0,0 +1,146 @@
1
+ import os
2
+ import json
3
+
4
+
5
+ def keypoints_in_label_config(label_config):
6
+ """
7
+ Check if the label config contains keypoints.
8
+ :param label_config: Label config in JSON format.
9
+ :return: True if keypoints are present, False otherwise.
10
+ """
11
+ for cfg in label_config.values():
12
+ if cfg.get("type") == "KeyPointLabels":
13
+ return True
14
+ return False
15
+
16
+
17
+ def update_categories_for_keypoints(categories, category_name_to_id, label_config):
18
+ keypoint_labels = []
19
+ for cfg in label_config.values():
20
+ if cfg.get("type") == "KeyPointLabels":
21
+ keypoint_labels.extend(cfg.get("labels", []))
22
+ keypoint_labels = list(dict.fromkeys(keypoint_labels))
23
+
24
+ non_kp = [cat.copy() for cat in categories if cat["name"] not in keypoint_labels]
25
+
26
+ new_categories = []
27
+ new_mapping = {}
28
+ next_id = 0
29
+ for cat in non_kp:
30
+ cat["id"] = next_id
31
+ new_categories.append(cat)
32
+ new_mapping[cat["name"]] = next_id
33
+ next_id += 1
34
+
35
+ if keypoint_labels:
36
+ merged_id = next_id
37
+ merged_category = {
38
+ "id": merged_id,
39
+ "name": "default",
40
+ "supercategory": "default",
41
+ "keypoints": keypoint_labels,
42
+ "skeleton": []
43
+ }
44
+ new_categories.append(merged_category)
45
+ for kp_name in keypoint_labels:
46
+ new_mapping[kp_name] = merged_id
47
+
48
+ return new_categories, new_mapping
49
+
50
+
51
+ def build_kp_order(label_config):
52
+ kp_block = {}
53
+
54
+ for tag in label_config.values():
55
+ if tag.get("type") == "KeyPointLabels":
56
+ kp_block.update(tag.get("labels_attrs", {}))
57
+
58
+ pairs, used = [], set()
59
+
60
+ for name, attrs in kp_block.items():
61
+ try:
62
+ idx = int(attrs.get("model_index"))
63
+ except (TypeError, ValueError):
64
+ continue
65
+ if idx in used:
66
+ continue
67
+ pairs.append((idx, name))
68
+ used.add(idx)
69
+
70
+ pairs.sort(key=lambda p: p[0])
71
+ result = [name for _, name in pairs]
72
+ return result
73
+
74
+
75
+ def get_bbox_coco(keypoints, kp_order):
76
+ xs = [keypoints[3*i] for i in range(len(kp_order)) if keypoints[3*i + 2] > 0]
77
+ ys = [keypoints[3*i + 1] for i in range(len(kp_order)) if keypoints[3*i + 2] > 0]
78
+ if xs and ys:
79
+ x_min = min(xs)
80
+ y_min = min(ys)
81
+ x_max = max(xs)
82
+ y_max = max(ys)
83
+ width = x_max - x_min
84
+ height = y_max - y_min
85
+ bbox = [x_min, y_min, width, height]
86
+ else:
87
+ bbox = [0, 0, 0, 0]
88
+ return bbox
89
+
90
+
91
+ def process_keypoints_for_coco(keypoint_labels, kp_order, annotation_id, image_id, category_name_to_id):
92
+ keypoints = [0] * (len(kp_order) * 3)
93
+
94
+ for kp in keypoint_labels:
95
+ width, height = kp["original_width"], kp["original_height"]
96
+ x, y = kp['x'] / 100 * width, kp['y'] / 100 * height
97
+ labels = kp.get('keypointlabels', [])
98
+ v = 2 if labels else 0
99
+ for label in labels:
100
+ if label in kp_order:
101
+ idx = kp_order.index(label)
102
+ keypoints[3 * idx] = int(round(x))
103
+ keypoints[3 * idx + 1] = int(round(y))
104
+ keypoints[3 * idx + 2] = v
105
+
106
+ num_keypoints = sum(1 for i in range(len(kp_order)) if keypoints[3*i + 2] > 0)
107
+
108
+ bbox = get_bbox_coco(keypoints, kp_order)
109
+
110
+ category_id = category_name_to_id.get(kp_order[0], 0)
111
+ annotation = {
112
+ 'id': annotation_id,
113
+ 'image_id': image_id,
114
+ 'category_id': category_id,
115
+ 'keypoints': keypoints,
116
+ 'num_keypoints': num_keypoints,
117
+ 'bbox': bbox,
118
+ 'iscrowd': 0
119
+ }
120
+ return annotation
121
+
122
+
123
+ def get_yolo_categories_for_keypoints(label_config):
124
+ # Get all keypoint labels from the label config
125
+ keypoint_labels = []
126
+ for cfg in label_config.values():
127
+ if cfg.get("type") == "KeyPointLabels":
128
+ keypoint_labels.extend(cfg.get("labels", []))
129
+ keypoint_labels = list(dict.fromkeys(keypoint_labels)) # Remove duplicates
130
+
131
+ # Get all rectangle labels from the label config
132
+ rectangle_labels = []
133
+ for cfg in label_config.values():
134
+ if cfg.get("type") == "RectangleLabels":
135
+ rectangle_labels.extend(cfg.get("labels", []))
136
+ rectangle_labels = list(dict.fromkeys(rectangle_labels)) # Remove duplicates
137
+
138
+ # Create categories for each rectangle label
139
+ categories = []
140
+ category_name_to_id = {}
141
+
142
+ for i, label in enumerate(rectangle_labels):
143
+ categories.append({"id": i, "name": label, "keypoints": keypoint_labels})
144
+ category_name_to_id[label] = i
145
+
146
+ return categories, category_name_to_id
@@ -18,7 +18,7 @@ class BaseClientWrapper:
18
18
 
19
19
  # even in the async case, refreshing access token (when the existing one is expired) should be sync
20
20
  from ..tokens.client_ext import TokensClientExt
21
- self._tokens_client = TokensClientExt(base_url=base_url, api_key=api_key)
21
+ self._tokens_client = TokensClientExt(base_url=base_url, api_key=api_key, client_wrapper=self)
22
22
 
23
23
 
24
24
  def get_timeout(self) -> typing.Optional[float]:
@@ -8,9 +8,11 @@ import re
8
8
  import json
9
9
  import jsonschema
10
10
 
11
+ from functools import cached_property
11
12
  from typing import Dict, Optional, List, Tuple, Any, Callable, Union
12
13
  from pydantic import BaseModel
13
14
 
15
+
14
16
  # from typing import Dict, Optional, List, Tuple, Any
15
17
  from collections import defaultdict, OrderedDict
16
18
  from lxml import etree
@@ -517,6 +519,19 @@ class LabelInterface:
517
519
 
518
520
  return lst
519
521
 
522
+ @cached_property
523
+ def ner_tags(self):
524
+ return self.find_tags('controls', lambda t: t.tag.lower() in ('labels', 'hypertextlabels'))
525
+
526
+ @cached_property
527
+ def image_tags(self):
528
+ return self.find_tags('objects', lambda t: t.tag.lower() == 'image')
529
+
530
+ @cached_property
531
+ def pdf_tags(self):
532
+ return self.find_tags('objects', lambda t: t.tag.lower() == 'pdf')
533
+
534
+
520
535
  def load_task(self, task):
521
536
  """Loads a task and substitutes the value in each object tag
522
537
  with actual data from the task, returning a copy of the
@@ -189,12 +189,17 @@ class AudioTag(ObjectTag):
189
189
 
190
190
 
191
191
  class ImageTag(ObjectTag):
192
- """ """
192
+ """Image tag"""
193
193
  tag: str = "Image"
194
194
 
195
195
  def _generate_example(self, examples, only_urls=False):
196
196
  """ """
197
197
  return examples.get("Image")
198
+
199
+ @property
200
+ def is_image_list(self):
201
+ """Check if the tag is an image list, i.e. it has a valueList attribute that accepts list of images"""
202
+ return bool(self.attr.get("valueList")) if self.attr else False
198
203
 
199
204
 
200
205
  class TableTag(ObjectTag):
@@ -1,6 +1,7 @@
1
1
  import threading
2
2
  import typing
3
3
  from datetime import datetime, timezone
4
+ import inspect
4
5
 
5
6
  import httpx
6
7
  import jwt
@@ -12,9 +13,10 @@ from ..types.access_token_response import AccessTokenResponse
12
13
  class TokensClientExt:
13
14
  """Client for managing authentication tokens."""
14
15
 
15
- def __init__(self, base_url: str, api_key: str):
16
+ def __init__(self, base_url: str, api_key: str, client_wrapper=None):
16
17
  self._base_url = base_url
17
18
  self._api_key = api_key
19
+ self._client_wrapper = client_wrapper
18
20
  self._use_legacy_token = not self._is_valid_jwt_token(api_key, raise_if_expired=True)
19
21
 
20
22
  # cache state for access token when using jwt-based api_key
@@ -78,9 +80,24 @@ class TokensClientExt:
78
80
 
79
81
  def refresh(self) -> AccessTokenResponse:
80
82
  """Refresh the access token and return the token response."""
81
- # We don't do this often, just use a separate httpx client for simplicity here
83
+ # We don't do this often, just use a separate sync httpx client for simplicity here
82
84
  # (avoids complicated state management and sync vs async handling)
83
- with httpx.Client() as sync_client:
85
+ # Create a new client with the same parameters as the existing one
86
+ existing_client = self._client_wrapper.httpx_client.httpx_client
87
+
88
+ # Get all parameters from httpx.Client.__init__
89
+ client_params = {}
90
+ sig = inspect.signature(httpx.Client.__init__)
91
+ for param_name in sig.parameters:
92
+ if param_name != 'self': # Skip 'self' parameter
93
+ try:
94
+ value = getattr(existing_client, param_name, None)
95
+ if value is not None:
96
+ client_params[param_name] = value
97
+ except AttributeError:
98
+ continue
99
+
100
+ with httpx.Client(**client_params) as sync_client:
84
101
  response = sync_client.request(
85
102
  method="POST",
86
103
  url=f"{self._base_url}/api/token/refresh/",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: label-studio-sdk
3
- Version: 1.0.12
3
+ Version: 1.0.14
4
4
  Summary:
5
5
  Requires-Python: >=3.9,<4
6
6
  Classifier: Intended Audience :: Developers
@@ -29,6 +29,7 @@ Requires-Dist: jsonschema (>=4.23.0)
29
29
  Requires-Dist: lxml (>=4.2.5)
30
30
  Requires-Dist: nltk (>=3.9.1,<4.0.0)
31
31
  Requires-Dist: numpy (>=1.26.4,<3.0.0)
32
+ Requires-Dist: opencv-python (>=4.9.0,<5.0.0)
32
33
  Requires-Dist: pandas (>=0.24.0)
33
34
  Requires-Dist: pydantic (>=1.9.2)
34
35
  Requires-Dist: pydantic-core (>=2.18.2,<3.0.0)
@@ -6,7 +6,7 @@ label_studio_sdk/_extensions/label_studio_tools/core/__init__.py,sha256=47DEQpj8
6
6
  label_studio_sdk/_extensions/label_studio_tools/core/label_config.py,sha256=P1S7dPjFkqF2zIQzk11iljhharrUc9qQRM_rUN38iJQ,6406
7
7
  label_studio_sdk/_extensions/label_studio_tools/core/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  label_studio_sdk/_extensions/label_studio_tools/core/utils/exceptions.py,sha256=JxaXUMghUp1YvL--s8KFC4mCHVbV39giE3kSBHCmuFU,66
9
- label_studio_sdk/_extensions/label_studio_tools/core/utils/io.py,sha256=FO0fBVvffuDjbQQcvqLsXmGUn1gCP1YmA-tmNsvX8oo,9650
9
+ label_studio_sdk/_extensions/label_studio_tools/core/utils/io.py,sha256=NJHLJA8Q93_MLmo_Yx9F4_Z-kkWCZmF_Ahi3MtDQCPo,14800
10
10
  label_studio_sdk/_extensions/label_studio_tools/core/utils/json_schema.py,sha256=_Lg3DxhRqGhzlk3egGUDufx-iaoEWec19upZKp-Cwic,3378
11
11
  label_studio_sdk/_extensions/label_studio_tools/core/utils/params.py,sha256=ZSUb-IXG5OcPQ7pJ8NDRLon-cMxnjVq6XtinxvTuJso,1244
12
12
  label_studio_sdk/_extensions/label_studio_tools/etl/__init__.py,sha256=SdN7JGLJ1araqbx-nL2fVdhm6E6CNyru-vWVs6sMswI,31
@@ -50,11 +50,12 @@ label_studio_sdk/comments/client.py,sha256=k359u7Q8YyPtaawoA1FW4dAuYzGVmeGM9rR2G
50
50
  label_studio_sdk/converter/__init__.py,sha256=qppSJed16HAiZbGons0yVrPRjszuWFia025Rm477q1c,201
51
51
  label_studio_sdk/converter/audio.py,sha256=U9oTULkeimodZhIkB16Gl3eJD8YzsbADWxW_r2tPcxw,1905
52
52
  label_studio_sdk/converter/brush.py,sha256=jRL3fLl_J06fVEX7Uat31ru0uUZ71C4zrXnX2qOcrIo,13370
53
- label_studio_sdk/converter/converter.py,sha256=o90sU1oY7xE6Ni6O5qMkBUcT96QzqwVg9yd6zu-1gqw,51338
53
+ label_studio_sdk/converter/converter.py,sha256=Fx3IZpTRLe1_rHPwPYHYeaWsHnK2QJ7RnXpjyabI68Y,49731
54
54
  label_studio_sdk/converter/exports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
55
  label_studio_sdk/converter/exports/brush_to_coco.py,sha256=YeVSyZxmXwLbqox7dS3IRuzR1mYTlUhg6YgK4Txq00U,13355
56
56
  label_studio_sdk/converter/exports/csv.py,sha256=F4t04tFsg5gBXTZNmkjw_NeEVsobRH_Y_vfFDi7R0Zw,2865
57
57
  label_studio_sdk/converter/exports/csv2.py,sha256=9FxcPtIDcuztDF-y4Z7Mm0AgbYUR1oMitYT6BlcOFes,3125
58
+ label_studio_sdk/converter/exports/yolo.py,sha256=0g57qBFDnTEIMJF8oD-vaPYJbtQeecJB74yWISt5EXo,5419
58
59
  label_studio_sdk/converter/funsd.py,sha256=QHoa8hzWQLkZQ87e9EgURit9kGGUCgDxoRONcSzmWlw,2544
59
60
  label_studio_sdk/converter/imports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
61
  label_studio_sdk/converter/imports/coco.py,sha256=FDUij8i329XRs3RFZEo-hKWseieyfE7vbQUtHYHpQs8,10312
@@ -62,11 +63,12 @@ label_studio_sdk/converter/imports/colors.py,sha256=F5_K4FIhOZl6LNEIVT2UU5L8HcmY
62
63
  label_studio_sdk/converter/imports/label_config.py,sha256=8RT2Jppvi1-Sl3ZNDA1uFyHr2NoU4-gM26S9iAEuQh8,1218
63
64
  label_studio_sdk/converter/imports/pathtrack.py,sha256=Xxxbw8fLLHTR57FEjVeeitjh35YbcIh_eVzuw2e5K9w,8096
64
65
  label_studio_sdk/converter/imports/yolo.py,sha256=kAJhsUV4ZxOEJE8yk7CvMGf_3w6aslqrj7OUOqHIwbo,8888
66
+ label_studio_sdk/converter/keypoints.py,sha256=KACb3W4aoiM3GBvXehooFS2SqUD82zsLMAXpZgioNyw,4561
65
67
  label_studio_sdk/converter/main.py,sha256=gfe5zPV2dnIk4ifG1AT95ExkzOSLzje0EOjnW0oC3q8,6442
66
68
  label_studio_sdk/converter/utils.py,sha256=VshPBwZLu2VPIGVsShKAkZwB_zKz0VvMkNRCwWeEqWg,18702
67
69
  label_studio_sdk/core/__init__.py,sha256=-t9txgeQZL_1FDw_08GEoj4ft1Cn9Dti6X0Drsadlr0,1519
68
70
  label_studio_sdk/core/api_error.py,sha256=RE8LELok2QCjABadECTvtDp7qejA1VmINCh6TbqPwSE,426
69
- label_studio_sdk/core/client_wrapper.py,sha256=fThAUV72_apQ4I2bS60iM84082lgXo4YMM6yvl07Kpg,2209
71
+ label_studio_sdk/core/client_wrapper.py,sha256=lAGxJnIC7HMfysZS9sLHX1lnc82jw_n3eo_pTeljlFc,2230
70
72
  label_studio_sdk/core/datetime_utils.py,sha256=nBys2IsYrhPdszxGKCNRPSOCwa-5DWOHG95FB8G9PKo,1047
71
73
  label_studio_sdk/core/file.py,sha256=d4NNbX8XvXP32z8KpK2Xovv33nFfruIrpz0QWxlgpZk,2663
72
74
  label_studio_sdk/core/http_client.py,sha256=siUQ6UV0ARZALlxubqWSSAAPC9B4VW8y6MGlHStfaeo,19552
@@ -155,9 +157,9 @@ label_studio_sdk/label_interface/base.py,sha256=NCgY7ntk0WSc9O9iXu3g37-CxbZgCx_W
155
157
  label_studio_sdk/label_interface/control_tags.py,sha256=qLe4gsRxvppuNtrxfmgZHFX1ahM-XhePlrchZfnJiL0,30141
156
158
  label_studio_sdk/label_interface/create.py,sha256=c3h5_FF4u5J62_mqq1oK2mjqXL-I1559C6MfoxkgO6s,6993
157
159
  label_studio_sdk/label_interface/data_examples.json,sha256=uCYvCtMIxPi1-jLlFhwJPh01tLyMIRwTjINeAeW-JzE,8195
158
- label_studio_sdk/label_interface/interface.py,sha256=U_2IkpQXxw8fBW_buwfWIOvgdk1z_xLHFdk8YPUC1eE,45519
160
+ label_studio_sdk/label_interface/interface.py,sha256=nEC_RQJ9VCCtYRKqJ7AYhggbLqvTKvJkPIRTQeIm8vc,45959
159
161
  label_studio_sdk/label_interface/label_tags.py,sha256=nWEo21Gd8IPzIO72UqraLrChIbvrSMCA_eEhzYGnGCc,2282
160
- label_studio_sdk/label_interface/object_tags.py,sha256=EGe3bYTZr92SezzWka8grYnvOQNtyfEYa5-yoM4a7Es,8705
162
+ label_studio_sdk/label_interface/object_tags.py,sha256=9k3DEYEh7aXSLh2JjH-SWNVupP1qgwHFte85Ix7-4dQ,8944
161
163
  label_studio_sdk/label_interface/objects.py,sha256=V1Spp0S9qE7iA-5kPCi0QyHrJ80Du9BUuYMsQUAQqc0,1535
162
164
  label_studio_sdk/label_interface/region.py,sha256=th39WeQk8ypi-4krEpsW0BZnoygu4XgvP4w7NkRQp2M,1755
163
165
  label_studio_sdk/ml/__init__.py,sha256=J4ncAcAOU_qriOx_Im9eFmXyupKM19SXMcpMcXSmw-I,455
@@ -213,7 +215,7 @@ label_studio_sdk/tasks/types/tasks_list_request_fields.py,sha256=5YXxQgyzoaL0QjS
213
215
  label_studio_sdk/tasks/types/tasks_list_response.py,sha256=j1pNluAWQOQ8-d9YXQyRQAefnrl8uLQEB7_L55Z8DME,1136
214
216
  label_studio_sdk/tokens/__init__.py,sha256=FTtvy8EDg9nNNg9WCatVgKTRYV8-_v1roeGPAKoa_pw,65
215
217
  label_studio_sdk/tokens/client.py,sha256=SvBcKXIsrTihMJC72Ifxv0U1N3gtLGz3JxqdXYA_hD4,19101
216
- label_studio_sdk/tokens/client_ext.py,sha256=chhzBuVYp0YeUrAnYVwDX5yJq5IJlomGhBQ1zvjZAkI,3976
218
+ label_studio_sdk/tokens/client_ext.py,sha256=LHy29mBizrxVs_xOVMiIJwNsQKcOjS8V8LWfWMdAdYU,4730
217
219
  label_studio_sdk/types/__init__.py,sha256=fQykjzHpX04ftslk5I_hWSJQ_H9Kd8XJAmSv18EVOIc,8905
218
220
  label_studio_sdk/types/access_token_response.py,sha256=RV9FqkIiFR_9kmKueB-KiqjVyneiqUkMVueAlk5fUyc,624
219
221
  label_studio_sdk/types/annotation.py,sha256=AnHm2VjMasWZsaNXVSUzLYbpYrmM4NPZgWQh7WGa6ZQ,3157
@@ -362,7 +364,7 @@ label_studio_sdk/workspaces/members/client.py,sha256=IVM52Yq_9zMQ3TUHT0AkZ5BTQ9a
362
364
  label_studio_sdk/workspaces/members/types/__init__.py,sha256=ZIa_rd7d6K9ZITjTU6fptyGgvjNDySksJ7Rbn4wyhD4,252
363
365
  label_studio_sdk/workspaces/members/types/members_create_response.py,sha256=7Hp5FSWm4xR5ZOEmEIglq5HYtM9KWZZBDp87jw7jYFg,668
364
366
  label_studio_sdk/workspaces/members/types/members_list_response_item.py,sha256=DIc5DJoVahI9olBis_iFgOJrAf05m2fCE8g4R5ZeDko,712
365
- label_studio_sdk-1.0.12.dist-info/LICENSE,sha256=ymVrFcHiJGjHeY30NWZgdV-xzNEtfuC63oK9ZeMDjhs,11341
366
- label_studio_sdk-1.0.12.dist-info/METADATA,sha256=Wn4jvmJgSE1RkJyMXNV0RjrjMcmzk33Mn7jN_2STUWE,5987
367
- label_studio_sdk-1.0.12.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
368
- label_studio_sdk-1.0.12.dist-info/RECORD,,
367
+ label_studio_sdk-1.0.14.dist-info/LICENSE,sha256=ymVrFcHiJGjHeY30NWZgdV-xzNEtfuC63oK9ZeMDjhs,11341
368
+ label_studio_sdk-1.0.14.dist-info/METADATA,sha256=v1D-HsVQxvhu1dESF077Nr-mtaIylYb1KQx6pOxoqww,6033
369
+ label_studio_sdk-1.0.14.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
370
+ label_studio_sdk-1.0.14.dist-info/RECORD,,