supervisely 6.73.276__py3-none-any.whl → 6.73.278__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,23 +1,42 @@
1
1
  import os
2
- from typing import List, Tuple
2
+ import shutil
3
+ from pathlib import Path
4
+ from typing import Callable, Dict, List, Optional, OrderedDict, Tuple, Union
3
5
 
4
6
  import numpy as np
5
- from supervisely.convert.image.image_helper import validate_image_bounds
7
+ from PIL import Image
8
+ from tqdm import tqdm
9
+
6
10
  from supervisely import (
7
11
  Annotation,
12
+ Dataset,
8
13
  Label,
9
14
  ObjClass,
10
15
  ObjClassCollection,
16
+ Project,
11
17
  ProjectMeta,
12
18
  generate_free_name,
13
19
  logger,
14
20
  )
21
+ from supervisely.convert.image.image_helper import validate_image_bounds
15
22
  from supervisely.geometry.bitmap import Bitmap
23
+ from supervisely.geometry.polygon import Polygon
16
24
  from supervisely.geometry.rectangle import Rectangle
25
+ from supervisely.imaging.color import generate_rgb
17
26
  from supervisely.imaging.image import read
27
+ from supervisely.io.fs import file_exists, get_file_ext, get_file_name
28
+ from supervisely.io.json import load_json_file
29
+ from supervisely.task.progress import tqdm_sly
18
30
 
19
31
  MASKS_EXTENSION = ".png"
20
32
 
33
+ # Export
34
+ SUPPORTED_GEOMETRY_TYPES = {Bitmap, Polygon, Rectangle}
35
+ VALID_IMG_EXT = {".jpe", ".jpeg", ".jpg"}
36
+ TRAIN_TAG_NAME = "train"
37
+ VAL_TAG_NAME = "val"
38
+ TRAINVAL_TAG_NAME = "trainval"
39
+
21
40
  default_classes_colors = {
22
41
  "neutral": (224, 224, 192),
23
42
  "aeroplane": (128, 0, 0),
@@ -60,15 +79,23 @@ def get_col2coord(img: np.ndarray) -> dict:
60
79
 
61
80
  def read_colors(colors_file: str) -> Tuple[ObjClassCollection, dict]:
62
81
  if os.path.isfile(colors_file):
63
- logger.info("Will try to read segmentation colors from provided file.")
64
- in_lines = filter(None, map(str.strip, open(colors_file, "r").readlines()))
65
- in_splitted = (x.split() for x in in_lines)
66
- # Format: {name: (R, G, B)}, values [0; 255]
67
- cls2col = {}
68
- for x in in_splitted:
69
- if len(x) != 4:
70
- raise ValueError("Invalid format of colors file.")
71
- cls2col[x[0]] = (int(x[1]), int(x[2]), int(x[3]))
82
+ try:
83
+ logger.info("Will try to read segmentation colors from provided file.")
84
+ with open(colors_file, "r") as file:
85
+ cls2col = {}
86
+ for line in file:
87
+ parts = line.strip().split()
88
+ if len(parts) < 4:
89
+ raise ValueError("Invalid format of colors file.")
90
+ class_name = " ".join(parts[:-3])
91
+ colors = tuple(map(int, parts[-3:]))
92
+ cls2col[class_name] = colors
93
+ except Exception as e:
94
+ logger.warning(
95
+ "Failed to read segmentation colors from provided file. "
96
+ "Will use default PascalVOC color mapping."
97
+ )
98
+ cls2col = default_classes_colors
72
99
  else:
73
100
  logger.info("Will use default PascalVOC color mapping.")
74
101
  cls2col = default_classes_colors
@@ -87,6 +114,7 @@ def read_colors(colors_file: str) -> Tuple[ObjClassCollection, dict]:
87
114
  color2class_name = {v: k for k, v in cls2col.items()}
88
115
  return obj_classes, color2class_name
89
116
 
117
+
90
118
  def get_ann(
91
119
  item,
92
120
  color2class_name: dict,
@@ -230,3 +258,381 @@ def update_meta_from_xml(
230
258
  bbox_classes_map[original_class_name] = class_name
231
259
 
232
260
  return meta
261
+
262
+
263
+ def sly_ann_to_pascal_voc(ann: Annotation, image_name: str) -> Tuple[dict]:
264
+ """
265
+ Convert Supervisely annotation to Pascal VOC format annotation.
266
+
267
+ :param ann: Supervisely annotation.
268
+ :type ann: :class:`Annotation<supervisely.annotation.annotation.Annotation>`
269
+ :param image_name: Image name.
270
+ :type image_name: :class:`str`
271
+ :return: Tuple with xml tree and instance and class masks in PIL.Image format.
272
+ :rtype: :class:`Tuple`
273
+
274
+ :Usage example:
275
+
276
+ .. code-block:: python
277
+
278
+ import supervisely as sly
279
+ from supervisely.convert.image.pascal_voc.pascal_voc_helper import sly_ann_to_pascal_voc
280
+
281
+ ann = sly.Annotation.from_json(ann_json, meta)
282
+ xml_tree, instance_mask, class_mask = sly_ann_to_pascal_voc(ann, image_name)
283
+ """
284
+
285
+ def from_ann_to_instance_mask(ann: Annotation, contour_thickness: int = 3):
286
+ mask = np.zeros((ann.img_size[0], ann.img_size[1], 3), dtype=np.uint8)
287
+ for label in ann.labels:
288
+ if label.obj_class.geometry_type == Rectangle:
289
+ continue
290
+
291
+ if label.obj_class.name == "neutral":
292
+ label.geometry.draw(mask, default_classes_colors["neutral"])
293
+ continue
294
+
295
+ label.geometry.draw_contour(mask, default_classes_colors["neutral"], contour_thickness)
296
+ label.geometry.draw(mask, label.obj_class.color)
297
+
298
+ res_mask = Image.fromarray(mask)
299
+ res_mask = res_mask.convert("P", palette=Image.ADAPTIVE) # pylint: disable=no-member
300
+ return res_mask
301
+
302
+ def from_ann_to_class_mask(ann: Annotation, contour_thickness: int = 3):
303
+ exist_colors = [[0, 0, 0], default_classes_colors["neutral"]]
304
+ mask = np.zeros((ann.img_size[0], ann.img_size[1], 3), dtype=np.uint8)
305
+ for label in ann.labels:
306
+ if label.obj_class.geometry_type == Rectangle:
307
+ continue
308
+
309
+ if label.obj_class.name == "neutral":
310
+ label.geometry.draw(mask, default_classes_colors["neutral"])
311
+ continue
312
+
313
+ new_color = generate_rgb(exist_colors)
314
+ exist_colors.append(new_color)
315
+ label.geometry.draw_contour(mask, default_classes_colors["neutral"], contour_thickness)
316
+ label.geometry.draw(mask, new_color)
317
+
318
+ res_mask = Image.fromarray(mask)
319
+ res_mask = res_mask.convert("P", palette=Image.ADAPTIVE) # pylint: disable=no-member
320
+ return res_mask
321
+
322
+ def from_ann_to_xml(ann: Annotation, image_name: str):
323
+ import lxml.etree as ET # pylint: disable=import-error
324
+
325
+ xml_root = ET.Element("annotation")
326
+
327
+ ET.SubElement(xml_root, "folder").text = f"VOC"
328
+ ET.SubElement(xml_root, "filename").text = image_name
329
+
330
+ xml_root_source = ET.SubElement(xml_root, "source")
331
+ ET.SubElement(xml_root_source, "database").text = ""
332
+
333
+ ET.SubElement(xml_root_source, "annotation").text = "PASCAL VOC"
334
+ ET.SubElement(xml_root_source, "image").text = ""
335
+
336
+ xml_root_size = ET.SubElement(xml_root, "size")
337
+ ET.SubElement(xml_root_size, "width").text = str(ann.img_size[1])
338
+ ET.SubElement(xml_root_size, "height").text = str(ann.img_size[0])
339
+ ET.SubElement(xml_root_size, "depth").text = "3"
340
+
341
+ ET.SubElement(xml_root, "segmented").text = "1" if len(ann.labels) > 0 else "0"
342
+
343
+ for label in ann.labels:
344
+ if label.obj_class.name == "neutral":
345
+ continue
346
+
347
+ bitmap_to_bbox = label.geometry.to_bbox()
348
+
349
+ xml_ann_obj = ET.SubElement(xml_root, "object")
350
+ ET.SubElement(xml_ann_obj, "name").text = label.obj_class.name
351
+ ET.SubElement(xml_ann_obj, "pose").text = "Unspecified"
352
+ ET.SubElement(xml_ann_obj, "truncated").text = "0"
353
+ ET.SubElement(xml_ann_obj, "difficult").text = "0"
354
+
355
+ xml_ann_obj_bndbox = ET.SubElement(xml_ann_obj, "bndbox")
356
+ ET.SubElement(xml_ann_obj_bndbox, "xmin").text = str(bitmap_to_bbox.left)
357
+ ET.SubElement(xml_ann_obj_bndbox, "ymin").text = str(bitmap_to_bbox.top)
358
+ ET.SubElement(xml_ann_obj_bndbox, "xmax").text = str(bitmap_to_bbox.right)
359
+ ET.SubElement(xml_ann_obj_bndbox, "ymax").text = str(bitmap_to_bbox.bottom)
360
+
361
+ tree = ET.ElementTree(xml_root)
362
+ return tree
363
+
364
+ pascal_ann = from_ann_to_xml(ann, image_name)
365
+ instance_mask = from_ann_to_instance_mask(ann)
366
+ class_mask = from_ann_to_class_mask(ann)
367
+ return pascal_ann, instance_mask, class_mask
368
+
369
+
370
+ def sly_ds_to_pascal_voc(
371
+ dataset: Dataset,
372
+ meta: ProjectMeta,
373
+ dest_dir: Optional[str] = None,
374
+ train_val_split_coef: float = 0.8,
375
+ log_progress: bool = False,
376
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
377
+ ) -> Tuple[Dict, Optional[Dict]]:
378
+ """
379
+ Convert Supervisely dataset to Pascal VOC format.
380
+
381
+ :param meta: Project meta information.
382
+ :type meta: :class:`ProjectMeta<supervisely.project.project_meta.ProjectMeta>`
383
+ :param dest_dir: Destination directory.
384
+ :type dest_dir: :class:`str`, optional
385
+ :param train_val_split_coef: Coefficient for splitting images into train and validation sets.
386
+ :type train_val_split_coef: :class:`float`, optional
387
+ :param log_progress: If True, log progress.
388
+ :type log_progress: :class:`str`, optional
389
+ :param progress_cb: Progress callback.
390
+ :type progress_cb: :class:`Callable`, optional
391
+ :return: None
392
+ :rtype: NoneType
393
+
394
+ :Usage example:
395
+
396
+ .. code-block:: python
397
+
398
+ import supervisely as sly
399
+ from supervisely.convert.image.pascal_voc.pascal_voc_helper import sly_ds_to_pascal_voc
400
+
401
+ project_path = "/home/admin/work/supervisely/projects/lemons_annotated"
402
+ project = sly.Project(project_path, sly.OpenMode.READ)
403
+
404
+ for ds in project.datasets:
405
+ dest_dir = "/home/admin/work/supervisely/projects/lemons_annotated_pascal_voc"
406
+ sly_ds_to_pascal_voc(ds, project.meta, dest_dir=dest_dir)
407
+ """
408
+ import lxml.etree as ET # pylint: disable=import-error
409
+
410
+ def write_main_set(
411
+ is_trainval: int,
412
+ images_stats: dict,
413
+ meta: ProjectMeta,
414
+ result_main_sets_dir: str,
415
+ result_segmentation_sets_dir: str,
416
+ ):
417
+ res_files = ["trainval.txt", "train.txt", "val.txt"]
418
+ for file in os.listdir(result_segmentation_sets_dir):
419
+ if file in res_files:
420
+ shutil.copyfile(
421
+ os.path.join(result_segmentation_sets_dir, file),
422
+ os.path.join(result_main_sets_dir, file),
423
+ )
424
+
425
+ train_imgs = [i for i in images_stats if i["dataset"] == TRAIN_TAG_NAME]
426
+ val_imgs = [i for i in images_stats if i["dataset"] == VAL_TAG_NAME]
427
+
428
+ write_objs = [
429
+ {"suffix": "trainval", "imgs": images_stats},
430
+ {"suffix": "train", "imgs": train_imgs},
431
+ {"suffix": "val", "imgs": val_imgs},
432
+ ]
433
+
434
+ if is_trainval == 1:
435
+ trainval_imgs = [
436
+ i for i in images_stats if i["dataset"] == TRAIN_TAG_NAME + VAL_TAG_NAME
437
+ ]
438
+ write_objs[0] = {"suffix": "trainval", "imgs": trainval_imgs}
439
+
440
+ for obj_cls in meta.obj_classes:
441
+ if obj_cls.geometry_type not in SUPPORTED_GEOMETRY_TYPES:
442
+ continue
443
+ if obj_cls.name == "neutral":
444
+ continue
445
+ for o in write_objs:
446
+ with open(
447
+ os.path.join(result_main_sets_dir, f'{obj_cls.name}_{o["suffix"]}.txt'), "a"
448
+ ) as f:
449
+ for img_stats in o["imgs"]:
450
+ v = "1" if obj_cls.name in img_stats["classes"] else "-1"
451
+ f.write(f'{img_stats["name"]} {v}\n')
452
+
453
+ def write_segm_set(is_trainval: int, images_stats: dict, result_imgsets_dir: str):
454
+ with open(os.path.join(result_imgsets_dir, "trainval.txt"), "a") as f:
455
+ if is_trainval == 1:
456
+ f.writelines(
457
+ i["name"] + "\n"
458
+ for i in images_stats
459
+ if i["dataset"] == TRAIN_TAG_NAME + VAL_TAG_NAME
460
+ )
461
+ else:
462
+ f.writelines(i["name"] + "\n" for i in images_stats)
463
+ with open(os.path.join(result_imgsets_dir, "train.txt"), "a") as f:
464
+ f.writelines(i["name"] + "\n" for i in images_stats if i["dataset"] == TRAIN_TAG_NAME)
465
+ with open(os.path.join(result_imgsets_dir, "val.txt"), "a") as f:
466
+ f.writelines(i["name"] + "\n" for i in images_stats if i["dataset"] == VAL_TAG_NAME)
467
+
468
+ if progress_cb is not None:
469
+ log_progress = False
470
+
471
+ if log_progress:
472
+ progress_cb = tqdm_sly(
473
+ desc=f"Converting dataset '{dataset.short_name}' to Pascal VOC format",
474
+ total=len(dataset),
475
+ )
476
+
477
+ logger.info(f"Processing dataset: '{dataset.name}'")
478
+
479
+ # Prepare Pascal VOC root directory
480
+ if dest_dir is None:
481
+ dest_dir = str(Path(dataset.path).parent / "pascal_voc")
482
+
483
+ pascal_root_path = os.path.join(dest_dir, "VOCdevkit", "VOC")
484
+ result_images_dir = os.path.join(pascal_root_path, "JPEGImages")
485
+ result_ann_dir = os.path.join(pascal_root_path, "Annotations")
486
+ result_obj_dir = os.path.join(pascal_root_path, "SegmentationObject")
487
+ result_class_dir = os.path.join(pascal_root_path, "SegmentationClass")
488
+ result_image_sets_dir = os.path.join(pascal_root_path, "ImageSets")
489
+ result_segmentation_sets_dir = os.path.join(result_image_sets_dir, "Segmentation")
490
+ result_main_sets_dir = os.path.join(result_image_sets_dir, "Main")
491
+ result_colors_file_path = os.path.join(pascal_root_path, "colors.txt")
492
+
493
+ # Create directories if not exist
494
+ os.makedirs(result_images_dir, exist_ok=True)
495
+ os.makedirs(result_ann_dir, exist_ok=True)
496
+ os.makedirs(result_obj_dir, exist_ok=True)
497
+ os.makedirs(result_class_dir, exist_ok=True)
498
+ os.makedirs(result_image_sets_dir, exist_ok=True)
499
+ os.makedirs(result_segmentation_sets_dir, exist_ok=True)
500
+ os.makedirs(result_main_sets_dir, exist_ok=True)
501
+
502
+ # Create colors.txt file
503
+ if not file_exists(result_colors_file_path):
504
+ with open(result_colors_file_path, "w") as f:
505
+ f.write(
506
+ f"neutral {default_classes_colors['neutral'][0]} {default_classes_colors['neutral'][1]} {default_classes_colors['neutral'][2]}\n"
507
+ )
508
+
509
+ image_stats = []
510
+ classes_colors = {}
511
+ for item_name, img_path, ann_path in dataset.items():
512
+ # Assign unique name to avoid conflicts
513
+ unique_name = f"{dataset.name}_{get_file_name(item_name)}"
514
+
515
+ # Load annotation
516
+ ann = Annotation.from_json(load_json_file(ann_path), meta)
517
+ pascal_ann, instance_mask, class_mask = sly_ann_to_pascal_voc(ann, unique_name)
518
+
519
+ # Write ann
520
+ ann_path = os.path.join(result_ann_dir, f"{unique_name}.xml")
521
+ ET.indent(pascal_ann, space=" ")
522
+ pascal_ann.write(ann_path, pretty_print=True)
523
+
524
+ # Save instance mask
525
+ instance_mask_path = os.path.join(
526
+ result_obj_dir, f"{unique_name}_instance{MASKS_EXTENSION}"
527
+ )
528
+ instance_mask.save(instance_mask_path)
529
+
530
+ # Save class mask
531
+ class_mask_path = os.path.join(result_class_dir, f"{unique_name}_class{MASKS_EXTENSION}")
532
+ class_mask.save(class_mask_path)
533
+
534
+ # Save original image
535
+ img_ext = get_file_ext(img_path)
536
+ if img_ext not in VALID_IMG_EXT:
537
+ jpg_name = f"{unique_name}.jpg"
538
+ jpg_image_path = os.path.join(result_images_dir, jpg_name)
539
+ img = Image.open(img_path)
540
+ img.save(jpg_image_path, "JPEG")
541
+ else:
542
+ jpg_name = f"{unique_name}{img_ext}"
543
+ jpg_image_path = os.path.join(result_images_dir, jpg_name)
544
+ shutil.copyfile(img_path, jpg_image_path)
545
+
546
+ # Update stats
547
+ cur_img_stats = {"classes": set(), "dataset": None, "name": jpg_name}
548
+ image_stats.append(cur_img_stats)
549
+
550
+ # Get classes colors
551
+ for label in ann.labels:
552
+ cur_img_stats["classes"].add(label.obj_class.name)
553
+ classes_colors[label.obj_class.name] = tuple(label.obj_class.color)
554
+
555
+ if log_progress:
556
+ progress_cb.update(1)
557
+
558
+ # Update colors.txt file
559
+ classes_colors = OrderedDict((sorted(classes_colors.items(), key=lambda t: t[0])))
560
+ with open(result_colors_file_path, "a") as cc:
561
+ for k in classes_colors.keys():
562
+ if k == "neutral":
563
+ continue
564
+ cc.write(f"{k} {classes_colors[k][0]} {classes_colors[k][1]} {classes_colors[k][2]}\n")
565
+
566
+ # Create splits
567
+ imgs_to_split = [i for i in image_stats if i["dataset"] is None]
568
+ train_len = int(len(imgs_to_split) * train_val_split_coef)
569
+
570
+ for img_stat in imgs_to_split[:train_len]:
571
+ img_stat["dataset"] = TRAIN_TAG_NAME
572
+ for img_stat in imgs_to_split[train_len:]:
573
+ img_stat["dataset"] = VAL_TAG_NAME
574
+
575
+ is_trainval = 0
576
+ write_segm_set(is_trainval, image_stats, result_segmentation_sets_dir)
577
+ write_main_set(
578
+ is_trainval, image_stats, meta, result_main_sets_dir, result_segmentation_sets_dir
579
+ )
580
+
581
+
582
+ def sly_project_to_pascal_voc(
583
+ project: Union[Project, str],
584
+ dest_dir: Optional[str] = None,
585
+ train_val_split_coef: float = 0.8,
586
+ log_progress: bool = True,
587
+ progress_cb: Optional[Union[tqdm, Callable]] = None,
588
+ ) -> None:
589
+ """
590
+ Convert Supervisely project to Pascal VOC format.
591
+
592
+ :param dest_dir: Destination directory.
593
+ :type dest_dir: :class:`str`, optional
594
+ :param train_val_split_coef: Coefficient for splitting images into train and validation sets.
595
+ :type train_val_split_coef: :class:`float`, optional
596
+ :param log_progress: Show uploading progress bar.
597
+ :type log_progress: :class:`bool`
598
+ :param progress_cb: Function for tracking conversion progress (for all items in the project).
599
+ :type progress_cb: callable, optional
600
+ :return: None
601
+ :rtype: NoneType
602
+
603
+ :Usage example:
604
+
605
+ .. code-block:: python
606
+
607
+ import supervisely as sly
608
+
609
+ # Local folder with Project
610
+ project_directory = "/home/admin/work/supervisely/source/project"
611
+
612
+ # Convert Project to Pascal VOC format
613
+ sly.Project(project_directory).to_pascal_voc(log_progress=True)
614
+ """
615
+ if dest_dir is None:
616
+ dest_dir = project.directory
617
+
618
+ Path(dest_dir).mkdir(parents=True, exist_ok=True)
619
+
620
+ if progress_cb is not None:
621
+ log_progress = False
622
+
623
+ if log_progress:
624
+ progress_cb = tqdm_sly(
625
+ desc="Converting Supervisely project to Pascal VOC format", total=project.total_items
626
+ )
627
+
628
+ for dataset in project.datasets:
629
+ dataset: Dataset
630
+ dataset.to_pascal_voc(
631
+ meta=project.meta,
632
+ dest_dir=dest_dir,
633
+ train_val_split_coef=train_val_split_coef,
634
+ log_progress=log_progress,
635
+ progress_cb=progress_cb,
636
+ )
637
+ logger.info(f"Dataset '{dataset.short_name}' has been converted to Pascal VOC format.")
638
+ logger.info(f"Project '{project.name}' has been converted to Pascal VOC format.")