Download object_detection/pre_process.py from Ishaank18/aifh: direct link, hf CLI and curl.
- Browser
- Download file 19.7 kB
-
https://huggingface.co/Ishaank18/aifh/resolve/main/object_detection/pre_process.py
- Command line
-
hf download hf://Ishaank18/aifh/object_detection/pre_process.py
-
curl -L -o pre_process.py https://huggingface.co/Ishaank18/aifh/resolve/main/object_detection/pre_process.py
19.7 kB
| """ | |
| pre_process.py -- Part B, Sections 3.1 and 3.2 | |
| (Data preparation, annotation checks, and exploratory analysis) | |
| 1. Loads Kvasir-SEG images, masks and the bounding-box json, and converts the | |
| annotations into the xyxy + label format Faster R-CNN expects. | |
| 2. Annotation QC: images with no box, boxes outside the image, degenerate | |
| (zero/negative area) boxes, box-vs-mask agreement (IoU between the annotated | |
| box and the tight bounding box of the segmentation mask), and json/image | |
| size disagreements. | |
| 3. Frozen stratified split with SEED = 16 -> splits/kvasir_detection_seed16.csv | |
| 4. EDA: box count per image, box width/height, box-area fraction, aspect ratio, | |
| polyp centre position, image-size distribution, per-split distribution. | |
| 5. At least 8 seed-selected image + mask + box overlays. | |
| Usage | |
| ----- | |
| python pre_process.py | |
| python pre_process.py --data-root ../Dataset/kvasir/Kvasir-SEG --n-examples 8 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| from collections import Counter | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| from common import (DET_SPLIT_CSV, boxplot, METRICS_DIR, OVERLAY_DIR, PLOTS_DIR, SEED, | |
| SEED_TAG, SPLIT_FRACTIONS, _mpl, box_iou_np, find_bbox_json, | |
| find_kvasir_root, load_bboxes, progress, save_json, | |
| seeded_rng, set_seed) | |
| IMG_EXT = {".jpg", ".jpeg", ".png", ".bmp", ".tif"} | |
| # --------------------------------------------------------------------------- | |
| def mask_bbox(mask: np.ndarray) -> list | None: | |
| """Tight xyxy box of the (largest connected component of the) mask.""" | |
| b = mask > 0.5 | |
| if b.sum() == 0: | |
| return None | |
| ys, xs = np.where(b) | |
| return [float(xs.min()), float(ys.min()), float(xs.max() + 1), float(ys.max() + 1)] | |
| def build_index(root: Path, bbox_json: Path) -> tuple[pd.DataFrame, dict]: | |
| from PIL import Image | |
| ann = load_bboxes(bbox_json) | |
| img_dir, mask_dir = root / "images", root / "masks" | |
| images = sorted(p for p in img_dir.iterdir() if p.suffix.lower() in IMG_EXT) | |
| rows, issues = [], { | |
| "images_without_annotation": [], "annotations_without_image": [], | |
| "images_with_zero_boxes": [], "degenerate_boxes": [], | |
| "boxes_out_of_bounds": [], "json_size_mismatch": [], | |
| "missing_masks": [], "empty_masks": [], "low_box_mask_iou": [], | |
| } | |
| ann_keys = set(ann.keys()) | |
| for ip in progress(images, desc="index kvasir", total=len(images)): | |
| stem = ip.stem | |
| with Image.open(ip) as im: | |
| W, H = im.size | |
| mp = mask_dir / f"{stem}{ip.suffix}" | |
| if not mp.exists(): | |
| for e in (".jpg", ".png", ".jpeg"): | |
| if (mask_dir / f"{stem}{e}").exists(): | |
| mp = mask_dir / f"{stem}{e}" | |
| break | |
| has_mask = mp.exists() | |
| if not has_mask: | |
| issues["missing_masks"].append(stem) | |
| a = ann.get(stem) | |
| if a is None: | |
| issues["images_without_annotation"].append(stem) | |
| boxes = [] | |
| else: | |
| ann_keys.discard(stem) | |
| boxes = a["boxes"] | |
| if a.get("width") and (int(a["width"]) != W or int(a["height"]) != H): | |
| issues["json_size_mismatch"].append( | |
| {"image_id": stem, "json": [a.get("width"), a.get("height")], | |
| "file": [W, H]}) | |
| clean = [] | |
| for b in boxes: | |
| x1, y1, x2, y2 = b | |
| x1, x2 = min(x1, x2), max(x1, x2) | |
| y1, y2 = min(y1, y2), max(y1, y2) | |
| if (x2 - x1) < 1 or (y2 - y1) < 1: | |
| issues["degenerate_boxes"].append({"image_id": stem, "box": b}) | |
| continue | |
| if x1 < 0 or y1 < 0 or x2 > W or y2 > H: | |
| issues["boxes_out_of_bounds"].append( | |
| {"image_id": stem, "box": [x1, y1, x2, y2], "image_size": [W, H]}) | |
| x1, y1 = max(0.0, x1), max(0.0, y1) | |
| x2, y2 = min(float(W), x2), min(float(H), y2) | |
| if (x2 - x1) < 1 or (y2 - y1) < 1: | |
| continue | |
| clean.append([x1, y1, x2, y2]) | |
| if not clean: | |
| issues["images_with_zero_boxes"].append(stem) | |
| # --- box vs mask agreement ----------------------------------------- | |
| box_mask_iou, mask_area_frac = np.nan, np.nan | |
| if has_mask: | |
| m = np.asarray(Image.open(mp).convert("L"), dtype=np.float32) / 255.0 | |
| if m.shape[:2] != (H, W): | |
| from PIL import Image as _I | |
| m = np.asarray(_I.open(mp).convert("L").resize((W, H), _I.NEAREST), | |
| dtype=np.float32) / 255.0 | |
| mb = mask_bbox(m) | |
| mask_area_frac = float((m > 0.5).mean()) | |
| if mb is None: | |
| issues["empty_masks"].append(stem) | |
| elif clean: | |
| box_mask_iou = float(box_iou_np(np.array(clean, np.float32), | |
| np.array([mb], np.float32)).max()) | |
| if box_mask_iou < 0.5: | |
| issues["low_box_mask_iou"].append( | |
| {"image_id": stem, "iou": round(box_mask_iou, 3)}) | |
| areas = [(b[2] - b[0]) * (b[3] - b[1]) for b in clean] | |
| rows.append({ | |
| "image_id": stem, | |
| "image_path": str(ip.resolve()), | |
| "mask_path": str(mp.resolve()) if has_mask else "", | |
| "width": W, "height": H, "image_area": W * H, | |
| "n_boxes": len(clean), | |
| "boxes": json.dumps([[round(v, 2) for v in b] for b in clean]), | |
| "total_box_area_frac": float(sum(areas) / (W * H)) if areas else 0.0, | |
| "max_box_area_frac": float(max(areas) / (W * H)) if areas else 0.0, | |
| "mask_area_frac": mask_area_frac, | |
| "box_mask_iou": box_mask_iou, | |
| }) | |
| issues["annotations_without_image"] = sorted(ann_keys) | |
| return pd.DataFrame(rows), issues | |
| # --------------------------------------------------------------------------- | |
| def _collapse_rare(strat: "pd.Series", min_count: int = 2) -> "pd.Series": | |
| """Merge any stratum below `min_count` members into a single 'rare' bucket. | |
| sklearn's stratified split requires at least 2 members per class, so this | |
| must be re-applied to whatever subset is about to be split -- a stratum | |
| that clears the threshold in the full dataset can still drop below it | |
| once a 70/30 (or 15/15) subset is drawn from it.""" | |
| vc = strat.value_counts() | |
| return strat.where(strat.map(vc) >= min_count, "rare") | |
| def make_splits(df: pd.DataFrame, seed: int = SEED, fracs=SPLIT_FRACTIONS) -> pd.DataFrame: | |
| """Stratified on a size bucket derived from the largest box, so that small, | |
| medium and large polyps are represented in every split (Kvasir-SEG has no | |
| class label to stratify on -- polyp size is the meaningful axis).""" | |
| from sklearn.model_selection import train_test_split | |
| d = df.copy().reset_index(drop=True) | |
| d["size_bucket"] = pd.cut(d["max_box_area_frac"], | |
| bins=[-0.001, 0.01, 0.05, 0.20, 1.01], | |
| labels=["tiny", "small", "medium", "large"]).astype(str) | |
| d.loc[d["n_boxes"] == 0, "size_bucket"] = "empty" | |
| d["strat"] = d["size_bucket"] + "_" + (d["n_boxes"] > 1).map({True: "multi", False: "single"}) | |
| # Collapse rare strata for the first split (70 / 30). | |
| strat1 = _collapse_rare(d["strat"], min_count=6) | |
| try: | |
| tr, tmp = train_test_split(d, train_size=fracs[0], random_state=seed, | |
| stratify=strat1, shuffle=True) | |
| except ValueError: | |
| # even the collapsed bucketing has a class with < 2 members overall; | |
| # fall back to an unstratified split rather than crashing. | |
| print("[warn] falling back to an unstratified train split: a size " | |
| "bucket has fewer than 2 images even after collapsing rare " | |
| "buckets together") | |
| tr, tmp = train_test_split(d, train_size=fracs[0], random_state=seed, shuffle=True) | |
| # Re-collapse against the smaller `tmp` subset before the second split -- | |
| # a stratum with 8 members overall can easily have only 1 in a 30% sample. | |
| rel_val = fracs[1] / (fracs[1] + fracs[2]) | |
| strat2 = _collapse_rare(tmp["strat"], min_count=2) | |
| try: | |
| va, te = train_test_split(tmp, train_size=rel_val, random_state=seed, | |
| stratify=strat2, shuffle=True) | |
| except ValueError: | |
| print("[warn] falling back to an unstratified val/test split for the " | |
| "same reason") | |
| va, te = train_test_split(tmp, train_size=rel_val, random_state=seed, shuffle=True) | |
| d["split"] = "" | |
| d.loc[tr.index, "split"] = "train" | |
| d.loc[va.index, "split"] = "val" | |
| d.loc[te.index, "split"] = "test" | |
| return d | |
| # --------------------------------------------------------------------------- | |
| def eda(d: pd.DataFrame, out_dir: Path = PLOTS_DIR) -> dict: | |
| plt = _mpl() | |
| stats: dict = {} | |
| boxes, wfrac, hfrac, afrac, ar, cx, cy, splits = [], [], [], [], [], [], [], [] | |
| for _, r in d.iterrows(): | |
| for b in json.loads(r["boxes"]): | |
| x1, y1, x2, y2 = b | |
| w, h = x2 - x1, y2 - y1 | |
| boxes.append([w, h]) | |
| wfrac.append(w / r["width"]); hfrac.append(h / r["height"]) | |
| afrac.append(w * h / r["image_area"]); ar.append(w / max(1e-6, h)) | |
| cx.append((x1 + x2) / 2 / r["width"]); cy.append((y1 + y2) / 2 / r["height"]) | |
| splits.append(r["split"]) | |
| boxes = np.array(boxes) if boxes else np.zeros((0, 2)) | |
| afrac = np.array(afrac); ar = np.array(ar) | |
| # ---- figure 1: counts and sizes ---- | |
| fig, axes = plt.subplots(2, 3, figsize=(15, 8)) | |
| cnt = d["n_boxes"].value_counts().sort_index() | |
| axes[0, 0].bar(cnt.index.astype(str), cnt.values, color="#4C72B0") | |
| axes[0, 0].set_title("Bounding boxes per image"); axes[0, 0].set_xlabel("n boxes") | |
| for i, v in enumerate(cnt.values): | |
| axes[0, 0].text(i, v, str(v), ha="center", va="bottom", fontsize=8) | |
| sz = d.groupby(["width", "height"]).size().sort_values(ascending=False).head(10) | |
| axes[0, 1].bar([f"{w}x{h}" for w, h in sz.index], sz.values, color="#55A868") | |
| axes[0, 1].set_title("Top-10 image resolutions"); axes[0, 1].set_yscale("log") | |
| plt.setp(axes[0, 1].get_xticklabels(), rotation=40, ha="right", fontsize=7) | |
| axes[0, 2].hist(afrac, bins=50, color="#C44E52") | |
| axes[0, 2].set_title("Box area fraction of image"); axes[0, 2].set_xlabel("area fraction") | |
| axes[0, 2].axvline(0.01, ls="--", c="k", lw=1) | |
| axes[0, 2].axvline(0.05, ls="--", c="k", lw=1) | |
| axes[0, 2].text(0.011, axes[0, 2].get_ylim()[1] * 0.8, " small | medium | large", fontsize=7) | |
| if len(boxes): | |
| axes[1, 0].scatter(boxes[:, 0], boxes[:, 1], s=6, alpha=0.35, color="#8172B2") | |
| axes[1, 0].set_xlabel("box width (px)"); axes[1, 0].set_ylabel("box height (px)") | |
| axes[1, 0].set_title("Box width vs height") | |
| axes[1, 1].hist(np.clip(ar, 0, 4), bins=50, color="#937860") | |
| axes[1, 1].set_title("Aspect ratio (w/h)"); axes[1, 1].axvline(1.0, ls="--", c="k", lw=1) | |
| hb = axes[1, 2].hist2d(cx, cy, bins=20, range=[[0, 1], [0, 1]], cmap="magma") | |
| axes[1, 2].invert_yaxis() | |
| axes[1, 2].set_title("Polyp centre position (normalised)") | |
| fig.colorbar(hb[3], ax=axes[1, 2], fraction=0.046) | |
| fig.suptitle(f"Kvasir-SEG exploratory analysis (seed={SEED})", fontsize=13) | |
| fig.tight_layout() | |
| fig.savefig(out_dir / f"B1_kvasir_eda_{SEED_TAG}.png", dpi=155); plt.close(fig) | |
| # ---- figure 2: split distribution ---- | |
| fig, axes = plt.subplots(1, 2, figsize=(11, 4.2)) | |
| piv = (d.pivot_table(index="size_bucket", columns="split", values="image_id", | |
| aggfunc="count", fill_value=0) | |
| .reindex(columns=["train", "val", "test"], fill_value=0)) | |
| piv.plot(kind="bar", stacked=True, ax=axes[0], colormap="viridis") | |
| axes[0].set_title("Polyp size bucket per split"); axes[0].set_ylabel("images") | |
| plt.setp(axes[0].get_xticklabels(), rotation=0) | |
| sdf = pd.DataFrame({"split": splits, "area_frac": afrac}) | |
| boxplot(axes[1], [sdf[sdf.split == s]["area_frac"].values for s in ["train", "val", "test"]], | |
| ["train", "val", "test"], showfliers=False) | |
| axes[1].set_title("Box area fraction per split") | |
| fig.tight_layout() | |
| fig.savefig(out_dir / f"B2_kvasir_split_distribution_{SEED_TAG}.png", dpi=155); plt.close(fig) | |
| stats["n_images"] = int(len(d)) | |
| stats["n_boxes_total"] = int(d["n_boxes"].sum()) | |
| stats["boxes_per_image"] = {str(k): int(v) for k, v in cnt.items()} | |
| stats["mean_boxes_per_image"] = float(d["n_boxes"].mean()) | |
| stats["n_images_with_zero_boxes"] = int((d["n_boxes"] == 0).sum()) | |
| stats["n_images_multi_box"] = int((d["n_boxes"] > 1).sum()) | |
| stats["image_resolutions_top10"] = {f"{w}x{h}": int(v) for (w, h), v in sz.items()} | |
| stats["n_distinct_resolutions"] = int(d.groupby(["width", "height"]).ngroups) | |
| stats["box_width_px"] = _desc(boxes[:, 0] if len(boxes) else np.zeros(0)) | |
| stats["box_height_px"] = _desc(boxes[:, 1] if len(boxes) else np.zeros(0)) | |
| stats["box_area_fraction"] = _desc(afrac) | |
| stats["aspect_ratio"] = _desc(ar) | |
| stats["centre_x"] = _desc(np.array(cx)); stats["centre_y"] = _desc(np.array(cy)) | |
| stats["size_buckets"] = { | |
| "small_lt_1pct": int((afrac < 0.01).sum()), | |
| "medium_1_to_5pct": int(((afrac >= 0.01) & (afrac < 0.05)).sum()), | |
| "large_ge_5pct": int((afrac >= 0.05).sum())} | |
| stats["split_counts"] = d["split"].value_counts().to_dict() | |
| stats["split_box_counts"] = d.groupby("split")["n_boxes"].sum().to_dict() | |
| stats["size_bucket_by_split"] = piv.to_dict() | |
| stats["foreground_pixel_fraction_mean"] = float(d["mask_area_frac"].dropna().mean()) | |
| stats["class_imbalance_note"] = ( | |
| "Kvasir-SEG is single-class, so 'imbalance' in detection is the " | |
| "foreground/background imbalance: on average only " | |
| f"{100 * float(np.mean(afrac)):.1f}% of an image is inside a polyp box, and the " | |
| "detector scores thousands of mostly-background anchors/proposals per image. " | |
| "This is precisely the imbalance the focal-loss ablation targets.") | |
| return stats | |
| def _desc(a: np.ndarray) -> dict: | |
| if a.size == 0: | |
| return {} | |
| return {"mean": float(a.mean()), "std": float(a.std()), "min": float(a.min()), | |
| "p05": float(np.percentile(a, 5)), "median": float(np.median(a)), | |
| "p95": float(np.percentile(a, 95)), "max": float(a.max())} | |
| # --------------------------------------------------------------------------- | |
| def qualitative(d: pd.DataFrame, n: int = 8, out_dir: Path = OVERLAY_DIR): | |
| """Seed-selected image / mask / box overlays spanning the size buckets.""" | |
| plt = _mpl() | |
| from PIL import Image | |
| rng = seeded_rng(offset=5) | |
| picks = [] | |
| for bucket in ["tiny", "small", "medium", "large"]: | |
| sub = d[d["size_bucket"] == bucket] | |
| if len(sub) == 0: | |
| continue | |
| k = min(max(1, n // 4), len(sub)) | |
| idx = rng.choice(len(sub), size=k, replace=False) | |
| picks.append(sub.iloc[idx]) | |
| sel = pd.concat(picks).head(n) if picks else d.head(n) | |
| if len(sel) < n: | |
| extra = d[~d["image_id"].isin(sel["image_id"])].head(n - len(sel)) | |
| sel = pd.concat([sel, extra]) | |
| ncol = 4 | |
| nrow = int(np.ceil(len(sel) / ncol)) | |
| fig, axes = plt.subplots(nrow, ncol, figsize=(4.0 * ncol, 4.3 * nrow)) | |
| axes = np.atleast_1d(axes).ravel() | |
| for k, (_, r) in enumerate(sel.iterrows()): | |
| img = np.asarray(Image.open(r["image_path"]).convert("RGB"), np.float32) / 255.0 | |
| if r["mask_path"]: | |
| m = np.asarray(Image.open(r["mask_path"]).convert("L") | |
| .resize((r["width"], r["height"]), Image.NEAREST), np.float32) / 255.0 | |
| img[..., 1] = np.where(m > 0.5, 0.55 * img[..., 1] + 0.45, img[..., 1]) | |
| axes[k].imshow(np.clip(img, 0, 1)) | |
| for b in json.loads(r["boxes"]): | |
| x1, y1, x2, y2 = b | |
| axes[k].add_patch(plt.Rectangle((x1, y1), x2 - x1, y2 - y1, fill=False, | |
| edgecolor="red", lw=2.0)) | |
| axes[k].set_title(f"{r['image_id'][:16]}...\n{r['width']}x{r['height']} | " | |
| f"{r['n_boxes']} box | area={r['max_box_area_frac']:.3f} | " | |
| f"{r['split']}", fontsize=7.5) | |
| axes[k].axis("off") | |
| for k in range(len(sel), len(axes)): | |
| axes[k].axis("off") | |
| fig.suptitle(f"Kvasir-SEG: image + mask (green) + box (red), seed={SEED}", fontsize=12) | |
| fig.tight_layout() | |
| out = out_dir / f"B3_kvasir_annotations_{SEED_TAG}.png" | |
| fig.savefig(out, dpi=150); plt.close(fig) | |
| print(f"[saved] {out}") | |
| return sel[["image_id", "split", "width", "height", "n_boxes", "max_box_area_frac"]] | |
| # --------------------------------------------------------------------------- | |
| def main(): | |
| ap = argparse.ArgumentParser(description="Part B data prep + EDA (seed 16)") | |
| ap.add_argument("--data-root", default=None) | |
| ap.add_argument("--bbox-json", default=None) | |
| ap.add_argument("--n-examples", type=int, default=8) | |
| args = ap.parse_args() | |
| set_seed(SEED) | |
| root = find_kvasir_root(args.data_root) | |
| bj = Path(args.bbox_json) if args.bbox_json else find_bbox_json(root) | |
| print(f"[info] kvasir root : {root}") | |
| print(f"[info] bbox json : {bj}") | |
| print(f"[info] seed : {SEED}") | |
| df, issues = build_index(root, bj) | |
| d = make_splits(df, seed=SEED) | |
| d.to_csv(DET_SPLIT_CSV, index=False) | |
| print(f"[saved] {DET_SPLIT_CSV}") | |
| print("\n=== Split counts (seed=%d) ===" % SEED) | |
| tab = d.groupby("split").agg(images=("image_id", "count"), boxes=("n_boxes", "sum")) | |
| tab = tab.reindex(["train", "val", "test"]) | |
| tab["mean_boxes_per_image"] = (tab["boxes"] / tab["images"]).round(3) | |
| print(tab.to_string()) | |
| stats = eda(d) | |
| audit = {"seed": SEED, "dataset_root": str(root), "bbox_json": str(bj), | |
| "n_images": int(len(d)), | |
| "n_images_without_annotation": len(issues["images_without_annotation"]), | |
| "n_annotations_without_image": len(issues["annotations_without_image"]), | |
| "n_images_with_zero_boxes": len(issues["images_with_zero_boxes"]), | |
| "n_degenerate_boxes": len(issues["degenerate_boxes"]), | |
| "n_boxes_out_of_bounds": len(issues["boxes_out_of_bounds"]), | |
| "n_json_size_mismatch": len(issues["json_size_mismatch"]), | |
| "n_missing_masks": len(issues["missing_masks"]), | |
| "n_empty_masks": len(issues["empty_masks"]), | |
| "n_low_box_mask_iou_lt_0.5": len(issues["low_box_mask_iou"]), | |
| "median_box_mask_iou": float(d["box_mask_iou"].median(skipna=True)), | |
| "split_table": tab.to_dict(), | |
| "eda": stats, | |
| "details": {k: (v[:100] if isinstance(v, list) else v) for k, v in issues.items()}} | |
| save_json(audit, METRICS_DIR / f"B_data_audit_{SEED_TAG}.json") | |
| sel = qualitative(d, n=args.n_examples) | |
| sel.to_csv(METRICS_DIR / f"B_seed_selected_examples_{SEED_TAG}.csv", index=False) | |
| print("\n=== ANNOTATION AUDIT ===") | |
| for k, v in audit.items(): | |
| if k.startswith("n_") or k.startswith("median_"): | |
| print(f" {k:34s}: {v}") | |
| print(f"\n box area fraction: mean={stats['box_area_fraction']['mean']:.4f} " | |
| f"median={stats['box_area_fraction']['median']:.4f}") | |
| print(f" size buckets : {stats['size_buckets']}") | |
| print("\n" + stats["class_imbalance_note"]) | |
| if __name__ == "__main__": | |
| main() | |