YoloDatasetCustomizer 0.3.0__tar.gz → 0.4.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.
Potentially problematic release.
This version of YoloDatasetCustomizer might be problematic. Click here for more details.
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/.gitattributes +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/.github/workflows/ci.yml +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/.gitignore +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/.python-version +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/LICENSE +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/PKG-INFO +17 -10
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/README.md +17 -11
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/pyproject.toml +2 -1
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/resources/Improvements.txt +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/src/YoloDatasetCustomizer/__init__.py +1 -1
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/src/YoloDatasetCustomizer/customizer.py +86 -37
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/src/YoloDatasetCustomizer/labels.py +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/src/YoloDatasetCustomizer/paths.py +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/src/YoloDatasetCustomizer/reader.py +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/src/YoloDatasetCustomizer/writer.py +0 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/test/YoloDatasetCustomizer_test.py +21 -0
- {yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/uv.lock +8 -4
|
File without changes
|
|
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.4.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/\*
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "YoloDatasetCustomizer"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Load one or multiple datasets. Reduce, mix or merge into a custom dataset."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -30,6 +30,7 @@ classifiers = [
|
|
|
30
30
|
|
|
31
31
|
dependencies = [
|
|
32
32
|
"pyyaml>=6.0.3",
|
|
33
|
+
"rich>=13.0.0",
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
[project.urls]
|
|
File without changes
|
{yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/src/YoloDatasetCustomizer/customizer.py
RENAMED
|
@@ -3,6 +3,7 @@ import os
|
|
|
3
3
|
import shutil
|
|
4
4
|
|
|
5
5
|
import yaml
|
|
6
|
+
from rich.progress import BarColumn, MofNCompleteColumn, Progress, TaskProgressColumn, TextColumn
|
|
6
7
|
|
|
7
8
|
from .labels import LabelFile
|
|
8
9
|
from .paths import _SPLIT_NAMES, _get_unique_path, _normalize_path
|
|
@@ -86,6 +87,16 @@ class YoloDatasetCustomizer:
|
|
|
86
87
|
new_class_indices = self.__generate_new_class_indices(class_names)
|
|
87
88
|
print(f"INFO: New class indices: {new_class_indices}")
|
|
88
89
|
|
|
90
|
+
# Always create every split folder so the folder structure matches what data.yaml references,
|
|
91
|
+
# even when no source dataset contributed images to a split.
|
|
92
|
+
for split_name in _SPLIT_NAMES:
|
|
93
|
+
os.makedirs(os.path.join(new_dataset_path, split_name, "labels"), exist_ok=True)
|
|
94
|
+
os.makedirs(os.path.join(new_dataset_path, split_name, "images"), exist_ok=True)
|
|
95
|
+
|
|
96
|
+
# Plan first, so every stage knows how many files it is going to copy
|
|
97
|
+
planned_files: dict[str, list[tuple[LabelFile, dict[str, list[str]], dict[str, str]]]] = {
|
|
98
|
+
split_name: [] for split_name in _SPLIT_NAMES
|
|
99
|
+
}
|
|
89
100
|
for data_set in self.__data_sets:
|
|
90
101
|
common_class_names = set(data_set.get_class_names()) & class_names
|
|
91
102
|
if not common_class_names:
|
|
@@ -112,46 +123,46 @@ class YoloDatasetCustomizer:
|
|
|
112
123
|
print(f"WARNING: Path '{missing_path}' does not exist! Skipping it.")
|
|
113
124
|
continue
|
|
114
125
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
os.
|
|
118
|
-
|
|
126
|
+
# Index images by exact stem so 'car.*' can't pick up e.g. 'car.v2.jpg'
|
|
127
|
+
images_by_stem: dict[str, list[str]] = {}
|
|
128
|
+
for image_name in os.listdir(old_images_path):
|
|
129
|
+
stem = os.path.splitext(image_name)[0]
|
|
130
|
+
images_by_stem.setdefault(stem, []).append(os.path.join(old_images_path, image_name))
|
|
119
131
|
|
|
120
132
|
for original_label_file_path in glob.glob(os.path.join(old_labels_path, "*.txt")):
|
|
121
133
|
label_file = LabelFile(original_label_file_path)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
data_file_writer = YoloDataFileWriter(new_dataset_path, list(new_class_indices.keys()))
|
|
134
|
+
if label_file.get_class_indices() & set(old_to_new_index_match):
|
|
135
|
+
planned_files[split_name].append((label_file, images_by_stem, old_to_new_index_match))
|
|
136
|
+
|
|
137
|
+
# One bar per stage (white, filled green), all visible from the start
|
|
138
|
+
with Progress(
|
|
139
|
+
TextColumn("{task.description}"),
|
|
140
|
+
BarColumn(style="white", complete_style="green", finished_style="green"),
|
|
141
|
+
TaskProgressColumn(),
|
|
142
|
+
MofNCompleteColumn(),
|
|
143
|
+
) as progress:
|
|
144
|
+
stage_tasks = {
|
|
145
|
+
split_name: progress.add_task(f"[{stage}/{len(_SPLIT_NAMES)}] {split_name:<5}", total=len(planned_files[split_name]))
|
|
146
|
+
for stage, split_name in enumerate(_SPLIT_NAMES, start=1)
|
|
147
|
+
}
|
|
148
|
+
for split_name in _SPLIT_NAMES:
|
|
149
|
+
new_labels_path = os.path.join(new_dataset_path, split_name, "labels")
|
|
150
|
+
new_images_path = os.path.join(new_dataset_path, split_name, "images")
|
|
151
|
+
for label_file, images_by_stem, old_to_new_index_match in planned_files[split_name]:
|
|
152
|
+
if not self.__copy_label_and_images(
|
|
153
|
+
label_file, images_by_stem, old_to_new_index_match, new_labels_path, new_images_path, ignore_img_formats
|
|
154
|
+
):
|
|
155
|
+
return False
|
|
156
|
+
progress.advance(stage_tasks[split_name])
|
|
157
|
+
|
|
158
|
+
# Pass the split folder names explicitly so data.yaml points at the folders created above
|
|
159
|
+
data_file_writer = YoloDataFileWriter(
|
|
160
|
+
new_dataset_path,
|
|
161
|
+
list(new_class_indices.keys()),
|
|
162
|
+
train_split="train",
|
|
163
|
+
val_split="val",
|
|
164
|
+
test_split="test",
|
|
165
|
+
)
|
|
155
166
|
try:
|
|
156
167
|
data_file_writer.write()
|
|
157
168
|
except Exception as e:
|
|
@@ -161,6 +172,44 @@ class YoloDatasetCustomizer:
|
|
|
161
172
|
|
|
162
173
|
return True
|
|
163
174
|
|
|
175
|
+
def __copy_label_and_images(
|
|
176
|
+
self,
|
|
177
|
+
label_file: LabelFile,
|
|
178
|
+
images_by_stem: dict[str, list[str]],
|
|
179
|
+
old_to_new_index_match: dict[str, str],
|
|
180
|
+
new_labels_path: str,
|
|
181
|
+
new_images_path: str,
|
|
182
|
+
ignore_img_formats: bool,
|
|
183
|
+
) -> bool:
|
|
184
|
+
"""Copy one label file with remapped indices plus its images. Returns False on error."""
|
|
185
|
+
new_label_file_path = label_file.copy_by_class_indices(old_to_new_index_match, new_labels_path)
|
|
186
|
+
if not new_label_file_path:
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
file_name_no_ext = os.path.splitext(os.path.basename(label_file.file_path))[0]
|
|
190
|
+
for img in images_by_stem.get(file_name_no_ext, []):
|
|
191
|
+
_, ext = os.path.splitext(img)
|
|
192
|
+
|
|
193
|
+
if not ignore_img_formats and ext.lower() not in self.SUPPORTED_IMAGE_FORMATS:
|
|
194
|
+
print(
|
|
195
|
+
f"\nERROR: Image format not supported by yolo: {img}\n"
|
|
196
|
+
f"Supported formats are: {self.SUPPORTED_IMAGE_FORMATS}"
|
|
197
|
+
)
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
new_image_name, _ = os.path.splitext(os.path.basename(new_label_file_path))
|
|
201
|
+
destination = os.path.join(new_images_path, new_image_name + ext)
|
|
202
|
+
try:
|
|
203
|
+
shutil.copyfile(img, destination)
|
|
204
|
+
except OSError as e:
|
|
205
|
+
print(
|
|
206
|
+
f"\nERROR: Image could not be copied:\n"
|
|
207
|
+
f"Source: {img}\nDestination: {destination}\n"
|
|
208
|
+
f"Exception was: {e}"
|
|
209
|
+
)
|
|
210
|
+
return False
|
|
211
|
+
return True
|
|
212
|
+
|
|
164
213
|
def __generate_new_class_indices(self, class_names: set[str]) -> dict[str, str]:
|
|
165
214
|
"""Generate a fresh label index mapping for the selected class names."""
|
|
166
215
|
ret: dict[str, str] = {}
|
{yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/src/YoloDatasetCustomizer/labels.py
RENAMED
|
File without changes
|
{yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/src/YoloDatasetCustomizer/paths.py
RENAMED
|
File without changes
|
{yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/src/YoloDatasetCustomizer/reader.py
RENAMED
|
File without changes
|
{yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/src/YoloDatasetCustomizer/writer.py
RENAMED
|
File without changes
|
{yolodatasetcustomizer-0.3.0 → yolodatasetcustomizer-0.4.0}/test/YoloDatasetCustomizer_test.py
RENAMED
|
@@ -340,6 +340,27 @@ nc: 2
|
|
|
340
340
|
new_image_path = os.path.join(new_dataset_path, "train", "images", "person_car.jpg")
|
|
341
341
|
self.assertTrue(os.path.exists(new_image_path))
|
|
342
342
|
|
|
343
|
+
def test_merge_keeps_splits_separate_and_yaml_matches_folders(self):
|
|
344
|
+
customizer = YoloDatasetCustomizer([self.good_dataset_dir, self.reordered_names_dataset_dir])
|
|
345
|
+
name = "merged_splits_dataset"
|
|
346
|
+
self.assertTrue(
|
|
347
|
+
customizer.create_new_dataset_for_class_names({"person", "car"}, dst_path=self.workspace_root, data_set_name=name)
|
|
348
|
+
)
|
|
349
|
+
new_dataset_path = os.path.join(self.workspace_root, name)
|
|
350
|
+
with open(os.path.join(new_dataset_path, "data.yaml"), "r", encoding="utf-8") as f:
|
|
351
|
+
new_data = yaml.safe_load(f)
|
|
352
|
+
|
|
353
|
+
for key in ("train", "val", "test"):
|
|
354
|
+
self.assertTrue(os.path.isdir(os.path.join(new_dataset_path, new_data[key])), f"{key} -> {new_data[key]}")
|
|
355
|
+
|
|
356
|
+
# good_dataset: person_car in train, car in valid, person in test
|
|
357
|
+
def stems(split: str) -> set[str]:
|
|
358
|
+
return {os.path.splitext(n)[0].split("_")[0] for n in os.listdir(os.path.join(new_dataset_path, split, "labels"))}
|
|
359
|
+
|
|
360
|
+
self.assertIn("person", stems("test"))
|
|
361
|
+
self.assertNotIn("car", stems("test"))
|
|
362
|
+
self.assertNotIn("person", stems("val"))
|
|
363
|
+
|
|
343
364
|
def test_yolo_dataset_customizer_returns_false_for_empty_class_names(self):
|
|
344
365
|
customizer = YoloDatasetCustomizer([self.good_dataset_dir])
|
|
345
366
|
self.assertFalse(customizer.create_new_dataset_for_class_names(set(), dst_path=self.workspace_root, data_set_name="empty_selection"))
|
|
@@ -372,7 +372,7 @@ name = "exceptiongroup"
|
|
|
372
372
|
version = "1.3.1"
|
|
373
373
|
source = { registry = "https://pypi.org/simple" }
|
|
374
374
|
dependencies = [
|
|
375
|
-
{ name = "typing-extensions"
|
|
375
|
+
{ name = "typing-extensions" },
|
|
376
376
|
]
|
|
377
377
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
|
378
378
|
wheels = [
|
|
@@ -405,7 +405,7 @@ name = "importlib-metadata"
|
|
|
405
405
|
version = "9.0.0"
|
|
406
406
|
source = { registry = "https://pypi.org/simple" }
|
|
407
407
|
dependencies = [
|
|
408
|
-
{ name = "zipp"
|
|
408
|
+
{ name = "zipp" },
|
|
409
409
|
]
|
|
410
410
|
sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" }
|
|
411
411
|
wheels = [
|
|
@@ -1078,10 +1078,11 @@ wheels = [
|
|
|
1078
1078
|
|
|
1079
1079
|
[[package]]
|
|
1080
1080
|
name = "yolodatasetcustomizer"
|
|
1081
|
-
version = "0.
|
|
1081
|
+
version = "0.4.0"
|
|
1082
1082
|
source = { editable = "." }
|
|
1083
1083
|
dependencies = [
|
|
1084
1084
|
{ name = "pyyaml" },
|
|
1085
|
+
{ name = "rich" },
|
|
1085
1086
|
]
|
|
1086
1087
|
|
|
1087
1088
|
[package.dev-dependencies]
|
|
@@ -1093,7 +1094,10 @@ dev = [
|
|
|
1093
1094
|
]
|
|
1094
1095
|
|
|
1095
1096
|
[package.metadata]
|
|
1096
|
-
requires-dist = [
|
|
1097
|
+
requires-dist = [
|
|
1098
|
+
{ name = "pyyaml", specifier = ">=6.0.3" },
|
|
1099
|
+
{ name = "rich", specifier = ">=13.0.0" },
|
|
1100
|
+
]
|
|
1097
1101
|
|
|
1098
1102
|
[package.metadata.requires-dev]
|
|
1099
1103
|
dev = [
|