YoloDatasetCustomizer 0.3.0__tar.gz → 0.5.0__tar.gz
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.
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/.gitattributes +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/.github/workflows/ci.yml +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/.gitignore +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/.python-version +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/LICENSE +1 -1
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/PKG-INFO +17 -10
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/README.md +17 -11
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/pyproject.toml +59 -58
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/resources/Improvements.txt +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/src/YoloDatasetCustomizer/__init__.py +1 -1
- yolodatasetcustomizer-0.5.0/src/YoloDatasetCustomizer/customizer.py +325 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/src/YoloDatasetCustomizer/labels.py +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/src/YoloDatasetCustomizer/paths.py +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/src/YoloDatasetCustomizer/reader.py +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/src/YoloDatasetCustomizer/writer.py +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/test/YoloDatasetCustomizer_test.py +68 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/uv.lock +1117 -1113
- yolodatasetcustomizer-0.3.0/src/YoloDatasetCustomizer/customizer.py +0 -174
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: YoloDatasetCustomizer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Load one or multiple datasets. Reduce, mix or merge into a custom dataset.
|
|
5
5
|
Project-URL: Homepage, https://github.com/mLuca/YoloDatasetCustomizer
|
|
6
6
|
Project-URL: Repository, https://github.com/mLuca/YoloDatasetCustomizer
|
|
@@ -23,6 +23,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
23
23
|
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
24
24
|
Requires-Python: >=3.10
|
|
25
25
|
Requires-Dist: pyyaml>=6.0.3
|
|
26
|
+
Requires-Dist: rich>=13.0.0
|
|
26
27
|
Description-Content-Type: text/markdown
|
|
27
28
|
|
|
28
29
|
# Intro
|
|
@@ -42,15 +43,21 @@ If a path ends on _data.yaml_ it will take that file into account.
|
|
|
42
43
|
If a path doesn't end on _/data.yaml_ it will recursevly look for all _data.yaml_ files from that directory downards.
|
|
43
44
|
e.g.
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
```python
|
|
47
|
+
from YoloDatasetCustomizer import YoloDatasetCustomizer
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
ydc = YoloDatasetCustomizer(list_to_existing_datasets)
|
|
50
|
+
ydc.add_data_sets([one_more_existing_dataset])
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
if ydc.create_new_dataset_for_class_names(
|
|
53
|
+
class_names=<set_of_wanted_class_names>,
|
|
54
|
+
dst_path=<optional_custom_path>,
|
|
55
|
+
data_set_name=<optional_custom_name>,
|
|
56
|
+
):
|
|
57
|
+
print("All good")
|
|
58
|
+
else:
|
|
59
|
+
print("Oh no.")
|
|
60
|
+
```
|
|
54
61
|
|
|
55
62
|
# Run tests
|
|
56
63
|
|
|
@@ -65,11 +72,11 @@ The project uses uv. Install dependencies and run tests with:
|
|
|
65
72
|
|
|
66
73
|
# Build project
|
|
67
74
|
|
|
68
|
-
Build the package
|
|
75
|
+
Build the package:
|
|
69
76
|
|
|
70
77
|
uv build
|
|
71
78
|
|
|
72
|
-
Publish to PyPI (still need twine)
|
|
79
|
+
Publish to PyPI (still need twine):
|
|
73
80
|
|
|
74
81
|
uv pip install twine
|
|
75
82
|
uv run twine upload dist/\*
|
|
@@ -15,15 +15,21 @@ If a path ends on _data.yaml_ it will take that file into account.
|
|
|
15
15
|
If a path doesn't end on _/data.yaml_ it will recursevly look for all _data.yaml_ files from that directory downards.
|
|
16
16
|
e.g.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
```python
|
|
19
|
+
from YoloDatasetCustomizer import YoloDatasetCustomizer
|
|
20
|
+
|
|
21
|
+
ydc = YoloDatasetCustomizer(list_to_existing_datasets)
|
|
22
|
+
ydc.add_data_sets([one_more_existing_dataset])
|
|
23
|
+
|
|
24
|
+
if ydc.create_new_dataset_for_class_names(
|
|
25
|
+
class_names=<set_of_wanted_class_names>,
|
|
26
|
+
dst_path=<optional_custom_path>,
|
|
27
|
+
data_set_name=<optional_custom_name>,
|
|
28
|
+
):
|
|
29
|
+
print("All good")
|
|
30
|
+
else:
|
|
31
|
+
print("Oh no.")
|
|
32
|
+
```
|
|
27
33
|
|
|
28
34
|
# Run tests
|
|
29
35
|
|
|
@@ -38,11 +44,11 @@ The project uses uv. Install dependencies and run tests with:
|
|
|
38
44
|
|
|
39
45
|
# Build project
|
|
40
46
|
|
|
41
|
-
Build the package
|
|
47
|
+
Build the package:
|
|
42
48
|
|
|
43
49
|
uv build
|
|
44
50
|
|
|
45
|
-
Publish to PyPI (still need twine)
|
|
51
|
+
Publish to PyPI (still need twine):
|
|
46
52
|
|
|
47
53
|
uv pip install twine
|
|
48
54
|
uv run twine upload dist/\*
|
|
@@ -1,58 +1,59 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
3
|
-
build-backend = "hatchling.build"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "YoloDatasetCustomizer"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Load one or multiple datasets. Reduce, mix or merge into a custom dataset."
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.10"
|
|
11
|
-
license = {text = "GPL-3.0-only"}
|
|
12
|
-
authors = [
|
|
13
|
-
{name = "Luca Mazzon", email = "mazzon.luca@gmail.com"},
|
|
14
|
-
]
|
|
15
|
-
keywords = ["ultralytics", "yolo", "dataset", "machine-learning", "computer-vision", "customization"]
|
|
16
|
-
classifiers = [
|
|
17
|
-
"Development Status :: 5 - Production/Stable",
|
|
18
|
-
"Intended Audience :: Developers",
|
|
19
|
-
"Intended Audience :: Science/Research",
|
|
20
|
-
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
21
|
-
"Programming Language :: Python :: 3",
|
|
22
|
-
"Programming Language :: Python :: 3.10",
|
|
23
|
-
"Programming Language :: Python :: 3.11",
|
|
24
|
-
"Programming Language :: Python :: 3.12",
|
|
25
|
-
"Programming Language :: Python :: 3.13",
|
|
26
|
-
"Programming Language :: Python :: 3.14",
|
|
27
|
-
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
28
|
-
"Topic :: Scientific/Engineering :: Image Recognition",
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
dependencies = [
|
|
32
|
-
"pyyaml>=6.0.3",
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "YoloDatasetCustomizer"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
description = "Load one or multiple datasets. Reduce, mix or merge into a custom dataset."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "GPL-3.0-only"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Luca Mazzon", email = "mazzon.luca@gmail.com"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ultralytics", "yolo", "dataset", "machine-learning", "computer-vision", "customization"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
28
|
+
"Topic :: Scientific/Engineering :: Image Recognition",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
dependencies = [
|
|
32
|
+
"pyyaml>=6.0.3",
|
|
33
|
+
"rich>=13.0.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/mLuca/YoloDatasetCustomizer"
|
|
38
|
+
Repository = "https://github.com/mLuca/YoloDatasetCustomizer"
|
|
39
|
+
"Issue Tracker" = "https://github.com/mLuca/YoloDatasetCustomizer/issues"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=9.1.1",
|
|
44
|
+
"twine>=6.2.0",
|
|
45
|
+
"ruff>=0.14.0",
|
|
46
|
+
"mypy>=1.18.0",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 120
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E", "F", "W", "I"]
|
|
54
|
+
ignore = ["E501"]
|
|
55
|
+
|
|
56
|
+
[tool.mypy]
|
|
57
|
+
python_version = "3.10"
|
|
58
|
+
files = ["src"]
|
|
59
|
+
ignore_missing_imports = true
|
|
File without changes
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.progress import BarColumn, MofNCompleteColumn, Progress, TaskProgressColumn, TextColumn
|
|
10
|
+
|
|
11
|
+
from .labels import LabelFile
|
|
12
|
+
from .paths import _SPLIT_NAMES, _get_unique_path, _normalize_path
|
|
13
|
+
from .reader import YoloDataFileReader
|
|
14
|
+
from .writer import YoloDataFileWriter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class YoloDatasetCustomizer:
|
|
18
|
+
"""Find and customize YOLO datasets across one or more directory paths."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, data_set_paths: list[str]):
|
|
21
|
+
"""Initialize with YAML dataset paths or directories to scan for YOLO datasets."""
|
|
22
|
+
self.__found_data_file_paths: set[str] = set()
|
|
23
|
+
self.__data_sets: list[YoloDataFileReader] = []
|
|
24
|
+
|
|
25
|
+
self.add_data_sets(data_set_paths)
|
|
26
|
+
self.SUPPORTED_IMAGE_FORMATS = [
|
|
27
|
+
".avif",
|
|
28
|
+
".bmp",
|
|
29
|
+
".dng",
|
|
30
|
+
".heic",
|
|
31
|
+
".jp2",
|
|
32
|
+
".jpeg",
|
|
33
|
+
".jpg",
|
|
34
|
+
".mpo",
|
|
35
|
+
".png",
|
|
36
|
+
".tif",
|
|
37
|
+
".tiff",
|
|
38
|
+
".webp",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
def add_data_sets(self, data_set_paths: list[str]):
|
|
42
|
+
"""Discover YOLO data.yaml files from explicit paths or directory trees."""
|
|
43
|
+
data_set_paths = [_normalize_path(path) for path in data_set_paths]
|
|
44
|
+
|
|
45
|
+
self.__found_data_file_paths.update(
|
|
46
|
+
[path for path in data_set_paths if os.path.basename(path).lower() == "data.yaml"]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
paths_without_file_ending = [
|
|
50
|
+
path for path in data_set_paths if os.path.basename(path).lower() != "data.yaml"
|
|
51
|
+
]
|
|
52
|
+
with Console().status("Searching for data.yaml files..."):
|
|
53
|
+
for path in paths_without_file_ending:
|
|
54
|
+
self.__found_data_file_paths.update(glob.glob(os.path.join(path, "**", "data.yaml"), recursive=True))
|
|
55
|
+
|
|
56
|
+
for path in self.__found_data_file_paths:
|
|
57
|
+
try:
|
|
58
|
+
ydfr = YoloDataFileReader(path)
|
|
59
|
+
self.__data_sets.append(ydfr)
|
|
60
|
+
except (yaml.YAMLError, TypeError, ValueError) as e:
|
|
61
|
+
print(f"ERROR: Issue loading YAML file at {path}: {e}")
|
|
62
|
+
except FileNotFoundError:
|
|
63
|
+
print(f"ERROR: Yaml file does not exist at {path}")
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def __read_label_files(label_paths: list[str], description: str) -> list[LabelFile]:
|
|
67
|
+
"""Parse label files in parallel (I/O bound) while showing a progress bar."""
|
|
68
|
+
label_files: list[LabelFile] = []
|
|
69
|
+
with Progress(
|
|
70
|
+
TextColumn("{task.description}"),
|
|
71
|
+
BarColumn(),
|
|
72
|
+
MofNCompleteColumn(),
|
|
73
|
+
transient=True,
|
|
74
|
+
) as progress:
|
|
75
|
+
task = progress.add_task(description, total=len(label_paths))
|
|
76
|
+
with ThreadPoolExecutor(max_workers=16) as pool:
|
|
77
|
+
for label_file in pool.map(LabelFile, label_paths):
|
|
78
|
+
label_files.append(label_file)
|
|
79
|
+
progress.advance(task)
|
|
80
|
+
return label_files
|
|
81
|
+
|
|
82
|
+
def get_found_data_file_paths(self) -> set[str]:
|
|
83
|
+
"""Return all discovered YOLO dataset YAML file paths."""
|
|
84
|
+
return self.__found_data_file_paths
|
|
85
|
+
|
|
86
|
+
def get_found_class_names(self) -> set[str]:
|
|
87
|
+
"""Return the union of all class names from discovered datasets."""
|
|
88
|
+
found_classes: set[str] = set()
|
|
89
|
+
for ydf in self.__data_sets:
|
|
90
|
+
found_classes.update(ydf.get_class_names())
|
|
91
|
+
return found_classes
|
|
92
|
+
|
|
93
|
+
def create_new_dataset_for_class_names(
|
|
94
|
+
self,
|
|
95
|
+
class_names: set[str],
|
|
96
|
+
dst_path: str = ".",
|
|
97
|
+
data_set_name: str = "new_dataset",
|
|
98
|
+
ignore_img_formats: bool = False,
|
|
99
|
+
) -> bool:
|
|
100
|
+
"""Create a filtered dataset containing only the requested classes."""
|
|
101
|
+
if len(class_names) == 0:
|
|
102
|
+
print("ERROR: No classes selected for new dataset.")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
new_dataset_path = os.path.join(_normalize_path(dst_path), data_set_name)
|
|
106
|
+
new_dataset_path = _get_unique_path(new_dataset_path)
|
|
107
|
+
|
|
108
|
+
new_class_indices = self.__generate_new_class_indices(class_names)
|
|
109
|
+
print(f"INFO: New class indices: {new_class_indices}")
|
|
110
|
+
|
|
111
|
+
# Always create every split folder so the folder structure matches what data.yaml references,
|
|
112
|
+
# even when no source dataset contributed images to a split.
|
|
113
|
+
for split_name in _SPLIT_NAMES:
|
|
114
|
+
os.makedirs(os.path.join(new_dataset_path, split_name, "labels"), exist_ok=True)
|
|
115
|
+
os.makedirs(os.path.join(new_dataset_path, split_name, "images"), exist_ok=True)
|
|
116
|
+
|
|
117
|
+
# Plan first, so every stage knows how many files it is going to copy
|
|
118
|
+
planned_files: dict[str, list[tuple[LabelFile, dict[str, list[str]], dict[str, str]]]] = {
|
|
119
|
+
split_name: [] for split_name in _SPLIT_NAMES
|
|
120
|
+
}
|
|
121
|
+
fallback_choice: str | None = None # Sticky answer: "all" or "none"
|
|
122
|
+
for data_set in self.__data_sets:
|
|
123
|
+
common_class_names = set(data_set.get_class_names()) & class_names
|
|
124
|
+
if not common_class_names:
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
old_to_new_index_match: dict[str, str] = {}
|
|
128
|
+
for class_name in common_class_names:
|
|
129
|
+
old_indices = data_set.get_indices_for_names({class_name})
|
|
130
|
+
if not old_indices:
|
|
131
|
+
continue
|
|
132
|
+
old_index = next(iter(old_indices))
|
|
133
|
+
new_index = new_class_indices[class_name]
|
|
134
|
+
old_to_new_index_match[old_index] = new_index
|
|
135
|
+
|
|
136
|
+
for split_name in _SPLIT_NAMES:
|
|
137
|
+
for split_path in data_set.get_split_paths_for_split_name(split_name):
|
|
138
|
+
old_images_path = os.path.join(_normalize_path(data_set.get_file_dir()), split_path)
|
|
139
|
+
old_labels_path = self.__generate_label_path_from_img_path(old_images_path)
|
|
140
|
+
|
|
141
|
+
missing_path = next(
|
|
142
|
+
(path for path in (old_labels_path, old_images_path) if not os.path.exists(path)), None
|
|
143
|
+
)
|
|
144
|
+
if missing_path is not None:
|
|
145
|
+
fallback_images_path = self.__find_fallback_images_path(data_set, split_path)
|
|
146
|
+
if fallback_images_path is None:
|
|
147
|
+
print(
|
|
148
|
+
f"WARNING: Path '{missing_path}' from '{data_set.get_file_path()}' "
|
|
149
|
+
f"does not exist! Skipping it."
|
|
150
|
+
)
|
|
151
|
+
continue
|
|
152
|
+
try:
|
|
153
|
+
choice = fallback_choice or self.__ask_use_fallback(
|
|
154
|
+
data_set.get_file_path(), split_path, fallback_images_path
|
|
155
|
+
)
|
|
156
|
+
except EOFError:
|
|
157
|
+
print(
|
|
158
|
+
"ERROR: Cannot prompt for Roboflow fallback because standard input is closed. "
|
|
159
|
+
"Dataset creation aborted."
|
|
160
|
+
)
|
|
161
|
+
return False
|
|
162
|
+
if choice in ("all", "none"):
|
|
163
|
+
fallback_choice = choice
|
|
164
|
+
if choice == "abort":
|
|
165
|
+
print("INFO: Aborted by user.")
|
|
166
|
+
return False
|
|
167
|
+
if choice in ("no", "none"):
|
|
168
|
+
print(f"WARNING: Skipping '{split_path}' of '{data_set.get_file_path()}'.")
|
|
169
|
+
continue
|
|
170
|
+
old_images_path = fallback_images_path
|
|
171
|
+
old_labels_path = self.__generate_label_path_from_img_path(old_images_path)
|
|
172
|
+
|
|
173
|
+
# Index images by exact stem so 'car.*' can't pick up e.g. 'car.v2.jpg'.
|
|
174
|
+
# Only regular files with an extension count, like the old 'car.*' lookup.
|
|
175
|
+
images_by_stem: dict[str, list[str]] = {}
|
|
176
|
+
with os.scandir(old_images_path) as entries:
|
|
177
|
+
for entry in entries:
|
|
178
|
+
stem, ext = os.path.splitext(entry.name)
|
|
179
|
+
if ext and entry.is_file():
|
|
180
|
+
images_by_stem.setdefault(stem, []).append(entry.path)
|
|
181
|
+
|
|
182
|
+
with os.scandir(old_labels_path) as entries:
|
|
183
|
+
label_paths = [
|
|
184
|
+
e.path for e in entries
|
|
185
|
+
if os.path.normcase(e.name).endswith(".txt") and e.is_file()
|
|
186
|
+
]
|
|
187
|
+
wanted_indices = set(old_to_new_index_match)
|
|
188
|
+
for label_file in self.__read_label_files(
|
|
189
|
+
label_paths, f"Scanning {split_name:<5} labels of {data_set.get_file_path()}"
|
|
190
|
+
):
|
|
191
|
+
if label_file.get_class_indices() & wanted_indices:
|
|
192
|
+
planned_files[split_name].append((label_file, images_by_stem, old_to_new_index_match))
|
|
193
|
+
|
|
194
|
+
# One bar per stage (white, filled green), all visible from the start
|
|
195
|
+
with Progress(
|
|
196
|
+
TextColumn("{task.description}"),
|
|
197
|
+
BarColumn(style="white", complete_style="green", finished_style="green"),
|
|
198
|
+
TaskProgressColumn(),
|
|
199
|
+
MofNCompleteColumn(),
|
|
200
|
+
) as progress:
|
|
201
|
+
stage_tasks = {
|
|
202
|
+
split_name: progress.add_task(
|
|
203
|
+
f"[{stage}/{len(_SPLIT_NAMES)}] {split_name:<5}", total=len(planned_files[split_name])
|
|
204
|
+
)
|
|
205
|
+
for stage, split_name in enumerate(_SPLIT_NAMES, start=1)
|
|
206
|
+
}
|
|
207
|
+
for split_name in _SPLIT_NAMES:
|
|
208
|
+
new_labels_path = os.path.join(new_dataset_path, split_name, "labels")
|
|
209
|
+
new_images_path = os.path.join(new_dataset_path, split_name, "images")
|
|
210
|
+
for label_file, images_by_stem, old_to_new_index_match in planned_files[split_name]:
|
|
211
|
+
if not self.__copy_label_and_images(
|
|
212
|
+
label_file,
|
|
213
|
+
images_by_stem,
|
|
214
|
+
old_to_new_index_match,
|
|
215
|
+
new_labels_path,
|
|
216
|
+
new_images_path,
|
|
217
|
+
ignore_img_formats,
|
|
218
|
+
):
|
|
219
|
+
return False
|
|
220
|
+
progress.advance(stage_tasks[split_name])
|
|
221
|
+
|
|
222
|
+
train_split, val_split, test_split = _SPLIT_NAMES
|
|
223
|
+
data_file_writer = YoloDataFileWriter(
|
|
224
|
+
new_dataset_path,
|
|
225
|
+
list(new_class_indices.keys()),
|
|
226
|
+
train_split=train_split,
|
|
227
|
+
val_split=val_split,
|
|
228
|
+
test_split=test_split,
|
|
229
|
+
)
|
|
230
|
+
try:
|
|
231
|
+
data_file_writer.write()
|
|
232
|
+
except Exception as e:
|
|
233
|
+
print(f"ERROR: Could not write new data.yaml to {new_dataset_path}. Generation of {data_set_name} is incomplete!")
|
|
234
|
+
print(f"Exception was: {e}")
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
print(f"SUCCESS: Dataset '{os.path.basename(new_dataset_path)}' created at: {new_dataset_path}")
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
def __copy_label_and_images(
|
|
241
|
+
self,
|
|
242
|
+
label_file: LabelFile,
|
|
243
|
+
images_by_stem: dict[str, list[str]],
|
|
244
|
+
old_to_new_index_match: dict[str, str],
|
|
245
|
+
new_labels_path: str,
|
|
246
|
+
new_images_path: str,
|
|
247
|
+
ignore_img_formats: bool,
|
|
248
|
+
) -> bool:
|
|
249
|
+
"""Copy one label file with remapped indices plus its images. Returns False on error."""
|
|
250
|
+
new_label_file_path = label_file.copy_by_class_indices(old_to_new_index_match, new_labels_path)
|
|
251
|
+
if not new_label_file_path:
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
file_name_no_ext = os.path.splitext(os.path.basename(label_file.file_path))[0]
|
|
255
|
+
for img in images_by_stem.get(file_name_no_ext, []):
|
|
256
|
+
_, ext = os.path.splitext(img)
|
|
257
|
+
|
|
258
|
+
if not ignore_img_formats and ext.lower() not in self.SUPPORTED_IMAGE_FORMATS:
|
|
259
|
+
print(
|
|
260
|
+
f"ERROR: Image format not supported by yolo: {img}\n"
|
|
261
|
+
f"Supported formats are: {self.SUPPORTED_IMAGE_FORMATS}"
|
|
262
|
+
)
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
new_image_name, _ = os.path.splitext(os.path.basename(new_label_file_path))
|
|
266
|
+
destination = os.path.join(new_images_path, new_image_name + ext)
|
|
267
|
+
try:
|
|
268
|
+
shutil.copyfile(img, destination)
|
|
269
|
+
except OSError as e:
|
|
270
|
+
print(
|
|
271
|
+
f"ERROR: Image could not be copied:\n"
|
|
272
|
+
f"Source: {img}\nDestination: {destination}\n"
|
|
273
|
+
f"Exception was: {e}"
|
|
274
|
+
)
|
|
275
|
+
return False
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
def __find_fallback_images_path(self, data_set: YoloDataFileReader, split_path: str) -> str | None:
|
|
279
|
+
"""Return the images path with leading '../' removed (legacy Roboflow style) if it and its labels exist."""
|
|
280
|
+
stripped = split_path.replace("\\", "/")
|
|
281
|
+
while stripped.startswith("../"):
|
|
282
|
+
stripped = stripped[3:]
|
|
283
|
+
if stripped == split_path.replace("\\", "/"):
|
|
284
|
+
return None
|
|
285
|
+
images_path = os.path.join(_normalize_path(data_set.get_file_dir()), stripped)
|
|
286
|
+
labels_path = self.__generate_label_path_from_img_path(images_path)
|
|
287
|
+
if os.path.isdir(images_path) and os.path.isdir(labels_path):
|
|
288
|
+
return os.path.normpath(images_path)
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
@staticmethod
|
|
292
|
+
def __ask_use_fallback(yaml_path: str, split_path: str, fallback_path: str) -> str:
|
|
293
|
+
"""Ask the user whether to use the fallback folder. Returns yes/all/no/none/abort."""
|
|
294
|
+
options = {"1": "yes", "2": "all", "3": "no", "4": "none", "5": "abort"}
|
|
295
|
+
print(
|
|
296
|
+
f"'{split_path}' in '{yaml_path}' does not exist, but was found at:\n {fallback_path}\n"
|
|
297
|
+
f"Use it? [1] Yes [2] All [3] No [4] None [5] Abort: ",
|
|
298
|
+
end="",
|
|
299
|
+
flush=True,
|
|
300
|
+
)
|
|
301
|
+
while True:
|
|
302
|
+
# getwch() bypasses stdin, so only use it for an interactive console
|
|
303
|
+
if sys.platform == "win32" and sys.stdin is not None and sys.stdin.isatty():
|
|
304
|
+
import msvcrt
|
|
305
|
+
|
|
306
|
+
key = msvcrt.getwch()
|
|
307
|
+
if key == "\x03":
|
|
308
|
+
raise KeyboardInterrupt
|
|
309
|
+
else:
|
|
310
|
+
key = (input() or " ")[0]
|
|
311
|
+
if key in options:
|
|
312
|
+
print(key)
|
|
313
|
+
return options[key]
|
|
314
|
+
|
|
315
|
+
def __generate_new_class_indices(self, class_names: set[str]) -> dict[str, str]:
|
|
316
|
+
"""Generate a fresh label index mapping for the selected class names."""
|
|
317
|
+
ret: dict[str, str] = {}
|
|
318
|
+
for i, name in enumerate(class_names):
|
|
319
|
+
ret[name] = str(i)
|
|
320
|
+
return ret
|
|
321
|
+
|
|
322
|
+
def __generate_label_path_from_img_path(self, img_path: str) -> str:
|
|
323
|
+
"""Convert an images directory path into its corresponding labels directory path."""
|
|
324
|
+
img_path = _normalize_path(img_path)
|
|
325
|
+
return os.path.normpath(os.path.join(os.path.dirname(img_path), "labels"))
|
{yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/src/YoloDatasetCustomizer/labels.py
RENAMED
|
File without changes
|
{yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/src/YoloDatasetCustomizer/paths.py
RENAMED
|
File without changes
|
{yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/src/YoloDatasetCustomizer/reader.py
RENAMED
|
File without changes
|
{yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.5.0}/src/YoloDatasetCustomizer/writer.py
RENAMED
|
File without changes
|