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.
- supervisely/__init__.py +2 -0
- supervisely/annotation/annotation.py +138 -1
- supervisely/convert/__init__.py +12 -56
- supervisely/convert/base_converter.py +12 -1
- supervisely/convert/image/__init__.py +22 -0
- supervisely/convert/image/coco/coco_helper.py +494 -2
- supervisely/convert/image/pascal_voc/pascal_voc_helper.py +417 -11
- supervisely/convert/image/yolo/yolo_helper.py +339 -4
- supervisely/convert/pointcloud/__init__.py +8 -0
- supervisely/convert/pointcloud_episodes/__init__.py +9 -0
- supervisely/convert/video/__init__.py +3 -0
- supervisely/convert/volume/__init__.py +3 -0
- supervisely/nn/training/gui/gui.py +25 -1
- supervisely/nn/training/gui/hyperparameters_selector.py +11 -1
- supervisely/nn/training/gui/model_selector.py +8 -2
- supervisely/nn/training/gui/training_artifacts.py +1 -1
- supervisely/nn/training/train_app.py +151 -46
- supervisely/project/project.py +311 -2
- {supervisely-6.73.277.dist-info → supervisely-6.73.279.dist-info}/METADATA +1 -1
- {supervisely-6.73.277.dist-info → supervisely-6.73.279.dist-info}/RECORD +24 -24
- {supervisely-6.73.277.dist-info → supervisely-6.73.279.dist-info}/LICENSE +0 -0
- {supervisely-6.73.277.dist-info → supervisely-6.73.279.dist-info}/WHEEL +0 -0
- {supervisely-6.73.277.dist-info → supervisely-6.73.279.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.277.dist-info → supervisely-6.73.279.dist-info}/top_level.txt +0 -0
|
@@ -1,23 +1,42 @@
|
|
|
1
1
|
import os
|
|
2
|
-
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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.")
|