Spaces:
Sleeping
Sleeping
File size: 10,956 Bytes
9b0ae38 fa0ce5b 9b0ae38 fa0ce5b 1a28594 9b0ae38 fa0ce5b 9b0ae38 fa0ce5b 9b0ae38 fa0ce5b 9b0ae38 fa0ce5b 9b0ae38 fa0ce5b 9b0ae38 1a28594 fa0ce5b 1a28594 fa0ce5b 1a28594 fa0ce5b 1a28594 9b0ae38 fa0ce5b 9b0ae38 1a28594 9b0ae38 fa0ce5b 9b0ae38 1a28594 9b0ae38 fa0ce5b 9b0ae38 fa0ce5b 9b0ae38 fa0ce5b 9b0ae38 fa0ce5b 9b0ae38 fa0ce5b 9b0ae38 | 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 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | # filename: src/data/loader.py
# purpose: HuggingFace COCO streaming, collection, and dataset saving pipeline
# version: 2.0
# SCHEMA NOTES (detection-datasets/coco):
# sample["image"] β PIL.Image (direct, not bytes dict)
# sample["objects"]["bbox"] β list of [x, y, w, h] (top-left + width/height, absolute pixels)
# sample["objects"]["category"] β list of 0-indexed HF category IDs (0-79, NOT COCO IDs 1-90)
# No trust_remote_code needed. No HF token required for public dataset.
# bbox format confirmed via mentor notebook: crop((x, y, x+w, y+h)) produces correct results.
import logging
from pathlib import Path
from PIL import Image
from config import (
CLASSES,
CHECKPOINT_FILE,
HF_CATEGORY_TO_CLASS_IDX,
NUM_CLASSES,
SELECTED_CLASSES,
TRAIN_SPLIT,
VAL_SPLIT,
)
from src.utils.helpers import load_json, save_json
logger = logging.getLogger(__name__)
# ββ COCO mapping verification βββββββββββββββββββββββββββββββββββββββββββββββββ
# Rule 28: call this as the first thing in 01_data_acquisition.py
def verify_coco_mapping() -> None:
"""Verify SELECTED_CLASSES and HF_CATEGORY_TO_CLASS_IDX are consistent."""
assert len(SELECTED_CLASSES) == NUM_CLASSES, (
f"SELECTED_CLASSES has {len(SELECTED_CLASSES)} entries, expected {NUM_CLASSES}"
)
assert len(HF_CATEGORY_TO_CLASS_IDX) == NUM_CLASSES, (
f"HF_CATEGORY_TO_CLASS_IDX has {len(HF_CATEGORY_TO_CLASS_IDX)} entries, expected {NUM_CLASSES}"
)
# Check round-trip: class_name β hf_id β class_idx β class_name
for cls_name, hf_id in SELECTED_CLASSES.items():
assert hf_id in HF_CATEGORY_TO_CLASS_IDX, (
f"HF ID {hf_id} (for '{cls_name}') missing from HF_CATEGORY_TO_CLASS_IDX"
)
class_idx = HF_CATEGORY_TO_CLASS_IDX[hf_id]
assert CLASSES[class_idx] == cls_name, (
f"Round-trip mismatch: '{cls_name}' β hf_id={hf_id} β idx={class_idx} β '{CLASSES[class_idx]}'"
)
# No duplicate HF IDs
hf_ids = list(SELECTED_CLASSES.values())
assert len(hf_ids) == len(set(hf_ids)), (
"Duplicate HF category IDs in SELECTED_CLASSES"
)
logger.info(f"COCO mapping verified: {NUM_CLASSES} classes, no duplicates")
print(f"COCO mapping OK: {NUM_CLASSES} classes, HF 0-indexed IDs, no duplicates")
# ββ Checkpoint / resume βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def load_checkpoint() -> dict[str, int]:
if CHECKPOINT_FILE.exists():
data = load_json(CHECKPOINT_FILE)
total = sum(data.values())
logger.info(f"Resumed from checkpoint: {total} images already collected")
return data
return {cls: 0 for cls in CLASSES}
def save_checkpoint(progress: dict[str, int]) -> None:
CHECKPOINT_FILE.parent.mkdir(parents=True, exist_ok=True)
save_json(progress, CHECKPOINT_FILE)
# ββ YOLO bbox conversion ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# bbox in HF dataset: [x, y, w, h] β top-left corner + width/height (absolute pixels)
# Confirmed by mentor notebook: crop((x, y, x+w, y+h)) produces correct object crops
def bbox_to_yolo(
x: float,
y: float,
w: float,
h: float,
img_w: int,
img_h: int,
class_idx: int,
) -> list[float]:
"""Convert [x, y, w, h] absolute pixels β YOLO [class, x_center, y_center, w, h] normalized."""
x_center = (x + w / 2.0) / img_w
y_center = (y + h / 2.0) / img_h
w_norm = w / img_w
h_norm = h / img_h
# Clamp to valid range
x_center = max(0.0, min(1.0, x_center))
y_center = max(0.0, min(1.0, y_center))
w_norm = max(0.001, min(1.0, w_norm))
h_norm = max(0.001, min(1.0, h_norm))
return [class_idx, x_center, y_center, w_norm, h_norm]
# ββ Overlap utilities βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def compute_overlap_ratio(target_box: list, other_box: list) -> float:
"""
Fraction of target_box area covered by other_box.
Expects [x, y, w, h] format (top-left corner + dimensions).
Use instead of IoU when checking whether a large object (person) dominates
a smaller object (chair). IoU penalises the large union area and under-detects
containment when other_box >> target_box.
Returns float in [0, 1].
"""
assert target_box[2] > 0 and target_box[3] > 0, (
f"target_box width/height must be positive: {target_box}. "
"Ensure bbox format is [x, y, w, h], not [x1, y1, x2, y2]."
)
assert other_box[2] > 0 and other_box[3] > 0, (
f"other_box width/height must be positive: {other_box}. "
"Ensure bbox format is [x, y, w, h], not [x1, y1, x2, y2]."
)
tx1, ty1 = target_box[0], target_box[1]
tx2, ty2 = target_box[0] + target_box[2], target_box[1] + target_box[3]
ox1, oy1 = other_box[0], other_box[1]
ox2, oy2 = other_box[0] + other_box[2], other_box[1] + other_box[3]
inter_x1 = max(tx1, ox1)
inter_y1 = max(ty1, oy1)
inter_x2 = min(tx2, ox2)
inter_y2 = min(ty2, oy2)
inter_area = max(0, inter_x2 - inter_x1) * max(0, inter_y2 - inter_y1)
target_area = target_box[2] * target_box[3]
return inter_area / target_area if target_area > 0 else 0.0
def check_person_overlap(
target_bbox: list,
all_bboxes: list,
all_cat_ids: list,
person_cat_id: int,
threshold: float = 0.50,
) -> bool:
"""
Return True if any person bbox covers > threshold fraction of target_bbox area.
Uses compute_overlap_ratio, NOT IoU. IoU under-detects containment when
person bbox is larger than chair bbox (the common COCO case).
person_cat_id: must match the ID system in all_cat_ids.
In this project: all_cat_ids contains HF 0-indexed IDs (loader.py line 8).
Pass SELECTED_CLASSES["person"] = 0.
threshold: 0.50 default (conservative). Adjust to 0.30 in a second
re-collection run if chair accuracy remains low after first run.
"""
for bbox, cat_id in zip(all_bboxes, all_cat_ids):
if cat_id != person_cat_id:
continue
if compute_overlap_ratio(target_bbox, bbox) > threshold:
return True
return False
# ββ Crop quality gates ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def check_crop_quality(img: Image.Image, x1: int, y1: int, x2: int, y2: int) -> str:
"""Return 'ok' or a rejection reason string.
Gates (all three must pass):
too_small : crop < 48Γ48px in original image (upscale > 4.7Γ β blurry)
too_tiny : bbox covers < 1% of image area (invisible/background object)
bad_aspect : longer side > 5Γ shorter side (sliver or letterbox crop)
"""
crop_w = x2 - x1
crop_h = y2 - y1
if crop_w < 80 or crop_h < 80:
return "too_small"
if (crop_w * crop_h) / (img.width * img.height) < 0.02:
return "too_tiny"
if max(crop_w, crop_h) / min(crop_w, crop_h) > 5.0:
return "bad_aspect"
return "ok"
# ββ Crop and save classification image βββββββββββββββββββββββββββββββββββββββ
def save_classification_crop(
img: Image.Image,
bbox: list[float],
class_name: str,
split: str,
img_idx: int,
classification_dir: Path,
crop_size: int = 224,
) -> bool | str:
"""Crop object bbox, apply quality gates, resize to 224Γ224, save.
Returns:
True β saved successfully
"too_small" β crop < 48px in either dimension
"too_tiny" β bbox covers < 1% of image area
"bad_aspect" β aspect ratio > 5:1
False β unexpected save error
"""
x, y, w, h = bbox
x1 = max(0, int(x))
y1 = max(0, int(y))
x2 = min(img.width, int(x + w))
y2 = min(img.height, int(y + h))
if x2 <= x1 or y2 <= y1:
return "too_small"
quality = check_crop_quality(img, x1, y1, x2, y2)
if quality != "ok":
return quality
try:
crop = img.crop((x1, y1, x2, y2))
crop = crop.resize((crop_size, crop_size), Image.LANCZOS)
out_dir = classification_dir / split / class_name
out_dir.mkdir(parents=True, exist_ok=True)
out_path = out_dir / f"{class_name}_{split}_{img_idx:04d}.jpg"
crop.save(str(out_path), "JPEG", quality=95)
return True
except Exception as e:
logger.warning(f"Crop failed for {class_name} idx={img_idx}: {e}")
return False
# ββ Save detection sample βββββββββββββββββββββββββββββββββββββββββββββββββββββ
def save_detection_sample(
img: Image.Image,
annotations: dict,
img_id: int,
split: str,
detection_dir: Path,
) -> None:
"""Save full image and YOLO-format .txt label for detection task."""
img_w, img_h = img.size
bboxes = annotations["bbox"]
cat_ids = annotations["category"]
# Build YOLO annotation lines for our 25 classes only
yolo_lines = []
for bbox, cat_id in zip(bboxes, cat_ids):
if cat_id not in HF_CATEGORY_TO_CLASS_IDX:
continue
class_idx = HF_CATEGORY_TO_CLASS_IDX[cat_id]
x, y, w, h = bbox
vals = bbox_to_yolo(x, y, w, h, img_w, img_h, class_idx)
yolo_lines.append(
f"{int(vals[0])} {vals[1]:.6f} {vals[2]:.6f} {vals[3]:.6f} {vals[4]:.6f}"
)
if not yolo_lines:
return
img_dir = detection_dir / "images" / split
lbl_dir = detection_dir / "labels" / split
img_dir.mkdir(parents=True, exist_ok=True)
lbl_dir.mkdir(parents=True, exist_ok=True)
img.save(str(img_dir / f"image_{img_id:06d}.jpg"), "JPEG", quality=95)
(lbl_dir / f"image_{img_id:06d}.txt").write_text(
"\n".join(yolo_lines), encoding="utf-8"
)
# ββ Split helper ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get_split(img_idx: int, total: int) -> str:
"""Return 'train', 'val', or 'test' based on img_idx position."""
train_end = int(total * TRAIN_SPLIT)
val_end = train_end + int(total * VAL_SPLIT)
if img_idx < train_end:
return "train"
elif img_idx < val_end:
return "val"
return "test"
|