agrobot_base/Python/yolov8-seg/analyse_dataset.py

198 lines
6.0 KiB
Python
Raw Normal View History

2026-02-24 13:38:14 +00:00
from pathlib import Path
from collections import defaultdict
import argparse
import math
IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp"}
def is_float(s: str) -> bool:
try:
float(s)
return True
except:
return False
def parse_label_line(line: str):
"""
Suporta:
- YOLO det: cls xc yc w h
- YOLO seg: cls x1 y1 x2 y2 ...
Retorna dict com:
cls (int), kind ("det"|"seg"|None), n_points (int|None)
"""
parts = line.strip().split()
if len(parts) < 2:
return None
if not is_float(parts[0]):
return None
cls = int(float(parts[0]))
nums = []
for p in parts[1:]:
if not is_float(p):
return None
nums.append(float(p))
# det clássico: 4 números
if len(nums) == 4:
return {"cls": cls, "kind": "det", "n_points": None}
# seg: pares (x,y)
if len(nums) >= 6 and (len(nums) % 2 == 0):
return {"cls": cls, "kind": "seg", "n_points": len(nums) // 2}
# caso estranho
return {"cls": cls, "kind": "unknown", "n_points": None}
def analyze_split(labels_dir: Path):
stats = {
"images_total": 0,
"images_bg": 0,
"images_with_obj": 0,
"images_per_class": defaultdict(int), # quantas imagens têm a classe
"instances_per_class": defaultdict(int), # quantas instâncias (linhas) por classe
"kind_counts": defaultdict(int), # det/seg/unknown
"poly_points": [], # lista de n_points (para seg)
"weird_lines": 0,
"empty_label_files": 0,
}
if not labels_dir.exists():
return stats
label_files = sorted(labels_dir.glob("*.txt"))
stats["images_total"] = len(label_files)
for lf in label_files:
text = lf.read_text(encoding="utf-8", errors="ignore").strip()
if not text:
stats["images_bg"] += 1
stats["empty_label_files"] += 1
continue
stats["images_with_obj"] += 1
classes_in_image = set()
for line in text.splitlines():
parsed = parse_label_line(line)
if parsed is None:
stats["weird_lines"] += 1
continue
cls = parsed["cls"]
kind = parsed["kind"]
stats["kind_counts"][kind] += 1
classes_in_image.add(cls)
stats["instances_per_class"][cls] += 1
if kind == "seg" and parsed["n_points"] is not None:
stats["poly_points"].append(parsed["n_points"])
for cls in classes_in_image:
stats["images_per_class"][cls] += 1
return stats
def summarize(stats):
out = []
out.append(f"images_total : {stats['images_total']}")
out.append(f"images_bg : {stats['images_bg']} ({pct(stats['images_bg'], stats['images_total'])})")
out.append(f"images_with_obj : {stats['images_with_obj']} ({pct(stats['images_with_obj'], stats['images_total'])})")
out.append(f"empty_label_files: {stats['empty_label_files']}")
out.append(f"weird_lines : {stats['weird_lines']}")
if stats["kind_counts"]:
out.append("label_kinds : " + ", ".join(f"{k}={v}" for k, v in sorted(stats["kind_counts"].items())))
# imagens por classe
if stats["images_per_class"]:
out.append("images_per_class : " + ", ".join(f"c{c}={n}" for c, n in sorted(stats["images_per_class"].items())))
else:
out.append("images_per_class : (nenhuma)")
# instâncias por classe
if stats["instances_per_class"]:
out.append("inst_per_class : " + ", ".join(f"c{c}={n}" for c, n in sorted(stats["instances_per_class"].items())))
else:
out.append("inst_per_class : (nenhuma)")
# polígonos
if stats["poly_points"]:
pts = stats["poly_points"]
out.append(f"seg_poly_points : count={len(pts)} min={min(pts)} mean={sum(pts)/len(pts):.2f} max={max(pts)}")
else:
out.append("seg_poly_points : (n/a)")
return "\n".join(out)
def pct(a, b):
if b == 0:
return "n/a"
return f"{(100.0*a/b):.1f}%"
def merge_stats(a, b):
"""merge b into a"""
a["images_total"] += b["images_total"]
a["images_bg"] += b["images_bg"]
a["images_with_obj"] += b["images_with_obj"]
a["empty_label_files"] += b["empty_label_files"]
a["weird_lines"] += b["weird_lines"]
for k, v in b["images_per_class"].items():
a["images_per_class"][k] += v
for k, v in b["instances_per_class"].items():
a["instances_per_class"][k] += v
for k, v in b["kind_counts"].items():
a["kind_counts"][k] += v
a["poly_points"].extend(b["poly_points"])
return a
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--base", type=str, required=True,
help="Pasta base que contém labels/train labels/val labels/test (ou labels direto)")
args = ap.parse_args()
base = Path(args.base)
labels = base / "labels"
splits = []
# padrão YOLO: labels/train, labels/val, labels/test
if (labels / "train").exists() or (labels / "val").exists() or (labels / "test").exists():
splits = ["train", "val", "test"]
split_dirs = {s: labels / s for s in splits}
else:
# fallback: base/labels direto
split_dirs = {"all": labels}
global_stats = {
"images_total": 0,
"images_bg": 0,
"images_with_obj": 0,
"images_per_class": defaultdict(int),
"instances_per_class": defaultdict(int),
"kind_counts": defaultdict(int),
"poly_points": [],
"weird_lines": 0,
"empty_label_files": 0,
}
for name, d in split_dirs.items():
st = analyze_split(d)
print("===================================")
print(f"SPLIT: {name} ({d})")
print(summarize(st))
print("===================================")
merge_stats(global_stats, st)
print("\n========== GLOBAL ==========")
print(summarize(global_stats))
if __name__ == "__main__":
main()