Spaces:
Sleeping
Sleeping
File size: 5,015 Bytes
2ea8a2d fa0ce5b 2ea8a2d fa0ce5b 2ea8a2d fa0ce5b 2ea8a2d fa0ce5b 2ea8a2d fa0ce5b 2ea8a2d fa0ce5b 2ea8a2d fa0ce5b 2ea8a2d fa0ce5b 2ea8a2d | 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 | # filename: src/data/preprocessor.py
# purpose: YOLO annotation validation, data.yaml generation, dataset structure verification
# version: 1.0
import logging
from pathlib import Path
import yaml
from config import CLASSES, NUM_CLASSES, DATA_PROCESSED_DIR
logger = logging.getLogger(__name__)
def validate_yolo_annotations(labels_dir: Path) -> None:
"""
Assert all YOLO label files have valid normalized values.
Raises AssertionError immediately on the first invalid line.
Call this before every YOLO training run.
"""
labels_dir = Path(labels_dir)
if not labels_dir.exists():
logger.warning(f"Labels dir not found: {labels_dir}")
return
files_checked = 0
lines_checked = 0
for txt_file in labels_dir.glob("*.txt"):
content = txt_file.read_text(encoding="utf-8").strip()
if not content:
continue
for line in content.splitlines():
parts = line.split()
if len(parts) < 5:
continue
class_id = int(parts[0])
x_c = float(parts[1])
y_c = float(parts[2])
w = float(parts[3])
h = float(parts[4])
assert 0 <= class_id < NUM_CLASSES, (
f"class_id {class_id} out of range [0, {NUM_CLASSES}) in {txt_file}"
)
# Use <= 1.0 — bbox_to_yolo clamps to min(1.0,...) so boundary values are valid
assert 0.0 < x_c <= 1.0, f"x_center {x_c} not in (0,1] in {txt_file}"
assert 0.0 < y_c <= 1.0, f"y_center {y_c} not in (0,1] in {txt_file}"
assert 0.0 < w <= 1.0, f"width {w} not in (0,1] in {txt_file}"
assert 0.0 < h <= 1.0, f"height {h} not in (0,1] in {txt_file}"
# Catch clearly wrong values (negative or >1)
assert x_c >= 0 and y_c >= 0, f"Negative center in {txt_file}"
lines_checked += 1
files_checked += 1
print(
f"YOLO annotations valid: {files_checked} files, {lines_checked} annotations in {labels_dir.name}/"
)
def create_yolo_data_yaml(detection_dir: Path) -> Path:
"""
Generate data.yaml with absolute paths — works identically in terminal and Colab.
Always regenerate before YOLO training (Colab paths differ from local paths).
"""
detection_dir = Path(detection_dir)
config = {
"path": str(detection_dir.absolute()),
"train": "images/train",
"val": "images/val",
"nc": NUM_CLASSES,
"names": {i: cls for i, cls in enumerate(CLASSES)},
}
yaml_path = detection_dir / "data.yaml"
with open(yaml_path, "w", encoding="utf-8") as f:
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
# Verify it loads cleanly
with open(yaml_path, encoding="utf-8") as f:
loaded = yaml.safe_load(f)
assert loaded["nc"] == NUM_CLASSES
assert len(loaded["names"]) == NUM_CLASSES
logger.info(f"data.yaml written: {yaml_path}")
print(f"data.yaml OK: nc={loaded['nc']}, path={loaded['path']}")
return yaml_path
def verify_dataset_structure() -> dict:
"""
Count images in every split/class folder and return a summary dict.
Confirms the dataset was built correctly before preprocessing.
"""
cls_dir = DATA_PROCESSED_DIR / "classification"
det_dir = DATA_PROCESSED_DIR / "detection"
result: dict = {
"classification": {},
"detection": {},
"issues": [],
}
# Classification counts
for split in ["train", "val", "test"]:
split_total = 0
per_class: dict[str, int] = {}
for cls in CLASSES:
count = (
len(list((cls_dir / split / cls).glob("*.jpg")))
if (cls_dir / split / cls).exists()
else 0
)
per_class[cls] = count
split_total += count
if count == 0:
result["issues"].append(f"classification/{split}/{cls}: 0 images")
result["classification"][split] = {"total": split_total, "per_class": per_class}
# Detection counts
for split in ["train", "val"]:
imgs = (
len(list((det_dir / "images" / split).glob("*.jpg")))
if (det_dir / "images" / split).exists()
else 0
)
lbls = (
len(list((det_dir / "labels" / split).glob("*.txt")))
if (det_dir / "labels" / split).exists()
else 0
)
result["detection"][split] = {"images": imgs, "labels": lbls}
if imgs != lbls:
result["issues"].append(
f"detection/{split}: images({imgs}) != labels({lbls})"
)
# Expected totals
total_cls = sum(
result["classification"][s]["total"] for s in ["train", "val", "test"]
)
result["classification"]["total"] = total_cls
result["detection"]["total_images"] = sum(
result["detection"][s]["images"] for s in ["train", "val"]
)
return result
|