label-studio-sdk 1.0.5__py3-none-any.whl → 1.0.7__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.
Files changed (68) hide show
  1. label_studio_sdk/__init__.py +70 -0
  2. label_studio_sdk/_extensions/eval/categorical.py +83 -0
  3. label_studio_sdk/_extensions/label_studio_tools/core/utils/io.py +35 -17
  4. label_studio_sdk/annotations/__init__.py +3 -0
  5. label_studio_sdk/annotations/client.py +109 -0
  6. label_studio_sdk/annotations/types/__init__.py +5 -0
  7. label_studio_sdk/annotations/types/annotations_create_bulk_response_item.py +29 -0
  8. label_studio_sdk/base_client.py +9 -0
  9. label_studio_sdk/comments/__init__.py +2 -0
  10. label_studio_sdk/comments/client.py +512 -0
  11. label_studio_sdk/converter/converter.py +2 -0
  12. label_studio_sdk/converter/imports/coco.py +14 -13
  13. label_studio_sdk/converter/utils.py +72 -3
  14. label_studio_sdk/core/client_wrapper.py +1 -1
  15. label_studio_sdk/files/client.py +26 -16
  16. label_studio_sdk/label_interface/interface.py +38 -5
  17. label_studio_sdk/model_providers/__init__.py +2 -0
  18. label_studio_sdk/model_providers/client.py +190 -0
  19. label_studio_sdk/projects/client.py +32 -16
  20. label_studio_sdk/projects/exports/client.py +133 -40
  21. label_studio_sdk/prompts/__init__.py +21 -0
  22. label_studio_sdk/prompts/client.py +862 -0
  23. label_studio_sdk/prompts/indicators/__init__.py +2 -0
  24. label_studio_sdk/prompts/indicators/client.py +194 -0
  25. label_studio_sdk/prompts/runs/__init__.py +5 -0
  26. label_studio_sdk/prompts/runs/client.py +354 -0
  27. label_studio_sdk/prompts/runs/types/__init__.py +5 -0
  28. label_studio_sdk/prompts/runs/types/runs_list_request_project_subset.py +5 -0
  29. label_studio_sdk/prompts/types/__init__.py +15 -0
  30. label_studio_sdk/prompts/types/prompts_batch_failed_predictions_request_failed_predictions_item.py +42 -0
  31. label_studio_sdk/prompts/types/prompts_batch_failed_predictions_response.py +29 -0
  32. label_studio_sdk/prompts/types/prompts_batch_predictions_request_results_item.py +62 -0
  33. label_studio_sdk/prompts/types/prompts_batch_predictions_response.py +29 -0
  34. label_studio_sdk/prompts/versions/__init__.py +2 -0
  35. label_studio_sdk/prompts/versions/client.py +921 -0
  36. label_studio_sdk/types/__init__.py +52 -0
  37. label_studio_sdk/types/comment.py +39 -0
  38. label_studio_sdk/types/comment_created_by.py +5 -0
  39. label_studio_sdk/types/inference_run.py +43 -0
  40. label_studio_sdk/types/inference_run_created_by.py +5 -0
  41. label_studio_sdk/types/inference_run_organization.py +5 -0
  42. label_studio_sdk/types/inference_run_project_subset.py +5 -0
  43. label_studio_sdk/types/inference_run_status.py +7 -0
  44. label_studio_sdk/types/key_indicator_value.py +30 -0
  45. label_studio_sdk/types/key_indicators.py +7 -0
  46. label_studio_sdk/types/key_indicators_item.py +51 -0
  47. label_studio_sdk/types/key_indicators_item_additional_kpis_item.py +37 -0
  48. label_studio_sdk/types/key_indicators_item_extra_kpis_item.py +37 -0
  49. label_studio_sdk/types/model_provider_connection.py +41 -0
  50. label_studio_sdk/types/model_provider_connection_created_by.py +5 -0
  51. label_studio_sdk/types/model_provider_connection_organization.py +5 -0
  52. label_studio_sdk/types/model_provider_connection_provider.py +5 -0
  53. label_studio_sdk/types/model_provider_connection_scope.py +5 -0
  54. label_studio_sdk/types/prompt.py +79 -0
  55. label_studio_sdk/types/prompt_created_by.py +5 -0
  56. label_studio_sdk/types/prompt_organization.py +5 -0
  57. label_studio_sdk/types/prompt_version.py +41 -0
  58. label_studio_sdk/types/prompt_version_created_by.py +5 -0
  59. label_studio_sdk/types/prompt_version_organization.py +5 -0
  60. label_studio_sdk/types/prompt_version_provider.py +5 -0
  61. label_studio_sdk/types/refined_prompt_response.py +64 -0
  62. label_studio_sdk/types/refined_prompt_response_refinement_status.py +7 -0
  63. label_studio_sdk/webhooks/client.py +245 -36
  64. label_studio_sdk/workspaces/client.py +20 -20
  65. label_studio_sdk-1.0.7.dist-info/LICENSE +201 -0
  66. {label_studio_sdk-1.0.5.dist-info → label_studio_sdk-1.0.7.dist-info}/METADATA +17 -3
  67. {label_studio_sdk-1.0.5.dist-info → label_studio_sdk-1.0.7.dist-info}/RECORD +68 -19
  68. {label_studio_sdk-1.0.5.dist-info → label_studio_sdk-1.0.7.dist-info}/WHEEL +1 -1
@@ -0,0 +1,512 @@
1
+ # This file was auto-generated by Fern from our API Definition.
2
+
3
+ import typing
4
+ from json.decoder import JSONDecodeError
5
+
6
+ from ..core.api_error import ApiError
7
+ from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
8
+ from ..core.jsonable_encoder import jsonable_encoder
9
+ from ..core.pydantic_utilities import pydantic_v1
10
+ from ..core.request_options import RequestOptions
11
+ from ..types.comment import Comment
12
+
13
+ # this is used as the default value for optional parameters
14
+ OMIT = typing.cast(typing.Any, ...)
15
+
16
+
17
+ class CommentsClient:
18
+ def __init__(self, *, client_wrapper: SyncClientWrapper):
19
+ self._client_wrapper = client_wrapper
20
+
21
+ def list(
22
+ self,
23
+ *,
24
+ project: typing.Optional[int] = None,
25
+ expand_created_by: typing.Optional[bool] = None,
26
+ annotation: typing.Optional[int] = None,
27
+ request_options: typing.Optional[RequestOptions] = None,
28
+ ) -> typing.List[Comment]:
29
+ """
30
+ Get a list of comments for a specific project.
31
+
32
+ Parameters
33
+ ----------
34
+ project : typing.Optional[int]
35
+ Project ID
36
+
37
+ expand_created_by : typing.Optional[bool]
38
+ Expand the created_by field with object instead of ID
39
+
40
+ annotation : typing.Optional[int]
41
+ Annotation ID
42
+
43
+ request_options : typing.Optional[RequestOptions]
44
+ Request-specific configuration.
45
+
46
+ Returns
47
+ -------
48
+ typing.List[Comment]
49
+
50
+
51
+ Examples
52
+ --------
53
+ from label_studio_sdk.client import LabelStudio
54
+
55
+ client = LabelStudio(
56
+ api_key="YOUR_API_KEY",
57
+ )
58
+ client.comments.list()
59
+ """
60
+ _response = self._client_wrapper.httpx_client.request(
61
+ "api/comments/",
62
+ method="GET",
63
+ params={"project": project, "expand_created_by": expand_created_by, "annotation": annotation},
64
+ request_options=request_options,
65
+ )
66
+ try:
67
+ if 200 <= _response.status_code < 300:
68
+ return pydantic_v1.parse_obj_as(typing.List[Comment], _response.json()) # type: ignore
69
+ _response_json = _response.json()
70
+ except JSONDecodeError:
71
+ raise ApiError(status_code=_response.status_code, body=_response.text)
72
+ raise ApiError(status_code=_response.status_code, body=_response_json)
73
+
74
+ def create(
75
+ self,
76
+ *,
77
+ annotation: typing.Optional[int] = OMIT,
78
+ project: typing.Optional[int] = OMIT,
79
+ text: typing.Optional[str] = OMIT,
80
+ is_resolved: typing.Optional[bool] = OMIT,
81
+ request_options: typing.Optional[RequestOptions] = None,
82
+ ) -> Comment:
83
+ """
84
+ Create a new comment.
85
+
86
+ Parameters
87
+ ----------
88
+ annotation : typing.Optional[int]
89
+
90
+ project : typing.Optional[int]
91
+
92
+ text : typing.Optional[str]
93
+
94
+ is_resolved : typing.Optional[bool]
95
+
96
+ request_options : typing.Optional[RequestOptions]
97
+ Request-specific configuration.
98
+
99
+ Returns
100
+ -------
101
+ Comment
102
+
103
+
104
+ Examples
105
+ --------
106
+ from label_studio_sdk.client import LabelStudio
107
+
108
+ client = LabelStudio(
109
+ api_key="YOUR_API_KEY",
110
+ )
111
+ client.comments.create()
112
+ """
113
+ _response = self._client_wrapper.httpx_client.request(
114
+ "api/comments/",
115
+ method="POST",
116
+ json={"annotation": annotation, "project": project, "text": text, "is_resolved": is_resolved},
117
+ request_options=request_options,
118
+ omit=OMIT,
119
+ )
120
+ try:
121
+ if 200 <= _response.status_code < 300:
122
+ return pydantic_v1.parse_obj_as(Comment, _response.json()) # type: ignore
123
+ _response_json = _response.json()
124
+ except JSONDecodeError:
125
+ raise ApiError(status_code=_response.status_code, body=_response.text)
126
+ raise ApiError(status_code=_response.status_code, body=_response_json)
127
+
128
+ def get(self, id: int, *, request_options: typing.Optional[RequestOptions] = None) -> Comment:
129
+ """
130
+ Get a specific comment.
131
+
132
+ Parameters
133
+ ----------
134
+ id : int
135
+ Comment ID
136
+
137
+ request_options : typing.Optional[RequestOptions]
138
+ Request-specific configuration.
139
+
140
+ Returns
141
+ -------
142
+ Comment
143
+
144
+
145
+ Examples
146
+ --------
147
+ from label_studio_sdk.client import LabelStudio
148
+
149
+ client = LabelStudio(
150
+ api_key="YOUR_API_KEY",
151
+ )
152
+ client.comments.get(
153
+ id=1,
154
+ )
155
+ """
156
+ _response = self._client_wrapper.httpx_client.request(
157
+ f"api/comments/{jsonable_encoder(id)}", method="GET", request_options=request_options
158
+ )
159
+ try:
160
+ if 200 <= _response.status_code < 300:
161
+ return pydantic_v1.parse_obj_as(Comment, _response.json()) # type: ignore
162
+ _response_json = _response.json()
163
+ except JSONDecodeError:
164
+ raise ApiError(status_code=_response.status_code, body=_response.text)
165
+ raise ApiError(status_code=_response.status_code, body=_response_json)
166
+
167
+ def delete(self, id: int, *, request_options: typing.Optional[RequestOptions] = None) -> None:
168
+ """
169
+ Delete a specific comment.
170
+
171
+ Parameters
172
+ ----------
173
+ id : int
174
+ Comment ID
175
+
176
+ request_options : typing.Optional[RequestOptions]
177
+ Request-specific configuration.
178
+
179
+ Returns
180
+ -------
181
+ None
182
+
183
+ Examples
184
+ --------
185
+ from label_studio_sdk.client import LabelStudio
186
+
187
+ client = LabelStudio(
188
+ api_key="YOUR_API_KEY",
189
+ )
190
+ client.comments.delete(
191
+ id=1,
192
+ )
193
+ """
194
+ _response = self._client_wrapper.httpx_client.request(
195
+ f"api/comments/{jsonable_encoder(id)}", method="DELETE", request_options=request_options
196
+ )
197
+ try:
198
+ if 200 <= _response.status_code < 300:
199
+ return
200
+ _response_json = _response.json()
201
+ except JSONDecodeError:
202
+ raise ApiError(status_code=_response.status_code, body=_response.text)
203
+ raise ApiError(status_code=_response.status_code, body=_response_json)
204
+
205
+ def update(
206
+ self,
207
+ id: int,
208
+ *,
209
+ annotation: typing.Optional[int] = OMIT,
210
+ project: typing.Optional[int] = OMIT,
211
+ text: typing.Optional[str] = OMIT,
212
+ is_resolved: typing.Optional[bool] = OMIT,
213
+ request_options: typing.Optional[RequestOptions] = None,
214
+ ) -> Comment:
215
+ """
216
+ Update a specific comment.
217
+
218
+ Parameters
219
+ ----------
220
+ id : int
221
+ Comment ID
222
+
223
+ annotation : typing.Optional[int]
224
+
225
+ project : typing.Optional[int]
226
+
227
+ text : typing.Optional[str]
228
+
229
+ is_resolved : typing.Optional[bool]
230
+
231
+ request_options : typing.Optional[RequestOptions]
232
+ Request-specific configuration.
233
+
234
+ Returns
235
+ -------
236
+ Comment
237
+
238
+
239
+ Examples
240
+ --------
241
+ from label_studio_sdk.client import LabelStudio
242
+
243
+ client = LabelStudio(
244
+ api_key="YOUR_API_KEY",
245
+ )
246
+ client.comments.update(
247
+ id=1,
248
+ )
249
+ """
250
+ _response = self._client_wrapper.httpx_client.request(
251
+ f"api/comments/{jsonable_encoder(id)}",
252
+ method="PATCH",
253
+ json={"annotation": annotation, "project": project, "text": text, "is_resolved": is_resolved},
254
+ request_options=request_options,
255
+ omit=OMIT,
256
+ )
257
+ try:
258
+ if 200 <= _response.status_code < 300:
259
+ return pydantic_v1.parse_obj_as(Comment, _response.json()) # type: ignore
260
+ _response_json = _response.json()
261
+ except JSONDecodeError:
262
+ raise ApiError(status_code=_response.status_code, body=_response.text)
263
+ raise ApiError(status_code=_response.status_code, body=_response_json)
264
+
265
+
266
+ class AsyncCommentsClient:
267
+ def __init__(self, *, client_wrapper: AsyncClientWrapper):
268
+ self._client_wrapper = client_wrapper
269
+
270
+ async def list(
271
+ self,
272
+ *,
273
+ project: typing.Optional[int] = None,
274
+ expand_created_by: typing.Optional[bool] = None,
275
+ annotation: typing.Optional[int] = None,
276
+ request_options: typing.Optional[RequestOptions] = None,
277
+ ) -> typing.List[Comment]:
278
+ """
279
+ Get a list of comments for a specific project.
280
+
281
+ Parameters
282
+ ----------
283
+ project : typing.Optional[int]
284
+ Project ID
285
+
286
+ expand_created_by : typing.Optional[bool]
287
+ Expand the created_by field with object instead of ID
288
+
289
+ annotation : typing.Optional[int]
290
+ Annotation ID
291
+
292
+ request_options : typing.Optional[RequestOptions]
293
+ Request-specific configuration.
294
+
295
+ Returns
296
+ -------
297
+ typing.List[Comment]
298
+
299
+
300
+ Examples
301
+ --------
302
+ from label_studio_sdk.client import AsyncLabelStudio
303
+
304
+ client = AsyncLabelStudio(
305
+ api_key="YOUR_API_KEY",
306
+ )
307
+ await client.comments.list()
308
+ """
309
+ _response = await self._client_wrapper.httpx_client.request(
310
+ "api/comments/",
311
+ method="GET",
312
+ params={"project": project, "expand_created_by": expand_created_by, "annotation": annotation},
313
+ request_options=request_options,
314
+ )
315
+ try:
316
+ if 200 <= _response.status_code < 300:
317
+ return pydantic_v1.parse_obj_as(typing.List[Comment], _response.json()) # type: ignore
318
+ _response_json = _response.json()
319
+ except JSONDecodeError:
320
+ raise ApiError(status_code=_response.status_code, body=_response.text)
321
+ raise ApiError(status_code=_response.status_code, body=_response_json)
322
+
323
+ async def create(
324
+ self,
325
+ *,
326
+ annotation: typing.Optional[int] = OMIT,
327
+ project: typing.Optional[int] = OMIT,
328
+ text: typing.Optional[str] = OMIT,
329
+ is_resolved: typing.Optional[bool] = OMIT,
330
+ request_options: typing.Optional[RequestOptions] = None,
331
+ ) -> Comment:
332
+ """
333
+ Create a new comment.
334
+
335
+ Parameters
336
+ ----------
337
+ annotation : typing.Optional[int]
338
+
339
+ project : typing.Optional[int]
340
+
341
+ text : typing.Optional[str]
342
+
343
+ is_resolved : typing.Optional[bool]
344
+
345
+ request_options : typing.Optional[RequestOptions]
346
+ Request-specific configuration.
347
+
348
+ Returns
349
+ -------
350
+ Comment
351
+
352
+
353
+ Examples
354
+ --------
355
+ from label_studio_sdk.client import AsyncLabelStudio
356
+
357
+ client = AsyncLabelStudio(
358
+ api_key="YOUR_API_KEY",
359
+ )
360
+ await client.comments.create()
361
+ """
362
+ _response = await self._client_wrapper.httpx_client.request(
363
+ "api/comments/",
364
+ method="POST",
365
+ json={"annotation": annotation, "project": project, "text": text, "is_resolved": is_resolved},
366
+ request_options=request_options,
367
+ omit=OMIT,
368
+ )
369
+ try:
370
+ if 200 <= _response.status_code < 300:
371
+ return pydantic_v1.parse_obj_as(Comment, _response.json()) # type: ignore
372
+ _response_json = _response.json()
373
+ except JSONDecodeError:
374
+ raise ApiError(status_code=_response.status_code, body=_response.text)
375
+ raise ApiError(status_code=_response.status_code, body=_response_json)
376
+
377
+ async def get(self, id: int, *, request_options: typing.Optional[RequestOptions] = None) -> Comment:
378
+ """
379
+ Get a specific comment.
380
+
381
+ Parameters
382
+ ----------
383
+ id : int
384
+ Comment ID
385
+
386
+ request_options : typing.Optional[RequestOptions]
387
+ Request-specific configuration.
388
+
389
+ Returns
390
+ -------
391
+ Comment
392
+
393
+
394
+ Examples
395
+ --------
396
+ from label_studio_sdk.client import AsyncLabelStudio
397
+
398
+ client = AsyncLabelStudio(
399
+ api_key="YOUR_API_KEY",
400
+ )
401
+ await client.comments.get(
402
+ id=1,
403
+ )
404
+ """
405
+ _response = await self._client_wrapper.httpx_client.request(
406
+ f"api/comments/{jsonable_encoder(id)}", method="GET", request_options=request_options
407
+ )
408
+ try:
409
+ if 200 <= _response.status_code < 300:
410
+ return pydantic_v1.parse_obj_as(Comment, _response.json()) # type: ignore
411
+ _response_json = _response.json()
412
+ except JSONDecodeError:
413
+ raise ApiError(status_code=_response.status_code, body=_response.text)
414
+ raise ApiError(status_code=_response.status_code, body=_response_json)
415
+
416
+ async def delete(self, id: int, *, request_options: typing.Optional[RequestOptions] = None) -> None:
417
+ """
418
+ Delete a specific comment.
419
+
420
+ Parameters
421
+ ----------
422
+ id : int
423
+ Comment ID
424
+
425
+ request_options : typing.Optional[RequestOptions]
426
+ Request-specific configuration.
427
+
428
+ Returns
429
+ -------
430
+ None
431
+
432
+ Examples
433
+ --------
434
+ from label_studio_sdk.client import AsyncLabelStudio
435
+
436
+ client = AsyncLabelStudio(
437
+ api_key="YOUR_API_KEY",
438
+ )
439
+ await client.comments.delete(
440
+ id=1,
441
+ )
442
+ """
443
+ _response = await self._client_wrapper.httpx_client.request(
444
+ f"api/comments/{jsonable_encoder(id)}", method="DELETE", request_options=request_options
445
+ )
446
+ try:
447
+ if 200 <= _response.status_code < 300:
448
+ return
449
+ _response_json = _response.json()
450
+ except JSONDecodeError:
451
+ raise ApiError(status_code=_response.status_code, body=_response.text)
452
+ raise ApiError(status_code=_response.status_code, body=_response_json)
453
+
454
+ async def update(
455
+ self,
456
+ id: int,
457
+ *,
458
+ annotation: typing.Optional[int] = OMIT,
459
+ project: typing.Optional[int] = OMIT,
460
+ text: typing.Optional[str] = OMIT,
461
+ is_resolved: typing.Optional[bool] = OMIT,
462
+ request_options: typing.Optional[RequestOptions] = None,
463
+ ) -> Comment:
464
+ """
465
+ Update a specific comment.
466
+
467
+ Parameters
468
+ ----------
469
+ id : int
470
+ Comment ID
471
+
472
+ annotation : typing.Optional[int]
473
+
474
+ project : typing.Optional[int]
475
+
476
+ text : typing.Optional[str]
477
+
478
+ is_resolved : typing.Optional[bool]
479
+
480
+ request_options : typing.Optional[RequestOptions]
481
+ Request-specific configuration.
482
+
483
+ Returns
484
+ -------
485
+ Comment
486
+
487
+
488
+ Examples
489
+ --------
490
+ from label_studio_sdk.client import AsyncLabelStudio
491
+
492
+ client = AsyncLabelStudio(
493
+ api_key="YOUR_API_KEY",
494
+ )
495
+ await client.comments.update(
496
+ id=1,
497
+ )
498
+ """
499
+ _response = await self._client_wrapper.httpx_client.request(
500
+ f"api/comments/{jsonable_encoder(id)}",
501
+ method="PATCH",
502
+ json={"annotation": annotation, "project": project, "text": text, "is_resolved": is_resolved},
503
+ request_options=request_options,
504
+ omit=OMIT,
505
+ )
506
+ try:
507
+ if 200 <= _response.status_code < 300:
508
+ return pydantic_v1.parse_obj_as(Comment, _response.json()) # type: ignore
509
+ _response_json = _response.json()
510
+ except JSONDecodeError:
511
+ raise ApiError(status_code=_response.status_code, body=_response.text)
512
+ raise ApiError(status_code=_response.status_code, body=_response_json)
@@ -346,6 +346,8 @@ class Converter(object):
346
346
  and "TextArea" in output_tag_types
347
347
  ):
348
348
  all_formats.remove(Format.ASR_MANIFEST.name)
349
+ if 'Video' in input_tag_types and 'TimelineLabels' in output_tag_types:
350
+ all_formats.remove(Format.YOLO_OBB.name)
349
351
 
350
352
  return all_formats
351
353
 
@@ -48,10 +48,9 @@ def create_bbox(annotation, categories, from_name, image_height, image_width, to
48
48
 
49
49
 
50
50
  def create_segmentation(
51
- annotation, categories, from_name, image_height, image_width, to_name
51
+ category_id, segmentation, categories, from_name, image_height, image_width, to_name
52
52
  ):
53
- label = categories[int(annotation["category_id"])]
54
- segmentation = annotation["segmentation"][0]
53
+ label = categories[int(category_id)]
55
54
  points = [list(x) for x in zip(*[iter(segmentation)] * 2)]
56
55
 
57
56
  for i in range(len(points)):
@@ -216,15 +215,17 @@ def convert_coco_to_ls(
216
215
  task[out_type][0]["result"].append(item)
217
216
 
218
217
  if "segmentation" in annotation and len(annotation["segmentation"]):
219
- item = create_segmentation(
220
- annotation,
221
- categories,
222
- segmentation_from_name,
223
- image_height,
224
- image_width,
225
- to_name,
226
- )
227
- task[out_type][0]["result"].append(item)
218
+ for single_segmentation in annotation["segmentation"]:
219
+ item = create_segmentation(
220
+ annotation["category_id"],
221
+ single_segmentation,
222
+ categories,
223
+ segmentation_from_name,
224
+ image_height,
225
+ image_width,
226
+ to_name,
227
+ )
228
+ task[out_type][0]["result"].append(item)
228
229
 
229
230
  if "keypoints" in annotation:
230
231
  items = create_keypoints(
@@ -270,7 +271,7 @@ def add_parser(subparsers):
270
271
  "--input",
271
272
  dest="input",
272
273
  required=True,
273
- help="directory with COCO where images, labels, notes.json are located",
274
+ help="input COCO json file",
274
275
  action=ExpandFullPath,
275
276
  )
276
277
  coco.add_argument(
@@ -17,10 +17,11 @@ from urllib.parse import urlparse
17
17
  import numpy as np
18
18
  import requests
19
19
  from PIL import Image
20
- from label_studio_sdk._extensions.label_studio_tools.core.utils.params import get_env
21
20
  from lxml import etree
22
21
  from nltk.tokenize.treebank import TreebankWordTokenizer
23
22
 
23
+ from label_studio_sdk._extensions.label_studio_tools.core.utils.params import get_env
24
+
24
25
  logger = logging.getLogger(__name__)
25
26
 
26
27
  _LABEL_TAGS = {"Label", "Choice"}
@@ -422,7 +423,7 @@ def convert_annotation_to_yolo(label):
422
423
  return x, y, w, h
423
424
 
424
425
 
425
- def convert_annotation_to_yolo_obb(label):
426
+ def convert_annotation_to_yolo_obb(label, normalize=True):
426
427
  """
427
428
  Convert LS annotation to Yolo OBB format.
428
429
 
@@ -435,6 +436,7 @@ def convert_annotation_to_yolo_obb(label):
435
436
  - width (float): Width of the object in percentage of the original width.
436
437
  - height (float): Height of the object in percentage of the original height.
437
438
  - rotation (float, optional): Rotation angle of the object in degrees (default is 0).
439
+ normalize (bool, optional): Whether to normalize the coordinates to the range [0, 1] (default is True).
438
440
 
439
441
  Returns:
440
442
  list of tuple or None: List of tuples containing the coordinates of the object in Yolo OBB format.
@@ -470,4 +472,71 @@ def convert_annotation_to_yolo_obb(label):
470
472
  ]
471
473
 
472
474
  # Normalize coordinates
473
- return [(coord[0] / org_width, coord[1] / org_height) for coord in coords]
475
+ if normalize:
476
+ return [(coord[0] / org_width, coord[1] / org_height) for coord in coords]
477
+ else:
478
+ return coords
479
+
480
+
481
+ def convert_yolo_obb_to_annotation(xyxyxyxy, original_width, original_height):
482
+ """
483
+ Convert YOLO Oriented Bounding Box (OBB) format to Label Studio format.
484
+
485
+ Args:
486
+ xyxyxyxy (list): List of 8 float values representing the absolute pixel coordinates
487
+ of the OBB in the format [x1, y1, x2, y2, x3, y3, x4, y4].
488
+ original_width (int): Original width of the image.
489
+ original_height (int): Original height of the image.
490
+
491
+ Returns:
492
+ dict: Dictionary containing the converted bounding box with the following keys:
493
+ - x: X-coordinate of the top-left corner of the bounding box in percentage.
494
+ - y: Y-coordinate of the top-left corner of the bounding box in percentage.
495
+ - width: Width of the bounding box in percentage.
496
+ - height: Height of the bounding box in percentage.
497
+ - rotation: Rotation angle of the bounding box in degrees.
498
+ """
499
+ # Reshape the coordinates into a 4x2 matrix
500
+ coords = np.array(xyxyxyxy, dtype=np.float64).reshape((4, 2))
501
+
502
+ # Calculate the center of the bounding box
503
+ center_x = np.mean(coords[:, 0])
504
+ center_y = np.mean(coords[:, 1])
505
+
506
+ # Calculate the width and height of the bounding box
507
+ width = np.linalg.norm(coords[0] - coords[1])
508
+ height = np.linalg.norm(coords[0] - coords[3])
509
+
510
+ # Calculate the rotation angle
511
+ dx = coords[1, 0] - coords[0, 0]
512
+ dy = coords[1, 1] - coords[0, 1]
513
+ r = np.degrees(np.arctan2(dy, dx))
514
+
515
+ # Find the top-left corner (x, y)
516
+ top_left_x = (
517
+ center_x
518
+ - (width / 2) * np.cos(np.radians(r))
519
+ + (height / 2) * np.sin(np.radians(r))
520
+ )
521
+ top_left_y = (
522
+ center_y
523
+ - (width / 2) * np.sin(np.radians(r))
524
+ - (height / 2) * np.cos(np.radians(r))
525
+ )
526
+
527
+ # Normalize the values
528
+ x = (top_left_x / original_width) * 100
529
+ y = (top_left_y / original_height) * 100
530
+ width = (width / original_width) * 100
531
+ height = (height / original_height) * 100
532
+
533
+ # Create the dictionary for Label Studio
534
+ return {
535
+ "x": x,
536
+ "y": y,
537
+ "width": width,
538
+ "height": height,
539
+ "rotation": r,
540
+ "original_width": original_width,
541
+ "original_height": original_height,
542
+ }
@@ -17,7 +17,7 @@ class BaseClientWrapper:
17
17
  headers: typing.Dict[str, str] = {
18
18
  "X-Fern-Language": "Python",
19
19
  "X-Fern-SDK-Name": "label-studio-sdk",
20
- "X-Fern-SDK-Version": "1.0.4",
20
+ "X-Fern-SDK-Version": "1.0.6",
21
21
  }
22
22
  headers["Authorization"] = f"Token {self.api_key}"
23
23
  return headers