supervisely 6.73.277__py3-none-any.whl → 6.73.279__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 supervisely might be problematic. Click here for more details.

@@ -1,12 +1,27 @@
1
1
  import os
2
+ import shutil
2
3
  import sys
3
4
  import uuid
4
5
  from copy import deepcopy
5
- from typing import List
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union
6
9
 
7
10
  import cv2
8
11
  import numpy as np
9
12
 
13
+ from supervisely._utils import generate_free_name
14
+ from supervisely.api.image_api import ImageApi
15
+ from supervisely.imaging.image import read as sly_image
16
+ from supervisely.io.fs import get_file_name_with_ext
17
+ from supervisely.io.json import dump_json_file, load_json_file
18
+ from supervisely.project.project import Dataset, OpenMode, Project
19
+ from supervisely.project.project_meta import ProjectMeta
20
+ from supervisely.task.progress import tqdm_sly
21
+
22
+ COCO_INSTANCES_FILE = "coco_instances.json"
23
+ COCO_CAPTIONS_FILE = "coco_captions.json"
24
+
10
25
 
11
26
  class HiddenCocoPrints:
12
27
  def __enter__(self):
@@ -321,5 +336,482 @@ def create_custom_geometry_config(num_keypoints=None, cat_labels=None, cat_edges
321
336
  for edge in cat_edges:
322
337
  template.add_edge(src=cat_labels[edge[0] - 1], dst=cat_labels[edge[1] - 1])
323
338
  else:
324
- logger.warn("Edges can not be mapped without skeleton, please check your annotation")
339
+ logger.warning("Edges can not be mapped without skeleton, please check your annotation")
325
340
  return template
341
+
342
+
343
+ def _get_graph_info(idx, obj_class):
344
+ data = {"supercategory": obj_class.name, "id": idx, "name": obj_class.name}
345
+ kp = {i: n["label"] for i, n in obj_class.geometry_config["nodes"].items()}
346
+ keys = {k: j for j, k in enumerate(list(kp.keys()), 1)}
347
+ edges = obj_class.geometry_config["edges"]
348
+ sk = [[keys[e["src"]], keys[e["dst"]]] for e in edges]
349
+ data["keypoints"] = list(kp.values())
350
+ data["skeleton"] = sk
351
+ return data
352
+
353
+
354
+ def get_categories_from_meta(meta: ProjectMeta):
355
+ cat = lambda idx, c: {"supercategory": c.name, "id": idx, "name": c.name}
356
+ return [
357
+ cat(idx, c) if c.geometry_type != GraphNodes else _get_graph_info(idx, c)
358
+ for idx, c in enumerate(meta.obj_classes)
359
+ ]
360
+
361
+
362
+ def sly_ann_to_coco(
363
+ ann: Annotation,
364
+ coco_image_id: int,
365
+ class_mapping: Dict[str, int],
366
+ coco_ann: Optional[Union[Dict, List]] = None,
367
+ last_label_id: Optional[int] = None,
368
+ coco_captions: Optional[Union[Dict, List]] = None,
369
+ last_caption_id: Optional[int] = None,
370
+ ) -> Tuple[List, List]:
371
+ """
372
+ Convert Supervisely annotation to COCO format annotation ("annotations" field).
373
+
374
+ :param coco_image_id: Image id in COCO format.
375
+ :type coco_image_id: int
376
+ :param class_mapping: Dictionary that maps class names to class ids.
377
+ :type class_mapping: Dict[str, int]
378
+ :param coco_ann: COCO annotation in dictionary or list format to append new annotations.
379
+ :type coco_ann: Union[Dict, List], optional
380
+ :param last_label_id: Last label id in COCO format to continue counting.
381
+ :type last_label_id: int, optional
382
+ :param coco_captions: COCO captions in dictionary or list format to append new captions.
383
+ :type coco_captions: Union[Dict, List], optional
384
+ :return: Tuple with list of COCO objects and list of COCO captions.
385
+ :rtype: :class:`tuple`
386
+
387
+
388
+ :Usage example:
389
+
390
+ .. code-block:: python
391
+
392
+ import supervisely as sly
393
+ from supervisely.convert.image.coco.coco_helper import sly_ann_to_coco
394
+
395
+
396
+ coco_instances = dict(
397
+ info=dict(
398
+ description="COCO dataset converted from Supervisely",
399
+ url="None",
400
+ version=str(1.0),
401
+ year=2025,
402
+ contributor="Supervisely",
403
+ date_created="2025-01-01 00:00:00",
404
+ ),
405
+ licenses=[dict(url="None", id=0, name="None")],
406
+ images=[],
407
+ annotations=[],
408
+ categories=get_categories_from_meta(meta), # [{"supercategory": "lemon", "id": 0, "name": "lemon"}, ...]
409
+ )
410
+
411
+ ann = sly.Annotation.from_json(ann_json, meta)
412
+ image_id = 11
413
+ label_id = 222
414
+ class_mapping = {obj_cls.name: idx for idx, obj_cls in enumerate(meta.obj_classes)}
415
+
416
+ curr_coco_ann, _ = sly_ann_to_coco(ann, image_id, class_mapping, coco_instances, label_id)
417
+ # or
418
+ # curr_coco_ann, _ = sly_ann_to_coco(ann, image_id, class_mapping, label_id=label_id)
419
+ # coco_instances["annotations"].extend(curr_coco_ann)
420
+
421
+ label_id += len(curr_coco_ann)
422
+ image_id += 1
423
+ """
424
+
425
+ coco_obj_template = lambda x, y, z: {
426
+ "id": x,
427
+ "segmentation": [],
428
+ "area": 0,
429
+ "iscrowd": 0,
430
+ "image_id": y,
431
+ "bbox": [],
432
+ "category_id": z,
433
+ }
434
+ label_id = last_label_id + 1 if last_label_id is not None else 1
435
+ if isinstance(coco_ann, dict):
436
+ coco_ann = coco_ann["annotations"]
437
+ if isinstance(coco_ann, list):
438
+ label_id = len(coco_ann) + 1
439
+
440
+ last_caption_id = last_caption_id + 1 if last_caption_id is not None else 1
441
+ if isinstance(coco_captions, dict):
442
+ coco_captions = coco_captions["annotations"]
443
+ if isinstance(coco_captions, list):
444
+ last_caption_id = len(coco_captions) + 1
445
+
446
+ def _update_inst_results(label_id, coco_ann, coco_obj, res):
447
+ label_id += 1
448
+ if isinstance(coco_ann, list):
449
+ coco_ann.append(coco_obj)
450
+ res.append(coco_obj)
451
+ return label_id
452
+
453
+ def _get_common_bbox(labels, sly_bbox=False, approx=False):
454
+ bboxes = [l.geometry.to_bbox() for l in labels]
455
+ x = min([bbox.left for bbox in bboxes])
456
+ y = min([bbox.top for bbox in bboxes])
457
+ max_x = max([bbox.right for bbox in bboxes])
458
+ max_y = max([bbox.bottom for bbox in bboxes])
459
+ if approx:
460
+ x, y, max_x, max_y = x - 10, y - 10, max_x + 10, max_y + 10
461
+ if sly_bbox:
462
+ return Rectangle(top=y, left=x, bottom=max_y, right=max_x)
463
+ return [x, y, max_x - x, max_y - y]
464
+
465
+ def _create_keypoints_obj(label, cat_id, label_id, coco_image_id):
466
+ nodes_dict = label.obj_class.geometry_config["nodes"]
467
+ keypoint_uuid_labels = {i: d["label"] for i, d in nodes_dict.items()}
468
+ keypoints = []
469
+ for key in keypoint_uuid_labels.keys():
470
+ if key not in label.geometry.nodes:
471
+ keypoints.extend([0, 0, 0])
472
+ else:
473
+ loc = label.geometry.nodes[key].location
474
+ keypoints.extend([loc.col, loc.row, 2])
475
+ coco_obj = coco_obj_template(label_id, coco_image_id, cat_id)
476
+ coco_obj["keypoints"] = keypoints
477
+ coco_obj["num_keypoints"] = len(keypoint_uuid_labels)
478
+ x, y = keypoints[0::3], keypoints[1::3]
479
+ x0, x1, y0, y1 = (np.min(x), np.max(x), np.min(y), np.max(y))
480
+ x0, x1, y0, y1 = int(x0), int(x1), int(y0), int(y1)
481
+ coco_obj["area"] = int((x1 - x0) * (y1 - y0))
482
+ coco_obj["bbox"] = [x0, y0, x1 - x0, y1 - y0]
483
+ return coco_obj
484
+
485
+ def _update_caption_results(caption_id, coco_captions, caption, res):
486
+ caption_id += 1
487
+ if isinstance(coco_captions, list):
488
+ coco_captions.append(caption)
489
+ res.append(caption)
490
+ return caption_id
491
+
492
+ res_inst = [] # result list of COCO objects
493
+
494
+ for binding_key, labels in ann.get_bindings().items():
495
+ if binding_key is None:
496
+ polygons = [l for l in labels if l.obj_class.geometry_type == Polygon]
497
+ masks = [l for l in labels if l.obj_class.geometry_type == Bitmap]
498
+ bboxes = [l for l in labels if l.obj_class.geometry_type == Rectangle]
499
+ graphs = [l for l in labels if l.obj_class.geometry_type == GraphNodes]
500
+ if len(masks) > 0:
501
+ for l in masks:
502
+ polygon_cls = l.obj_class.clone(geometry_type=Polygon)
503
+ polygons.extend(l.convert(polygon_cls))
504
+ for label in polygons + bboxes:
505
+ cat_id = class_mapping[label.obj_class.name]
506
+ coco_obj = coco_obj_template(label_id, coco_image_id, cat_id)
507
+ coco_obj["bbox"] = _get_common_bbox([label])
508
+ coco_obj["area"] = label.geometry.area
509
+ if label.obj_class.geometry_type == Polygon:
510
+ poly = label.geometry.to_json()["points"]["exterior"]
511
+ poly = np.array(poly).flatten().astype(float).tolist()
512
+ coco_obj["segmentation"] = [poly]
513
+
514
+ label_id = _update_inst_results(label_id, coco_ann, coco_obj, res_inst)
515
+
516
+ for label in graphs:
517
+ cat_id = class_mapping[label.obj_class.name]
518
+ new_obj = _create_keypoints_obj(label, cat_id, label_id, coco_image_id)
519
+ label_id = _update_inst_results(label_id, coco_ann, new_obj, res_inst)
520
+
521
+ continue
522
+
523
+ bboxes = [l for l in labels if l.obj_class.geometry_type == Rectangle]
524
+ polygons = [l for l in labels if l.obj_class.geometry_type == Polygon]
525
+ masks = [l for l in labels if l.obj_class.geometry_type == Bitmap]
526
+ graphs = [l for l in labels if l.obj_class.geometry_type == GraphNodes]
527
+
528
+ if len(masks) > 0: # convert Bitmap to Polygon
529
+ for l in masks:
530
+ polygon_cls = l.obj_class.clone(geometry_type=Polygon)
531
+ polygons.extend(l.convert(polygon_cls))
532
+
533
+ matched_bbox = False
534
+ if len(polygons) > 0: # process polygons
535
+ cat_id = class_mapping[polygons[0].obj_class.name]
536
+ coco_obj = coco_obj_template(label_id, coco_image_id, cat_id)
537
+ if len(bboxes) > 0:
538
+ found = _get_common_bbox(bboxes, sly_bbox=True, approx=True)
539
+ new = _get_common_bbox(polygons, sly_bbox=True)
540
+ matched_bbox = found.contains(new)
541
+
542
+ polys = [l.geometry.to_json()["points"]["exterior"] for l in polygons]
543
+ polys = [np.array(p).flatten().astype(float).tolist() for p in polys]
544
+ coco_obj["segmentation"] = polys
545
+ coco_obj["area"] = sum([l.geometry.area for l in polygons])
546
+ coco_obj["bbox"] = _get_common_bbox(bboxes if matched_bbox else polygons)
547
+ label_id = _update_inst_results(label_id, coco_ann, coco_obj, res_inst)
548
+
549
+ if len(graphs) > 0:
550
+ if len(graphs) > 1:
551
+ logger.warning(
552
+ "Multiple Keypoints in one binding key are not supported. "
553
+ "Only the first graph will be converted."
554
+ )
555
+ cat_id = class_mapping[graphs[0].obj_class.name]
556
+ coco_obj = _create_keypoints_obj(graphs[0], cat_id, label_id, coco_image_id)
557
+ label_id = _update_inst_results(label_id, coco_ann, coco_obj, res_inst)
558
+
559
+ if len(bboxes) > 0 and not matched_bbox: # process bboxes separately
560
+ for label in bboxes:
561
+ cat_id = class_mapping[label.obj_class.name]
562
+ coco_obj = coco_obj_template(label_id, coco_image_id, cat_id)
563
+ coco_obj["bbox"] = _get_common_bbox([label])
564
+ coco_obj["area"] = label.geometry.area
565
+
566
+ label_id = _update_inst_results(label_id, coco_ann, coco_obj, res_inst)
567
+
568
+ is_caption = lambda t: t.meta.name == "caption" and t.meta.value_type == TagValueType.ANY_STRING
569
+ caption_tags = [tag for tag in ann.img_tags if is_caption(tag)]
570
+
571
+ res_captions = [] # result list of COCO captions
572
+ for tag in caption_tags:
573
+ caption = {"image_id": coco_image_id, "id": caption_id, "caption": tag.value}
574
+ caption_id = _update_caption_results(caption_id, coco_captions, caption, res_captions)
575
+
576
+ return res_inst, res_captions
577
+
578
+
579
+ def has_caption_tag(meta: ProjectMeta) -> bool:
580
+ tag = meta.get_tag_meta("caption")
581
+ return tag is not None and tag.value_type == TagValueType.ANY_STRING
582
+
583
+
584
+ def create_coco_ann_template(meta: ProjectMeta) -> Dict[str, Any]:
585
+ now = datetime.now()
586
+ coco_ann = dict(
587
+ info=dict(
588
+ description="COCO dataset converted from Supervisely",
589
+ url="None",
590
+ version=str(1.0),
591
+ year=now.year,
592
+ contributor="Supervisely",
593
+ date_created=now.strftime("%Y-%m-%d %H:%M:%S"),
594
+ ),
595
+ licenses=[dict(url="None", id=0, name="None")],
596
+ images=[],
597
+ annotations=[],
598
+ categories=[],
599
+ )
600
+ coco_ann["categories"] = get_categories_from_meta(meta)
601
+ return coco_ann
602
+
603
+
604
+ def sly_ds_to_coco(
605
+ dataset: Dataset,
606
+ meta: ProjectMeta,
607
+ return_type: Literal["path", "dict"] = "path",
608
+ dest_dir: Optional[str] = None,
609
+ copy_images: bool = False,
610
+ with_captions: bool = False,
611
+ log_progress: bool = False,
612
+ progress_cb: Optional[Callable] = None,
613
+ ) -> Union[str, Tuple[str, str], Dict, Tuple[Dict, Dict]]:
614
+ """
615
+ Convert Supervisely dataset to COCO format.
616
+
617
+ Note: Depending on the `return_type` and `with_captions` parameters, the function returns different values.
618
+
619
+ :param dataset: Supervisely dataset.
620
+ :type dataset: :class:`Dataset<supervisely.project.dataset.Dataset>`
621
+ :param meta: Project meta information.
622
+ :type meta: :class:`ProjectMeta<supervisely.project.project_meta.ProjectMeta>`
623
+ :param return_type: Type of return value.
624
+ If 'path', returns paths to COCO dataset files.
625
+ If 'dict', returns COCO dataset dictionaries.
626
+ :param dest_dir: Destination path to save COCO dataset.
627
+ :type dest_dir: :class:`str`, optional
628
+ :param copy_images: If True, copies images to the destination directory.
629
+ :type copy_images: :class:`bool`, optional
630
+ :param with_captions: If True, returns COCO captions.
631
+ :type with_captions: :class:`bool`, optional
632
+ :param log_progress: If True, logs the progress of the conversion.
633
+ :type log_progress: :class:`bool`, optional
634
+ :param progress_cb: Callback function to track the progress of the conversion.
635
+ :type progress_cb: :class:`Callable`, optional
636
+ :return:
637
+ If return_type is 'path', returns paths to COCO dataset file or file (instances or instances and captions).
638
+ If return_type is 'dict', returns COCO dataset dictionary or dictionaries (instances or instances and captions).
639
+ :rtype: :class:`tuple`
640
+
641
+ :Usage example:
642
+
643
+ .. code-block:: python
644
+
645
+ import supervisely as sly
646
+ from supervisely.convert.image.coco.coco_helper import sly_ds_to_coco
647
+
648
+ project_path = "/home/admin/work/supervisely/projects/lemons_annotated"
649
+ project = sly.Project(project_path, sly.OpenMode.READ)
650
+
651
+ for ds in project.datasets:
652
+ dest_dir = "/home/admin/work/supervisely/projects/lemons_annotated/ds1"
653
+ coco_json, coco_captions, coco_json_path, coco_captions_path = sly_ds_to_coco(ds, project.meta, save=True, dest_dir=dest_dir)
654
+ """
655
+ dest_dir = Path(dataset.path).parent / "coco" if dest_dir is None else Path(dest_dir)
656
+ save_json = return_type == "path"
657
+ if save_json is True:
658
+ annotations_dir = dest_dir / "annotations"
659
+ annotations_dir.mkdir(parents=True, exist_ok=True)
660
+ if copy_images is True:
661
+ images_dir = dest_dir / "images"
662
+ images_dir.mkdir(parents=True, exist_ok=True)
663
+
664
+ if progress_cb is not None:
665
+ log_progress = False
666
+
667
+ if log_progress:
668
+ progress_cb = tqdm_sly(
669
+ desc=f"Converting dataset '{dataset.short_name}' to COCO format",
670
+ total=len(dataset),
671
+ ).update
672
+
673
+ coco_ann = create_coco_ann_template(meta)
674
+ coco_captions = create_coco_ann_template(meta) if with_captions else None
675
+
676
+ image_coco = lambda info, idx: dict(
677
+ license="None",
678
+ file_name=info.name,
679
+ url="None",
680
+ height=info.height,
681
+ width=info.width,
682
+ date_captured=info.created_at,
683
+ id=idx,
684
+ sly_id=info.id,
685
+ )
686
+
687
+ class_mapping = {cls.name: idx for idx, cls in enumerate(meta.obj_classes)}
688
+ label_id = 0
689
+ caption_id = 0
690
+ for image_idx, name in enumerate(dataset.get_items_names(), 1):
691
+ img_path = dataset.get_img_path(name)
692
+ img_name = get_file_name_with_ext(img_path)
693
+ ann_path = dataset.get_ann_path(name)
694
+ img_info_path = dataset.get_img_info_path(name)
695
+
696
+ if copy_images:
697
+ dst_img_path = images_dir / img_name
698
+ shutil.copy(img_path, dst_img_path)
699
+
700
+ if os.path.exists(img_info_path):
701
+ image_info_json = load_json_file(img_info_path)
702
+ else:
703
+ img = sly_image(img_path, remove_alpha_channel=False)
704
+ now = datetime.now()
705
+ image_info_json = {
706
+ "id": None,
707
+ "name": img_name,
708
+ "height": img.shape[0],
709
+ "width": img.shape[1],
710
+ "created_at": now.strftime("%Y-%m-%d %H:%M:%S"),
711
+ }
712
+ image_info = ImageApi._convert_json_info(ImageApi(None), image_info_json)
713
+
714
+ coco_ann["images"].append(image_coco(image_info, image_idx))
715
+ if with_captions is True:
716
+ coco_captions["images"].append(image_coco(image_info, image_idx)) # pylint: disable=unsubscriptable-object
717
+
718
+ ann = Annotation.load_json_file(ann_path, meta)
719
+ if ann.img_size is None or ann.img_size == (0, 0) or ann.img_size == (None, None):
720
+ img = sly_image(img_path)
721
+ ann = ann.clone(img_size=[img.shape[0], img.shape[1]])
722
+ insts, captions = ann.to_coco(
723
+ image_idx, class_mapping, coco_ann, label_id, coco_captions, caption_id
724
+ )
725
+ label_id += len(insts)
726
+ caption_id += len(captions)
727
+
728
+ if progress_cb is not None:
729
+ progress_cb(1)
730
+
731
+ ann_path = None
732
+ captions_path = None
733
+ if save_json is True:
734
+ logger.info("Saving COCO annotations to disk...")
735
+ ann_path = str(annotations_dir / COCO_INSTANCES_FILE)
736
+ dump_json_file(coco_ann, ann_path)
737
+ logger.info(f"Saved COCO instances to '{ann_path}'")
738
+
739
+ if with_captions is True:
740
+ captions_path = str(annotations_dir / COCO_CAPTIONS_FILE)
741
+ dump_json_file(coco_captions, captions_path)
742
+ logger.info(f"Saved COCO captions to '{captions_path}'")
743
+
744
+ return ann_path, captions_path
745
+ return ann_path
746
+ if with_captions:
747
+ return coco_ann, coco_captions
748
+ return coco_ann
749
+
750
+
751
+ def sly_project_to_coco(
752
+ project: Union[Project, str],
753
+ dest_dir: Optional[str] = None,
754
+ copy_images: bool = False,
755
+ with_captions: bool = False,
756
+ log_progress: bool = True,
757
+ progress_cb: Optional[Callable] = None,
758
+ ) -> None:
759
+ """
760
+ Convert Supervisely project to COCO format.
761
+
762
+ :param project: Supervisely project.
763
+ :type project: :class:`Project<supervisely.project.project.Project>` or :class:`str`
764
+ :param dest_dir: Destination directory.
765
+ :type dest_dir: :class:`str`, optional
766
+ :param copy_images: Copy images to destination directory.
767
+ :type copy_images: :class:`bool`, optional
768
+ :param with_captions: Return COCO captions.
769
+ :type with_captions: :class:`bool`, optional
770
+ :param log_progress: Show uploading progress bar.
771
+ :type log_progress: :class:`bool`
772
+ :param progress_cb: Function for tracking conversion progress (for all items in the project).
773
+ :type progress_cb: callable, optional
774
+ :return: None
775
+ :rtype: NoneType
776
+
777
+ :Usage example:
778
+
779
+ .. code-block:: python
780
+
781
+ import supervisely as sly
782
+
783
+ # Local folder with Project
784
+ project_directory = "/home/admin/work/supervisely/source/project"
785
+
786
+ # Convert Project to COCO format
787
+ sly.Project(project_directory).to_coco(log_progress=True)
788
+ """
789
+ if isinstance(project, str):
790
+ project = Project(project, mode=OpenMode.READ)
791
+
792
+ dest_dir = Path(dest_dir) if dest_dir is not None else Path(project.directory).parent / "coco"
793
+ dest_dir.mkdir(parents=True, exist_ok=True)
794
+
795
+ if progress_cb is not None:
796
+ log_progress = False
797
+
798
+ if log_progress:
799
+ progress_cb = tqdm_sly(
800
+ desc="Converting Supervisely project to COCO format", total=project.total_items
801
+ ).update
802
+
803
+ used_ds_names = set()
804
+ for ds in project.datasets:
805
+ ds: Dataset
806
+ coco_dir = generate_free_name(used_ds_names, ds.short_name, extend_used_names=True)
807
+ ds.to_coco(
808
+ meta=project.meta,
809
+ return_type="path",
810
+ dest_dir=dest_dir / coco_dir,
811
+ copy_images=copy_images,
812
+ with_captions=with_captions,
813
+ log_progress=log_progress,
814
+ progress_cb=progress_cb,
815
+ )
816
+ logger.info(f"Dataset '{ds.short_name}' has been converted to COCO format.")
817
+ logger.info(f"Project '{project.name}' has been converted to COCO format.")