Skip to content

Commit 4112f01

Browse files
committed
Add shuffle=False for time series and flat directory support (closes #44, #47)
Add shuffle parameter (default True) to ratio(), fixed(), kfold(). When False, files are split in sorted order without randomization. Add --no-shuffle CLI flag. Auto-detect flat input directories (no class subdirectories) and split files directly into train/val/test without class nesting. Oversampling is not supported with flat directories.
1 parent 79543a5 commit 4112f01

13 files changed

Lines changed: 227 additions & 6 deletions

File tree

β€ŽREADME.mdβ€Ž

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Split folders with files (e.g. images) into **train**, **validation** and **test** (dataset) folders.
44

5-
The input folder should have the following format:
5+
The input folder should have the following format (with class subdirectories):
66

77
```
88
input/
@@ -16,6 +16,15 @@ input/
1616
...
1717
```
1818

19+
Or a **flat directory** without class subdirectories:
20+
21+
```
22+
input/
23+
file1.jpg
24+
file2.jpg
25+
...
26+
```
27+
1928
In order to give you this:
2029

2130
```
@@ -47,6 +56,7 @@ This should get you started to do some serious deep learning on your data. [Read
4756

4857
- Split files into a training set and a validation set (and optionally a test set).
4958
- Works on any file types.
59+
- Supports both class-based directory structures and flat directories.
5060
- The files get shuffled (can be disabled for time series data).
5161
- A [seed](https://docs.python.org/3/library/random.html#random.seed) makes splits reproducible.
5262
- Allows randomized [oversampling](https://en.wikipedia.org/wiki/Oversampling_and_undersampling_in_data_analysis) for imbalanced datasets.
@@ -113,6 +123,28 @@ splitfolders.ratio("input_folder", output="output",
113123
ratio=(.8, .1, .1), shuffle=False)
114124
```
115125

126+
### Flat directories
127+
128+
If your input folder contains files directly (no class subdirectories), `splitfolders` auto-detects this and splits files into `train/`, `val/`, `test/` without creating class subfolders:
129+
130+
```python
131+
# input_folder/ contains file1.jpg, file2.jpg, ... (no subdirs)
132+
splitfolders.ratio("input_folder", output="output", ratio=(.8, .1, .1))
133+
```
134+
135+
Output:
136+
```
137+
output/
138+
train/
139+
file1.jpg
140+
...
141+
val/
142+
file5.jpg
143+
...
144+
```
145+
146+
> **Note:** Oversampling is not available with flat directories (there are no classes to balance).
147+
116148
### Grouping files
117149

118150
When your data has multiple files per sample (e.g. an image and its annotation), you need to keep them together during the split. There are several ways to do this.

β€Žsplitfolders/split.pyβ€Ž

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
use_tqdm = False
4949

5050

51-
def check_input_format(input):
51+
def check_input_format(input, allow_flat=False):
5252
p_input = Path(input)
5353
if not p_input.exists():
5454
err_msg = f'The provided input folder "{input}" does not exists.'
@@ -60,14 +60,19 @@ def check_input_format(input):
6060
raise ValueError(f'The provided input folder "{input}" is not a directory')
6161

6262
dirs = list_dirs(input)
63-
if len(dirs) == 0:
63+
if len(dirs) == 0 and not allow_flat:
6464
raise ValueError(
6565
f'The input data is not in a right format. Within your folder "{input}"'
6666
" there are no directories. Consult the documentation how to the folder"
6767
" structure should look like."
6868
)
6969

7070

71+
def _is_flat(input):
72+
"""Returns True if the input directory has no subdirectories (flat file layout)."""
73+
return len(list_dirs(input)) == 0
74+
75+
7176
def _get_copy_fn(move):
7277
"""Return a function(src, dst_dir) that copies/moves/symlinks a file into dst_dir."""
7378
if move is True or move == "move":
@@ -114,7 +119,7 @@ def ratio(
114119
if len(ratio) not in (2, 3):
115120
raise ValueError("`ratio` should")
116121

117-
check_input_format(input)
122+
check_input_format(input, allow_flat=(group != "sibling"))
118123
valid_extensions(formats)
119124

120125
if use_tqdm:
@@ -124,6 +129,11 @@ def ratio(
124129
split_sibling_dirs_ratio(
125130
input, output, ratio, seed, prog_bar if use_tqdm else None, move, formats, shuffle,
126131
)
132+
elif _is_flat(input):
133+
split_flat_dir_ratio(
134+
input, output, ratio, seed, prog_bar if use_tqdm else None,
135+
group_prefix, group, move, formats, shuffle,
136+
)
127137
else:
128138
for class_dir in list_dirs(input):
129139
split_class_dir_ratio(
@@ -139,12 +149,17 @@ def fixed(
139149
input, output="output", seed=1337, fixed=(100, 100), oversample=False,
140150
group_prefix=None, group=None, move=False, formats=None, shuffle=True,
141151
):
142-
check_input_format(input)
152+
check_input_format(input, allow_flat=(group != "sibling"))
143153
valid_extensions(formats)
144154

145155
if group == "sibling" and oversample:
146156
raise ValueError("Cannot use `oversample=True` with `group='sibling'` (no classes to balance).")
147157

158+
flat = _is_flat(input) and group != "sibling"
159+
160+
if flat and oversample:
161+
raise ValueError("Cannot use `oversample=True` with a flat input directory (no classes to balance).")
162+
148163
if fixed == "auto":
149164
if not oversample:
150165
raise ValueError(
@@ -180,6 +195,15 @@ def fixed(
180195
prog_bar.close()
181196
return
182197

198+
if flat:
199+
split_flat_dir_fixed(
200+
input, output, fixed, seed, prog_bar if use_tqdm else None,
201+
group_prefix, group, move, formats, shuffle,
202+
)
203+
if use_tqdm:
204+
prog_bar.close()
205+
return
206+
183207
classes_dirs = list_dirs(input)
184208
num_items = []
185209
for class_dir in classes_dirs:
@@ -235,7 +259,7 @@ def kfold(
235259
if k < 2:
236260
raise ValueError("`k` must be 2 or greater.")
237261

238-
check_input_format(input)
262+
check_input_format(input, allow_flat=(group != "sibling"))
239263
valid_extensions(formats)
240264

241265
if use_tqdm:
@@ -245,6 +269,11 @@ def kfold(
245269
split_sibling_dirs_kfold(
246270
input, output, k, seed, prog_bar if use_tqdm else None, move, formats, shuffle,
247271
)
272+
elif _is_flat(input):
273+
split_flat_dir_kfold(
274+
input, output, k, seed, prog_bar if use_tqdm else None,
275+
group_prefix, group, move, formats, shuffle,
276+
)
248277
else:
249278
for class_dir in list_dirs(input):
250279
split_class_dir_kfold(
@@ -384,6 +413,85 @@ def copy_files(files_type, class_dir, output, prog_bar, move):
384413
copy_fn(f, full_path)
385414

386415

416+
def copy_files_flat(files_type, output, prog_bar, move):
417+
"""
418+
Copies files into output/split_name/ without class subdirectories.
419+
"""
420+
copy_fn = _get_copy_fn(move)
421+
422+
for files, folder_type in files_type:
423+
full_path = Path(output) / folder_type
424+
425+
full_path.mkdir(parents=True, exist_ok=True)
426+
for f in files:
427+
if prog_bar is not None:
428+
prog_bar.update()
429+
if isinstance(f, tuple):
430+
for x in f:
431+
copy_fn(x, full_path)
432+
else:
433+
copy_fn(f, full_path)
434+
435+
436+
def split_flat_dir_ratio(input_dir, output, ratio, seed, prog_bar, group_prefix, group, move, formats, shuffle=True):
437+
"""Splits a flat directory (no class subdirs)."""
438+
files = setup_files(input_dir, seed, group_prefix, group, formats, shuffle)
439+
440+
split_train_idx = int(ratio[0] * len(files))
441+
split_val_idx = split_train_idx + int(ratio[1] * len(files))
442+
443+
li = split_files(files, split_train_idx, split_val_idx, len(ratio) == 3)
444+
copy_files_flat(li, output, prog_bar, move)
445+
446+
447+
def split_flat_dir_fixed(input_dir, output, fixed, seed, prog_bar, group_prefix, group, move, formats, shuffle=True):
448+
"""Splits a flat directory with fixed counts."""
449+
files = setup_files(input_dir, seed, group_prefix, group, formats, shuffle)
450+
451+
if not len(files) >= sum(fixed):
452+
raise ValueError(
453+
f"Not enough files for the requested fixed split."
454+
f" There are only {len(files)} files but fixed={fixed} requires at least {sum(fixed)}."
455+
)
456+
457+
if len(fixed) <= 2:
458+
split_train_idx = len(files) - sum(fixed)
459+
split_val_idx = split_train_idx + fixed[0]
460+
else:
461+
split_train_idx = fixed[0]
462+
split_val_idx = fixed[0] + fixed[1]
463+
464+
li = split_files(
465+
files,
466+
split_train_idx,
467+
split_val_idx,
468+
len(fixed) >= 2,
469+
None if len(fixed) != 3 else fixed[2],
470+
)
471+
copy_files_flat(li, output, prog_bar, move)
472+
473+
474+
def split_flat_dir_kfold(input_dir, output, k, seed, prog_bar, group_prefix, group, move, formats, shuffle=True):
475+
"""Splits a flat directory into k folds."""
476+
files = setup_files(input_dir, seed, group_prefix, group, formats, shuffle)
477+
478+
fold_size = len(files) // k
479+
remainder = len(files) % k
480+
partitions = []
481+
idx = 0
482+
for i in range(k):
483+
size = fold_size + (1 if i < remainder else 0)
484+
partitions.append(files[idx : idx + size])
485+
idx += size
486+
487+
for i in range(k):
488+
val_files = partitions[i]
489+
train_files = [f for j, part in enumerate(partitions) if j != i for f in part]
490+
fold_output = str(Path(output) / f"fold_{i + 1}")
491+
li = [(train_files, "train"), (val_files, "val")]
492+
copy_files_flat(li, fold_output, prog_bar, move)
493+
494+
387495
def copy_sibling_files(files_type, type_dir_names, output, prog_bar, move):
388496
"""
389497
Copies files in sibling mode: each group is a tuple of files across type dirs.

β€Žtests/imgs_flat/file_01.jpgβ€Ž

Loading

β€Žtests/imgs_flat/file_02.jpgβ€Ž

Loading

β€Žtests/imgs_flat/file_03.jpgβ€Ž

Loading

β€Žtests/imgs_flat/file_04.jpgβ€Ž

Loading

β€Žtests/imgs_flat/file_05.jpgβ€Ž

Loading

β€Žtests/imgs_flat/file_06.jpgβ€Ž

Loading

β€Žtests/imgs_flat/file_07.jpgβ€Ž

Loading

β€Žtests/imgs_flat/file_08.jpgβ€Ž

Loading

0 commit comments

Comments
Β (0)