agrobot_base/Python/yolov8-seg/split.py

246 lines
7.2 KiB
Python
Raw Normal View History

2026-02-24 13:38:14 +00:00
from pathlib import Path
import random
import shutil
from collections import defaultdict
# =========================
# CONFIG
# =========================
BASE_DIR = Path(r".") # <- altere
IMAGES_IN = BASE_DIR / "images" / "original"
LABELS_IN = BASE_DIR / "labels" / "original"
# percentuais (0..1). Tem que somar 1.0
TRAIN_P = 0.70
VAL_P = 0.30
TEST_P = 0.00 # se não quiser test, deixa 0
SEED = 42
COPY_MODE = True # True=copia, False=move
# extensões de imagem aceitas
IMG_EXTS = [".jpg", ".jpeg", ".png"]
# se você quiser limitar a split por classes específicas, informe aqui (opcional)
# ex: {0, 1} para erva/cana. Se None, pega todas que aparecerem.
ALLOWED_CLASSES = None
# =========================
def ensure_dirs(*dirs: Path):
for d in dirs:
d.mkdir(parents=True, exist_ok=True)
def parse_classes_from_label(txt_path: Path):
"""
Retorna um set de classes presentes no arquivo label YOLO-seg.
Se vazio/sem arquivo -> set() (background).
"""
if not txt_path.exists():
return set()
content = txt_path.read_text(encoding="utf-8", errors="ignore").strip()
if not content:
return set()
classes = set()
for line in content.splitlines():
parts = line.strip().split()
if not parts:
continue
try:
cls = int(float(parts[0]))
classes.add(cls)
except:
continue
return classes
def key_from_classes(classes: set):
"""
Cria chave de estrato por combinação de classes.
background => "BG"
ex: {0} => "C0"
{1} => "C1"
{0,1} => "C0_C1"
"""
if not classes:
return "BG"
return "_".join([f"C{c}" for c in sorted(classes)])
def proportional_counts(n, p_train, p_val, p_test):
"""
Converte n e percentuais em contagens inteiras que somam n.
Faz arredondamento e ajusta no final.
"""
t = int(round(n * p_train))
v = int(round(n * p_val))
s = int(round(n * p_test))
# Ajusta para somar n
total = t + v + s
while total != n:
if total > n:
# tira de quem tem mais
if t >= v and t >= s and t > 0:
t -= 1
elif v >= t and v >= s and v > 0:
v -= 1
elif s > 0:
s -= 1
else:
# adiciona em quem tem menos
if t <= v and t <= s:
t += 1
elif v <= t and v <= s:
v += 1
else:
s += 1
total = t + v + s
return t, v, s
def copy_or_move(src: Path, dst: Path, copy_mode=True):
if not src.exists():
return
if copy_mode:
shutil.copy2(src, dst)
else:
shutil.move(src, dst)
def main():
# valida percentuais
psum = TRAIN_P + VAL_P + TEST_P
if abs(psum - 1.0) > 1e-6:
raise ValueError(f"TRAIN_P+VAL_P+TEST_P precisa somar 1.0. Atual: {psum}")
if not IMAGES_IN.exists():
raise FileNotFoundError(f"Pasta não encontrada: {IMAGES_IN}")
if not LABELS_IN.exists():
raise FileNotFoundError(f"Pasta não encontrada: {LABELS_IN}")
# destinos
images_train = BASE_DIR / "images" / "train"
images_val = BASE_DIR / "images" / "val"
images_test = BASE_DIR / "images" / "test"
labels_train = BASE_DIR / "labels" / "train"
labels_val = BASE_DIR / "labels" / "val"
labels_test = BASE_DIR / "labels" / "test"
ensure_dirs(images_train, images_val, images_test, labels_train, labels_val, labels_test)
# lista imagens (baseadas em IMAGES_IN)
imgs = []
for ext in IMG_EXTS:
imgs.extend(IMAGES_IN.glob(f"*{ext}"))
imgs = sorted(imgs)
if not imgs:
print("[INFO] Nenhuma imagem encontrada em:", IMAGES_IN)
return
# monta itens com (stem, img_path, raw_path, label_path, classes, stratum)
items = []
all_classes = set()
for img_path in imgs:
stem = img_path.stem
raw_path = IMAGES_IN / f"{stem}.raw"
label_path = LABELS_IN / f"{stem}.txt"
classes = parse_classes_from_label(label_path)
if ALLOWED_CLASSES is not None:
classes = set(c for c in classes if c in ALLOWED_CLASSES)
all_classes |= classes
stratum = key_from_classes(classes)
items.append({
"stem": stem,
"img": img_path,
"raw": raw_path if raw_path.exists() else None,
"label": label_path if label_path.exists() else None,
"classes": classes,
"stratum": stratum
})
# agrupa por estrato
groups = defaultdict(list)
for it in items:
groups[it["stratum"]].append(it)
random.seed(SEED)
train_set, val_set, test_set = [], [], []
# split estratificado por estrato
for stratum, group_items in groups.items():
random.shuffle(group_items)
n = len(group_items)
nt, nv, ns = proportional_counts(n, TRAIN_P, VAL_P, TEST_P)
train_set.extend(group_items[:nt])
val_set.extend(group_items[nt:nt+nv])
test_set.extend(group_items[nt+nv:nt+nv+ns])
# embaralha cada split (opcional)
random.shuffle(train_set)
random.shuffle(val_set)
random.shuffle(test_set)
def report(split_name, split_items):
counts = defaultdict(int)
for it in split_items:
if not it["classes"]:
counts["BG"] += 1
else:
for c in it["classes"]:
counts[f"C{c}"] += 1
return counts
# Copia/move arquivos
def place(items_list, img_out: Path, lbl_out: Path):
for it in items_list:
# imagem
copy_or_move(it["img"], img_out / it["img"].name, COPY_MODE)
# raw (se existir)
if it["raw"] is not None:
copy_or_move(it["raw"], img_out / it["raw"].name, COPY_MODE)
# label: se não existir, cria vazio (background)
out_label = lbl_out / f"{it['stem']}.txt"
if it["label"] is not None:
copy_or_move(it["label"], out_label, COPY_MODE)
else:
out_label.write_text("", encoding="utf-8")
place(train_set, images_train, labels_train)
place(val_set, images_val, labels_val)
place(test_set, images_test, labels_test)
# relatório
print("========================================")
print("Split concluído ✅")
print(f"Total imagens: {len(items)}")
print(f"Train: {len(train_set)} | Val: {len(val_set)} | Test: {len(test_set)}")
print("----------------------------------------")
print("Classes detectadas no dataset:", sorted(all_classes) if all_classes else "Nenhuma (só BG)")
print("----------------------------------------")
print("Distribuição (contagem de imagens que contém a classe):")
print("Train:", dict(report("train", train_set)))
print("Val :", dict(report("val", val_set)))
print("Test :", dict(report("test", test_set)))
print("----------------------------------------")
print("Pastas destino:")
print(" -", images_train)
print(" -", images_val)
print(" -", images_test)
print(" -", labels_train)
print(" -", labels_val)
print(" -", labels_test)
print("========================================")
if __name__ == "__main__":
main()