File size: 9,235 Bytes
81b4246
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
"""
CarDentIQ β€” Dataset Preparation
Developer: Saksham Pathak (github.com/parthmax2)

Supports two modes:
  1. Roboflow download  β†’  python scripts/prepare_data.py --roboflow
  2. Organise local data β†’  python scripts/prepare_data.py --source path/to/raw

Raw folder expected layout (mode 2):
  <source>/
  β”œβ”€β”€ images/   (*.jpg / *.png / *.bmp / *.webp)
  └── labels/   (*.txt  YOLO format β€” class cx cy w h, normalised)

Output layout written to <dest> (default: data/ at project root):
  data/
  β”œβ”€β”€ images/train | val | test
  └── labels/train | val | test
"""

import os
import random
import shutil
import argparse
from collections import Counter
from pathlib import Path

# ── project root (one level above this script) ───────────────────
ROOT = Path(__file__).resolve().parent.parent

TRAIN_RATIO = 0.70
VAL_RATIO   = 0.20
TEST_RATIO  = 0.10
SEED        = 42

IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp"}

CLASS_NAMES = [
    "no_damage", "lost_parts", "torn", "dent",
    "paint_scratch", "hole", "broken_glass", "broken_lamp",
]


# ─────────────────────────────────────────────
# 1. ROBOFLOW DOWNLOAD
# ─────────────────────────────────────────────
def download_from_roboflow(dest: Path) -> None:
    """
    Downloads a car-damage dataset from Roboflow Universe.
    Requires:  pip install roboflow
    Free API key: https://roboflow.com

    Dataset used:
      https://universe.roboflow.com/car-damage-detection/car-damage-detection-dataset
    Swap workspace / project / version to match your own export.
    """
    try:
        from roboflow import Roboflow
    except ImportError:
        raise SystemExit("roboflow not installed. Run: pip install roboflow")

    api_key = os.environ.get("ROBOFLOW_API_KEY", "")
    if not api_key:
        raise SystemExit(
            "Set ROBOFLOW_API_KEY before running with --roboflow.\n"
            "  Windows PowerShell: $env:ROBOFLOW_API_KEY='YOUR_KEY'\n"
            "  Linux / macOS:      export ROBOFLOW_API_KEY=YOUR_KEY"
        )

    rf      = Roboflow(api_key=api_key)
    project = rf.workspace("car-damage-detection").project("car-damage-detection-dataset")
    dataset = project.version(1).download("yolov11", location=str(dest / "_rf_tmp"))

    _remap_roboflow_export(dest / "_rf_tmp", dest)
    shutil.rmtree(dest / "_rf_tmp", ignore_errors=True)
    print("[prepare] Roboflow download complete.")


def _remap_roboflow_export(src: Path, dst: Path) -> None:
    for rf_split, out_split in [("train", "train"), ("valid", "val"), ("test", "test")]:
        for kind in ("images", "labels"):
            src_dir = src / rf_split / kind
            dst_dir = dst / kind / out_split
            dst_dir.mkdir(parents=True, exist_ok=True)
            if src_dir.exists():
                for f in src_dir.iterdir():
                    shutil.copy2(f, dst_dir / f.name)


# ─────────────────────────────────────────────
# 2. LOCAL DATA ORGANISATION
# ─────────────────────────────────────────────
def organise_local(source: Path, dest: Path) -> None:
    img_dir = source / "images"
    lbl_dir = source / "labels"

    if not img_dir.exists():
        raise SystemExit(f"images/ directory not found inside {source}")
    if not lbl_dir.exists():
        raise SystemExit(f"labels/ directory not found inside {source}")

    all_images = sorted(p for p in img_dir.iterdir() if p.suffix.lower() in IMG_EXTS)
    print(f"[prepare] Found {len(all_images)} images in {img_dir}")

    paired, no_label = [], []
    for img in all_images:
        lbl = lbl_dir / (img.stem + ".txt")
        (paired if lbl.exists() else no_label).append((img, lbl) if lbl.exists() else img)

    if no_label:
        print(f"[prepare] WARNING – {len(no_label)} images skipped (no matching label)")

    random.seed(SEED)
    random.shuffle(paired)

    n       = len(paired)
    n_val   = int(n * VAL_RATIO)
    n_test  = int(n * TEST_RATIO)
    n_train = n - n_val - n_test

    splits = {
        "train": paired[:n_train],
        "val":   paired[n_train : n_train + n_val],
        "test":  paired[n_train + n_val :],
    }

    for split, items in splits.items():
        img_out = dest / "images" / split
        lbl_out = dest / "labels" / split
        img_out.mkdir(parents=True, exist_ok=True)
        lbl_out.mkdir(parents=True, exist_ok=True)
        for img, lbl in items:
            shutil.copy2(img, img_out / img.name)
            shutil.copy2(lbl, lbl_out / lbl.name)
        print(f"[prepare] {split:5s} β†’ {len(items):5d} samples")

    print(f"[prepare] Dataset written to: {dest.resolve()}")


# ─────────────────────────────────────────────
# 3. INTEGRITY VALIDATION
# ─────────────────────────────────────────────
def validate_dataset(dest: Path) -> None:
    print("\n[validate] Checking dataset integrity …")
    nc     = len(CLASS_NAMES)
    issues = 0

    for split in ("train", "val", "test"):
        img_dir = dest / "images" / split
        lbl_dir = dest / "labels" / split
        if not img_dir.exists():
            print(f"  MISSING: {img_dir}")
            continue

        img_stems = {p.stem for p in img_dir.iterdir() if p.suffix.lower() in IMG_EXTS}
        lbl_stems = {p.stem for p in lbl_dir.glob("*.txt")}

        for stem in img_stems - lbl_stems:
            print(f"  [{split}] no label for: {stem}")
            issues += 1
        for stem in lbl_stems - img_stems:
            print(f"  [{split}] orphan label: {stem}")
            issues += 1

        for lbl_file in lbl_dir.glob("*.txt"):
            for line in lbl_file.read_text().splitlines():
                parts = line.strip().split()
                if not parts:
                    continue
                cls_id = int(parts[0])
                if not (0 <= cls_id < nc):
                    print(f"  [{split}] bad class {cls_id} in {lbl_file.name}")
                    issues += 1

        print(f"  [{split}] images={len(img_stems)}  labels={len(lbl_stems)}")

    if issues == 0:
        print("[validate] Dataset is clean.")
    else:
        print(f"[validate] {issues} issue(s) found β€” fix before training.")


# ─────────────────────────────────────────────
# 4. CLASS DISTRIBUTION REPORT
# ─────────────────────────────────────────────
def print_stats(dest: Path) -> None:
    print("\n[stats] Class distribution per split:")
    counters = {s: Counter() for s in ("train", "val", "test")}

    for split in ("train", "val", "test"):
        lbl_dir = dest / "labels" / split
        if not lbl_dir.exists():
            continue
        for f in lbl_dir.glob("*.txt"):
            for line in f.read_text().splitlines():
                parts = line.strip().split()
                if parts:
                    counters[split][int(parts[0])] += 1

    header = f"{'Class':<20}" + "".join(f"{s:>8}" for s in ("train", "val", "test"))
    print(header)
    print("-" * len(header))
    for i, name in enumerate(CLASS_NAMES):
        print(f"{name:<20}" + "".join(f"{counters[s][i]:>8}" for s in ("train", "val", "test")))


# ─────────────────────────────────────────────
# MAIN
# ─────────────────────────────────────────────
def main() -> None:
    parser = argparse.ArgumentParser(
        description="Prepare car-damage dataset for YOLO training",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("--roboflow", action="store_true",
                       help="Download from Roboflow (requires ROBOFLOW_API_KEY)")
    group.add_argument("--source", type=Path,
                       help="Path to raw dataset with images/ and labels/ subdirs")
    parser.add_argument("--dest", type=Path, default=ROOT / "data",
                        help="Output directory for the organised dataset")
    parser.add_argument("--no-validate", action="store_true",
                        help="Skip integrity check after organising")
    args = parser.parse_args()

    args.dest.mkdir(parents=True, exist_ok=True)

    if args.roboflow:
        download_from_roboflow(args.dest)
    else:
        organise_local(args.source, args.dest)

    if not args.no_validate:
        validate_dataset(args.dest)
        print_stats(args.dest)


if __name__ == "__main__":
    main()