Upload folder using huggingface_hub
Browse files- .DS_Store +0 -0
- model_files/.DS_Store +0 -0
- model_files/cls.pth +3 -0
- model_files/seg.pt +3 -0
- predictor.py +456 -0
- requirements.txt +6 -0
- validator_local.py +671 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
model_files/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
model_files/cls.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5408c4bc1f5e8934cb284835cec15b99871302720b4742a255600877c6de0990
|
| 3 |
+
size 94397593
|
model_files/seg.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8e7c3b9e132f934f5d8860881a9d8be3f33934f4c6cb492acf7548ccaa555604
|
| 3 |
+
size 54809692
|
predictor.py
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
predictor.py β Student inference file for hidden evaluation.
|
| 3 |
+
|
| 4 |
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 5 |
+
β DO NOT RENAME ANY FUNCTION. β
|
| 6 |
+
β DO NOT CHANGE FUNCTION SIGNATURES. β
|
| 7 |
+
β DO NOT REMOVE ANY FUNCTION. β
|
| 8 |
+
β DO NOT RENAME CLS_CLASS_MAPPING or SEG_CLASS_MAPPING. β
|
| 9 |
+
β You may add helper functions / imports as needed. β
|
| 10 |
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 11 |
+
|
| 12 |
+
Tasks
|
| 13 |
+
-----
|
| 14 |
+
Task 3.1 β Multi-label image-level classification (5 classes).
|
| 15 |
+
Task 3.2 β Object detection + instance segmentation (5 classes).
|
| 16 |
+
|
| 17 |
+
You must implement ALL FOUR functions below.
|
| 18 |
+
|
| 19 |
+
Class Mappings
|
| 20 |
+
--------------
|
| 21 |
+
Fill in the two dictionaries below (CLS_CLASS_MAPPING, SEG_CLASS_MAPPING)
|
| 22 |
+
to map your model's output indices to the canonical category names.
|
| 23 |
+
|
| 24 |
+
The canonical 5 categories (from the DeepFashion2 subset) are:
|
| 25 |
+
short sleeve top, long sleeve top, trousers, shorts, skirt
|
| 26 |
+
|
| 27 |
+
Your indices can be in any order, but the category name strings
|
| 28 |
+
must match exactly (case-insensitive). Background class is optional
|
| 29 |
+
but recommended for detection/segmentation models β the evaluator
|
| 30 |
+
will automatically ignore it.
|
| 31 |
+
|
| 32 |
+
Important: Masks must be at the ORIGINAL image resolution.
|
| 33 |
+
If your model internally resizes images, resize the masks back
|
| 34 |
+
to the input image dimensions before returning them.
|
| 35 |
+
|
| 36 |
+
Model Weights
|
| 37 |
+
-------------
|
| 38 |
+
Place your trained weights inside model_files/ as:
|
| 39 |
+
model_files/cls.pt (or cls.pth) β classification model
|
| 40 |
+
model_files/seg.pt (or seg.pth) β detection + segmentation model
|
| 41 |
+
|
| 42 |
+
Evaluation Metrics
|
| 43 |
+
------------------
|
| 44 |
+
Classification : Macro F1-score + Per-label macro accuracy
|
| 45 |
+
Detection : mAP @ [0.5 : 0.05 : 0.95]
|
| 46 |
+
Segmentation : Per-class mIoU (macro-averaged)
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
from __future__ import annotations
|
| 50 |
+
|
| 51 |
+
import json
|
| 52 |
+
from pathlib import Path
|
| 53 |
+
from typing import Any, Dict, List
|
| 54 |
+
|
| 55 |
+
import numpy as np
|
| 56 |
+
import torch
|
| 57 |
+
import torch.nn as nn
|
| 58 |
+
from PIL import Image
|
| 59 |
+
from torchvision import transforms # β this was missing
|
| 60 |
+
from torchvision.models import resnet50
|
| 61 |
+
from ultralytics import YOLO
|
| 62 |
+
|
| 63 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 64 |
+
# CLASS MAPPINGS β FILL THESE IN
|
| 65 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 66 |
+
|
| 67 |
+
# Classification: maps your model's output index β canonical class name.
|
| 68 |
+
# Must have exactly 5 entries (one per clothing class, NO background).
|
| 69 |
+
# Example:
|
| 70 |
+
# CLS_CLASS_MAPPING = {
|
| 71 |
+
# 0: "short sleeve top",
|
| 72 |
+
# 1: "long sleeve top",
|
| 73 |
+
# 2: "trousers",
|
| 74 |
+
# 3: "shorts",
|
| 75 |
+
# 4: "skirt",
|
| 76 |
+
# }
|
| 77 |
+
CLS_CLASS_MAPPING: Dict[int, str] = {
|
| 78 |
+
0: "short sleeve top",
|
| 79 |
+
1: "trousers",
|
| 80 |
+
2: "shorts",
|
| 81 |
+
3: "long sleeve top",
|
| 82 |
+
4: "skirt",
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
# Detection + Segmentation: maps your model's output index β class name.
|
| 86 |
+
# Include background if your model outputs it (evaluator will ignore it).
|
| 87 |
+
# Example:
|
| 88 |
+
# SEG_CLASS_MAPPING = {
|
| 89 |
+
# 0: "background",
|
| 90 |
+
# 1: "short sleeve top",
|
| 91 |
+
# 2: "long sleeve top",
|
| 92 |
+
# 3: "trousers",
|
| 93 |
+
# 4: "shorts",
|
| 94 |
+
# 5: "skirt",
|
| 95 |
+
# }
|
| 96 |
+
SEG_CLASS_MAPPING: Dict[int, str] = {
|
| 97 |
+
0: "short sleeve top",
|
| 98 |
+
1: "long sleeve top",
|
| 99 |
+
2: "shorts",
|
| 100 |
+
3: "trousers",
|
| 101 |
+
4: "skirt",
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 106 |
+
# Helper utilities (you may modify or add more)
|
| 107 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _find_weights(folder: Path, stem: str) -> Path:
|
| 111 |
+
"""Return the first existing weights file matching stem.pt or stem.pth."""
|
| 112 |
+
for ext in (".pt", ".pth"):
|
| 113 |
+
candidate = folder / "model_files" / (stem + ext)
|
| 114 |
+
if candidate.exists():
|
| 115 |
+
return candidate
|
| 116 |
+
raise FileNotFoundError(f"No weights file found for '{stem}' in {folder / 'model_files'}")
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _load_json(path: Path) -> Dict[str, Any]:
|
| 120 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 121 |
+
return json.load(f)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# ImageNet normalization β same as training
|
| 125 |
+
_IMAGENET_MEAN = [0.485, 0.456, 0.406]
|
| 126 |
+
_IMAGENET_STD = [0.229, 0.224, 0.225]
|
| 127 |
+
|
| 128 |
+
_cls_transform = transforms.Compose(
|
| 129 |
+
[
|
| 130 |
+
transforms.Resize(256),
|
| 131 |
+
transforms.CenterCrop(224),
|
| 132 |
+
transforms.ToTensor(),
|
| 133 |
+
transforms.Normalize(mean=_IMAGENET_MEAN, std=_IMAGENET_STD),
|
| 134 |
+
]
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
CLS_THRESHOLD = 0.5 # same threshold used in training evaluation
|
| 138 |
+
|
| 139 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 140 |
+
# TASK 3.1 β CLASSIFICATION (ResNet-50)
|
| 141 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def load_classification_model(folder: str, device: str) -> Any:
|
| 145 |
+
"""Load ResNet-50 classification model from model_files/cls.pth"""
|
| 146 |
+
folder = Path(folder)
|
| 147 |
+
weights_path = _find_weights(folder, "cls")
|
| 148 |
+
|
| 149 |
+
# Build same architecture as training
|
| 150 |
+
model = resnet50(weights=None)
|
| 151 |
+
model.fc = nn.Linear(2048, 5) # 5 classes, no background
|
| 152 |
+
|
| 153 |
+
checkpoint = torch.load(weights_path, map_location=device, weights_only=False)
|
| 154 |
+
|
| 155 |
+
# Checkpoint was saved as dict with 'model_state_dict' key
|
| 156 |
+
if isinstance(checkpoint, dict) and "model_state_dict" in checkpoint:
|
| 157 |
+
state_dict = checkpoint["model_state_dict"]
|
| 158 |
+
else:
|
| 159 |
+
# fallback: assume raw state dict
|
| 160 |
+
state_dict = checkpoint
|
| 161 |
+
|
| 162 |
+
model.load_state_dict(state_dict)
|
| 163 |
+
model.to(device)
|
| 164 |
+
model.eval()
|
| 165 |
+
|
| 166 |
+
return {"model": model, "device": device}
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def predict_classification(model: Any, images: List[Image.Image]) -> List[Dict]:
|
| 170 |
+
"""
|
| 171 |
+
Multi-label classification with ResNet-50.
|
| 172 |
+
Returns list of {"labels": [0/1, 0/1, 0/1, 0/1, 0/1]} per image.
|
| 173 |
+
Order matches CLS_CLASS_MAPPING: short sleeve top, trousers, shorts, long sleeve top, skirt
|
| 174 |
+
"""
|
| 175 |
+
net = model["model"]
|
| 176 |
+
device = model["device"]
|
| 177 |
+
|
| 178 |
+
results = []
|
| 179 |
+
|
| 180 |
+
# Process in a single batch for efficiency
|
| 181 |
+
batch = torch.stack([_cls_transform(img.convert("RGB")) for img in images]).to(device) # (N, 3, 224, 224)
|
| 182 |
+
|
| 183 |
+
with torch.no_grad():
|
| 184 |
+
logits = net(batch) # (N, 5)
|
| 185 |
+
probs = torch.sigmoid(logits).cpu().numpy() # (N, 5)
|
| 186 |
+
|
| 187 |
+
for prob_row in probs:
|
| 188 |
+
binary_labels = [int(p >= CLS_THRESHOLD) for p in prob_row]
|
| 189 |
+
results.append({"labels": binary_labels})
|
| 190 |
+
|
| 191 |
+
return results
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 195 |
+
# TASK 3.2 β DETECTION + INSTANCE SEGMENTATION (YOLOv8-seg)
|
| 196 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def load_detection_model(folder: str, device: str) -> Any:
|
| 200 |
+
"""Load YOLOv8-seg model from model_files/seg.pt"""
|
| 201 |
+
folder = Path(folder)
|
| 202 |
+
weights_path = _find_weights(folder, "seg")
|
| 203 |
+
|
| 204 |
+
model = YOLO(str(weights_path))
|
| 205 |
+
|
| 206 |
+
# Move to correct device
|
| 207 |
+
# YOLO uses device string directly: "cpu", "0", "cuda"
|
| 208 |
+
if device == "cuda":
|
| 209 |
+
yolo_device = "0"
|
| 210 |
+
else:
|
| 211 |
+
yolo_device = "cpu"
|
| 212 |
+
|
| 213 |
+
return {"model": model, "device": yolo_device}
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def predict_detection_segmentation(
|
| 217 |
+
model: Any,
|
| 218 |
+
images: List[Image.Image],
|
| 219 |
+
) -> List[Dict]:
|
| 220 |
+
"""
|
| 221 |
+
Detection + instance segmentation with YOLOv8-seg.
|
| 222 |
+
Returns list of {boxes, scores, labels, masks} per image.
|
| 223 |
+
Masks are resized back to original image resolution.
|
| 224 |
+
"""
|
| 225 |
+
net = model["model"]
|
| 226 |
+
yolo_device = model["device"]
|
| 227 |
+
|
| 228 |
+
results = []
|
| 229 |
+
|
| 230 |
+
for img in images:
|
| 231 |
+
img_rgb = img.convert("RGB")
|
| 232 |
+
orig_w, orig_h = img_rgb.size # PIL: (width, height)
|
| 233 |
+
|
| 234 |
+
# Run YOLO inference
|
| 235 |
+
# Pass PIL image directly β ultralytics handles it
|
| 236 |
+
preds = net(
|
| 237 |
+
img_rgb,
|
| 238 |
+
imgsz=640,
|
| 239 |
+
conf=0.25,
|
| 240 |
+
iou=0.45,
|
| 241 |
+
device=yolo_device,
|
| 242 |
+
verbose=False,
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
result = preds[0] # single image result
|
| 246 |
+
|
| 247 |
+
# ββ Empty detection case ββββββββββββββββββββββββββββββ
|
| 248 |
+
if result.boxes is None or len(result.boxes) == 0:
|
| 249 |
+
results.append(
|
| 250 |
+
{
|
| 251 |
+
"boxes": [],
|
| 252 |
+
"scores": [],
|
| 253 |
+
"labels": [],
|
| 254 |
+
"masks": [],
|
| 255 |
+
}
|
| 256 |
+
)
|
| 257 |
+
continue
|
| 258 |
+
|
| 259 |
+
# ββ Extract boxes, scores, labels βββββββββββββββββββββ
|
| 260 |
+
boxes_xyxy = result.boxes.xyxy.cpu().numpy() # (N, 4) float32
|
| 261 |
+
scores = result.boxes.conf.cpu().numpy() # (N,) float32
|
| 262 |
+
labels = result.boxes.cls.cpu().numpy().astype(int) # (N,)
|
| 263 |
+
|
| 264 |
+
# Clip boxes to image bounds just in case
|
| 265 |
+
boxes_xyxy[:, 0] = np.clip(boxes_xyxy[:, 0], 0, orig_w)
|
| 266 |
+
boxes_xyxy[:, 1] = np.clip(boxes_xyxy[:, 1], 0, orig_h)
|
| 267 |
+
boxes_xyxy[:, 2] = np.clip(boxes_xyxy[:, 2], 0, orig_w)
|
| 268 |
+
boxes_xyxy[:, 3] = np.clip(boxes_xyxy[:, 3], 0, orig_h)
|
| 269 |
+
|
| 270 |
+
boxes_list = boxes_xyxy.tolist() # list of [x1,y1,x2,y2]
|
| 271 |
+
scores_list = scores.tolist() # list of float
|
| 272 |
+
labels_list = labels.tolist() # list of int
|
| 273 |
+
|
| 274 |
+
# ββ Extract + resize masks ββββββββββββββββββββββββββββ
|
| 275 |
+
masks_list = []
|
| 276 |
+
|
| 277 |
+
if result.masks is not None:
|
| 278 |
+
# result.masks.data shape: (N, mask_h, mask_w) β float32 in [0,1]
|
| 279 |
+
raw_masks = result.masks.data.cpu().numpy() # (N, mH, mW)
|
| 280 |
+
|
| 281 |
+
for raw_mask in raw_masks:
|
| 282 |
+
# Threshold to binary
|
| 283 |
+
binary = (raw_mask > 0.5).astype(np.uint8)
|
| 284 |
+
|
| 285 |
+
# Resize to original image size if needed
|
| 286 |
+
if binary.shape != (orig_h, orig_w):
|
| 287 |
+
mask_pil = Image.fromarray(binary * 255, mode="L")
|
| 288 |
+
mask_pil = mask_pil.resize((orig_w, orig_h), Image.NEAREST)
|
| 289 |
+
binary = (np.array(mask_pil) > 127).astype(np.uint8)
|
| 290 |
+
|
| 291 |
+
masks_list.append(binary)
|
| 292 |
+
else:
|
| 293 |
+
# YOLO returned boxes but no masks (shouldn't happen with seg model)
|
| 294 |
+
# Fill with empty masks
|
| 295 |
+
for _ in range(len(boxes_list)):
|
| 296 |
+
masks_list.append(np.zeros((orig_h, orig_w), dtype=np.uint8))
|
| 297 |
+
|
| 298 |
+
results.append(
|
| 299 |
+
{
|
| 300 |
+
"boxes": boxes_list,
|
| 301 |
+
"scores": scores_list,
|
| 302 |
+
"labels": labels_list,
|
| 303 |
+
"masks": masks_list,
|
| 304 |
+
}
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
return results
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
# # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 311 |
+
# # TASK 3.1 β CLASSIFICATION
|
| 312 |
+
# # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 313 |
+
|
| 314 |
+
# def load_classification_model(folder: str, device: str) -> Any:
|
| 315 |
+
# """
|
| 316 |
+
# Load your trained classification model.
|
| 317 |
+
|
| 318 |
+
# Parameters
|
| 319 |
+
# ----------
|
| 320 |
+
# folder : str
|
| 321 |
+
# Absolute path to your submission folder (the one containing
|
| 322 |
+
# this predictor.py, model_files/, class_mapping_cls.json, etc.).
|
| 323 |
+
# device : str
|
| 324 |
+
# PyTorch device string, e.g. "cuda", "mps", or "cpu".
|
| 325 |
+
|
| 326 |
+
# Returns
|
| 327 |
+
# -------
|
| 328 |
+
# model : Any
|
| 329 |
+
# Whatever object your predict_classification function needs.
|
| 330 |
+
# This is passed directly as the first argument to
|
| 331 |
+
# predict_classification().
|
| 332 |
+
|
| 333 |
+
# Notes
|
| 334 |
+
# -----
|
| 335 |
+
# - Load weights from <folder>/model_files/cls.pt (or .pth).
|
| 336 |
+
# - Use CLS_CLASS_MAPPING defined above to map output indices.
|
| 337 |
+
# - The returned object can be a dict, a nn.Module, or anything
|
| 338 |
+
# your prediction function expects.
|
| 339 |
+
# """
|
| 340 |
+
# raise NotImplementedError("TODO: implement load_classification_model")
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
# def predict_classification(model: Any, images: List[Image.Image]) -> List[Dict]:
|
| 344 |
+
# """
|
| 345 |
+
# Run multi-label classification on a list of images.
|
| 346 |
+
|
| 347 |
+
# Parameters
|
| 348 |
+
# ----------
|
| 349 |
+
# model : Any
|
| 350 |
+
# The object returned by load_classification_model().
|
| 351 |
+
# images : list of PIL.Image.Image
|
| 352 |
+
# A list of RGB PIL images.
|
| 353 |
+
|
| 354 |
+
# Returns
|
| 355 |
+
# -------
|
| 356 |
+
# results : list of dict
|
| 357 |
+
# One dict per image, with the key "labels":
|
| 358 |
+
|
| 359 |
+
# [
|
| 360 |
+
# {"labels": [int, int, int, int, int]},
|
| 361 |
+
# {"labels": [int, int, int, int, int]},
|
| 362 |
+
# ...
|
| 363 |
+
# ]
|
| 364 |
+
|
| 365 |
+
# Each "labels" list has exactly 5 elements (one per class,
|
| 366 |
+
# in the order defined by your CLS_CLASS_MAPPING dictionary).
|
| 367 |
+
# Each element is 0 or 1.
|
| 368 |
+
|
| 369 |
+
# Example
|
| 370 |
+
# -------
|
| 371 |
+
# >>> results = predict_classification(model, [img1, img2])
|
| 372 |
+
# >>> results[0]
|
| 373 |
+
# {"labels": [1, 0, 0, 1, 0]}
|
| 374 |
+
# """
|
| 375 |
+
# raise NotImplementedError("TODO: implement predict_classification")
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
# # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 379 |
+
# # TASK 3.2 β DETECTION + INSTANCE SEGMENTATION
|
| 380 |
+
# # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 381 |
+
|
| 382 |
+
# def load_detection_model(folder: str, device: str) -> Any:
|
| 383 |
+
# """
|
| 384 |
+
# Load your trained detection + segmentation model.
|
| 385 |
+
|
| 386 |
+
# Parameters
|
| 387 |
+
# ----------
|
| 388 |
+
# folder : str
|
| 389 |
+
# Absolute path to your submission folder.
|
| 390 |
+
# device : str
|
| 391 |
+
# PyTorch device string, e.g. "cuda", "mps", or "cpu".
|
| 392 |
+
|
| 393 |
+
# Returns
|
| 394 |
+
# -------
|
| 395 |
+
# model : Any
|
| 396 |
+
# Whatever object your predict_detection_segmentation function
|
| 397 |
+
# needs. Passed directly as the first argument.
|
| 398 |
+
|
| 399 |
+
# Notes
|
| 400 |
+
# -----
|
| 401 |
+
# - Load weights from <folder>/model_files/seg.pt (or .pth).
|
| 402 |
+
# - Use SEG_CLASS_MAPPING defined above to map output indices.
|
| 403 |
+
# """
|
| 404 |
+
# raise NotImplementedError("TODO: implement load_detection_model")
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
# def predict_detection_segmentation(
|
| 408 |
+
# model: Any,
|
| 409 |
+
# images: List[Image.Image],
|
| 410 |
+
# ) -> List[Dict]:
|
| 411 |
+
# """
|
| 412 |
+
# Run detection + instance segmentation on a list of images.
|
| 413 |
+
|
| 414 |
+
# Parameters
|
| 415 |
+
# ----------
|
| 416 |
+
# model : Any
|
| 417 |
+
# The object returned by load_detection_model().
|
| 418 |
+
# images : list of PIL.Image.Image
|
| 419 |
+
# A list of RGB PIL images.
|
| 420 |
+
|
| 421 |
+
# Returns
|
| 422 |
+
# -------
|
| 423 |
+
# results : list of dict
|
| 424 |
+
# One dict per image with keys "boxes", "scores", "labels", "masks":
|
| 425 |
+
|
| 426 |
+
# [
|
| 427 |
+
# {
|
| 428 |
+
# "boxes": [[x1, y1, x2, y2], ...], # list of float coords
|
| 429 |
+
# "scores": [float, ...], # confidence in [0, 1]
|
| 430 |
+
# "labels": [int, ...], # class indices (see mapping)
|
| 431 |
+
# "masks": [np.ndarray, ...] # binary masks, HΓW, uint8
|
| 432 |
+
# },
|
| 433 |
+
# ...
|
| 434 |
+
# ]
|
| 435 |
+
|
| 436 |
+
# Output contract
|
| 437 |
+
# ---------------
|
| 438 |
+
# - boxes / scores / labels / masks must all have the same length
|
| 439 |
+
# (= number of detected instances in that image).
|
| 440 |
+
# - Each box is [x1, y1, x2, y2] with x1 < x2, y1 < y2.
|
| 441 |
+
# - Coordinates must be within image bounds (0 β€ x β€ width, 0 β€ y β€ height).
|
| 442 |
+
# - Each score is a float in [0, 1].
|
| 443 |
+
# - Each label is an int index matching your SEG_CLASS_MAPPING.
|
| 444 |
+
# - Each mask is a 2-D numpy array of shape (image_height, image_width)
|
| 445 |
+
# with dtype uint8, containing only 0 and 1.
|
| 446 |
+
# - If no objects are detected, return empty lists for all keys.
|
| 447 |
+
|
| 448 |
+
# Example
|
| 449 |
+
# -------
|
| 450 |
+
# >>> results = predict_detection_segmentation(model, [img])
|
| 451 |
+
# >>> results[0]["boxes"]
|
| 452 |
+
# [[100.0, 40.0, 300.0, 420.0], [50.0, 200.0, 250.0, 600.0]]
|
| 453 |
+
# >>> results[0]["masks"][0].shape
|
| 454 |
+
# (height, width)
|
| 455 |
+
# """
|
| 456 |
+
# raise NotImplementedError("TODO: implement predict_detection_segmentation")
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch
|
| 2 |
+
torchvision
|
| 3 |
+
ultralytics
|
| 4 |
+
numpy
|
| 5 |
+
Pillow
|
| 6 |
+
scikit-learn
|
validator_local.py
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
validator_local.py β Robust student self-check script.
|
| 3 |
+
|
| 4 |
+
Place this inside your VRMP1_<roll_number>/ folder and run:
|
| 5 |
+
|
| 6 |
+
python validator_local.py
|
| 7 |
+
|
| 8 |
+
This validates with 100% coverage:
|
| 9 |
+
β All required files and weights exist
|
| 10 |
+
β predictor.py imports without errors
|
| 11 |
+
β CLS_CLASS_MAPPING and SEG_CLASS_MAPPING are correctly filled
|
| 12 |
+
β All 4 functions are implemented (not NotImplementedError)
|
| 13 |
+
β Models load successfully
|
| 14 |
+
β Classification output format is correct on a REAL image
|
| 15 |
+
β Detection + segmentation output format is correct on a REAL image
|
| 16 |
+
β Mask dimensions match the original image
|
| 17 |
+
β All value ranges and types are correct
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import importlib.util
|
| 23 |
+
import json
|
| 24 |
+
import sys
|
| 25 |
+
import traceback
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
from typing import Any, Dict, List
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
from PIL import Image, ImageDraw
|
| 31 |
+
from sklearn.metrics import f1_score
|
| 32 |
+
|
| 33 |
+
# DeepFashion2 category_id β name (dataset constant)
|
| 34 |
+
DEEPFASHION_CATID_TO_NAME: Dict[int, str] = {
|
| 35 |
+
1: "short sleeve top",
|
| 36 |
+
2: "long sleeve top",
|
| 37 |
+
3: "short sleeve outwear",
|
| 38 |
+
4: "long sleeve outwear",
|
| 39 |
+
5: "vest",
|
| 40 |
+
6: "sling",
|
| 41 |
+
7: "shorts",
|
| 42 |
+
8: "trousers",
|
| 43 |
+
9: "skirt",
|
| 44 |
+
10: "short sleeve dress",
|
| 45 |
+
11: "long sleeve dress",
|
| 46 |
+
12: "vest dress",
|
| 47 |
+
13: "sling dress",
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
# Populated at runtime from the student's CLS_CLASS_MAPPING
|
| 51 |
+
CANONICAL_CLASSES: set = set()
|
| 52 |
+
CANONICAL_CLASSES_LIST: list = []
|
| 53 |
+
NUM_CLASSES: int = 0
|
| 54 |
+
CANONICAL_NAME_TO_IDX: Dict[str, int] = {}
|
| 55 |
+
CATEGORY_ID_TO_CANONICAL: Dict[int, int] = {}
|
| 56 |
+
|
| 57 |
+
# βββ Counters βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
+
_pass_count = 0
|
| 59 |
+
_fail_count = 0
|
| 60 |
+
_warn_count = 0
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _pass(msg: str):
|
| 64 |
+
global _pass_count
|
| 65 |
+
_pass_count += 1
|
| 66 |
+
print(f" [PASS] {msg}")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _fail(msg: str):
|
| 70 |
+
global _fail_count
|
| 71 |
+
_fail_count += 1
|
| 72 |
+
print(f" [FAIL] {msg}")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _warn(msg: str):
|
| 76 |
+
global _warn_count
|
| 77 |
+
_warn_count += 1
|
| 78 |
+
print(f" [WARN] {msg}")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _check(condition: bool, pass_msg: str, fail_msg: str) -> bool:
|
| 82 |
+
if condition:
|
| 83 |
+
_pass(pass_msg)
|
| 84 |
+
return True
|
| 85 |
+
else:
|
| 86 |
+
_fail(fail_msg)
|
| 87 |
+
return False
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# βββ Locate the real test image ββββββββββββββββββββββββββββββββββ
|
| 91 |
+
|
| 92 |
+
def _find_test_image(folder: Path) -> Path | None:
|
| 93 |
+
"""Walk up from the student folder to find hidden_dataset/images/000001.jpg."""
|
| 94 |
+
search = folder.parent # workspace root (one level up from VRMP1_*)
|
| 95 |
+
candidate = search / "hidden_dataset" / "images" / "000001.jpg"
|
| 96 |
+
if candidate.exists():
|
| 97 |
+
return candidate
|
| 98 |
+
# Try any image in hidden_dataset
|
| 99 |
+
hd = search / "hidden_dataset" / "images"
|
| 100 |
+
if hd.is_dir():
|
| 101 |
+
imgs = sorted(hd.glob("*.jpg"))
|
| 102 |
+
if imgs:
|
| 103 |
+
return imgs[0]
|
| 104 |
+
return None
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _find_test_annotation(img_path: Path) -> Path | None:
|
| 108 |
+
"""Find the annotation JSON matching the test image."""
|
| 109 |
+
annos_dir = img_path.parent.parent / "annos"
|
| 110 |
+
anno_path = annos_dir / (img_path.stem + ".json")
|
| 111 |
+
return anno_path if anno_path.exists() else None
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
# βββ GT loading & metric helpers βββββββββββββββββββββββββββββββββ
|
| 115 |
+
|
| 116 |
+
def load_annotation(anno_path: Path) -> List[Dict[str, Any]]:
|
| 117 |
+
"""Parse annotation JSON β list of GT items (only canonical classes)."""
|
| 118 |
+
with open(anno_path, "r", encoding="utf-8") as f:
|
| 119 |
+
data = json.load(f)
|
| 120 |
+
items = []
|
| 121 |
+
for val in data.values():
|
| 122 |
+
if not isinstance(val, dict) or "bounding_box" not in val:
|
| 123 |
+
continue
|
| 124 |
+
cat_id = val["category_id"]
|
| 125 |
+
if cat_id not in CATEGORY_ID_TO_CANONICAL:
|
| 126 |
+
continue
|
| 127 |
+
items.append({
|
| 128 |
+
"box": val["bounding_box"],
|
| 129 |
+
"segmentation": val["segmentation"],
|
| 130 |
+
"category_id": cat_id,
|
| 131 |
+
"category_name": val.get("category_name", ""),
|
| 132 |
+
"canonical_idx": CATEGORY_ID_TO_CANONICAL[cat_id],
|
| 133 |
+
})
|
| 134 |
+
return items
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def rasterize_polygons(segmentation: list, width: int, height: int) -> np.ndarray:
|
| 138 |
+
"""Render polygon coordinate lists into a binary (H, W) mask."""
|
| 139 |
+
canvas = Image.new("L", (width, height), 0)
|
| 140 |
+
draw = ImageDraw.Draw(canvas)
|
| 141 |
+
for poly in segmentation:
|
| 142 |
+
coords = [(poly[i], poly[i + 1]) for i in range(0, len(poly) - 1, 2)]
|
| 143 |
+
if len(coords) >= 3:
|
| 144 |
+
draw.polygon(coords, fill=1)
|
| 145 |
+
return np.array(canvas, dtype=np.uint8)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def build_remap(student_mapping: dict) -> Dict[int, int]:
|
| 149 |
+
"""Map student class index β canonical class index by name matching."""
|
| 150 |
+
remap: Dict[int, int] = {}
|
| 151 |
+
for s_idx, s_name in student_mapping.items():
|
| 152 |
+
name = str(s_name).strip().lower()
|
| 153 |
+
if name in CANONICAL_NAME_TO_IDX:
|
| 154 |
+
remap[int(s_idx)] = CANONICAL_NAME_TO_IDX[name]
|
| 155 |
+
return remap
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
# βββ Class mapping validation ββββββββββββββββββββββββββββββββββββ
|
| 159 |
+
|
| 160 |
+
def validate_class_mapping(mapping, label: str, allow_background: bool) -> bool:
|
| 161 |
+
if not _check(isinstance(mapping, dict),
|
| 162 |
+
f"{label} is a dict.",
|
| 163 |
+
f"{label} must be a dict, got {type(mapping).__name__}."):
|
| 164 |
+
return False
|
| 165 |
+
|
| 166 |
+
if not _check(len(mapping) > 0,
|
| 167 |
+
f"{label} is non-empty ({len(mapping)} entries).",
|
| 168 |
+
f"{label} is empty β you must fill in your class mapping!"):
|
| 169 |
+
return False
|
| 170 |
+
|
| 171 |
+
# Check keys are ints
|
| 172 |
+
all_int_keys = all(isinstance(k, int) for k in mapping.keys())
|
| 173 |
+
_check(all_int_keys,
|
| 174 |
+
f"{label} keys are all integers.",
|
| 175 |
+
f"{label} keys must be integers. Got: {[type(k).__name__ for k in mapping.keys()]}")
|
| 176 |
+
|
| 177 |
+
# Check values are strings
|
| 178 |
+
all_str_vals = all(isinstance(v, str) for v in mapping.values())
|
| 179 |
+
_check(all_str_vals,
|
| 180 |
+
f"{label} values are all strings.",
|
| 181 |
+
f"{label} values must be strings.")
|
| 182 |
+
|
| 183 |
+
# Check canonical class coverage
|
| 184 |
+
clothing_names = set()
|
| 185 |
+
for k, v in mapping.items():
|
| 186 |
+
name = str(v).strip().lower()
|
| 187 |
+
if name == "background":
|
| 188 |
+
if not allow_background:
|
| 189 |
+
_warn(f"{label}: index {k} is 'background' β not expected in CLS_CLASS_MAPPING.")
|
| 190 |
+
continue
|
| 191 |
+
clothing_names.add(name)
|
| 192 |
+
|
| 193 |
+
missing = CANONICAL_CLASSES - clothing_names
|
| 194 |
+
extra = clothing_names - CANONICAL_CLASSES
|
| 195 |
+
if extra:
|
| 196 |
+
_warn(f"{label}: unrecognized classes (will be ignored by evaluator): {extra}")
|
| 197 |
+
|
| 198 |
+
if not _check(len(missing) == 0,
|
| 199 |
+
f"{label} covers all 5 canonical classes.",
|
| 200 |
+
f"{label} missing canonical classes: {missing}"):
|
| 201 |
+
return False
|
| 202 |
+
|
| 203 |
+
if not allow_background:
|
| 204 |
+
expected = 5
|
| 205 |
+
_check(len(mapping) == expected,
|
| 206 |
+
f"{label} has exactly {expected} entries (no background).",
|
| 207 |
+
f"{label} should have {expected} entries for classification, got {len(mapping)}.")
|
| 208 |
+
return True
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
# βββ Classification output validation ββββββββββββββββββββββββββββ
|
| 212 |
+
|
| 213 |
+
def validate_cls_output(outputs: list, num_images: int, num_classes: int) -> bool:
|
| 214 |
+
ok = True
|
| 215 |
+
if not _check(isinstance(outputs, list),
|
| 216 |
+
"Classification returns a list.",
|
| 217 |
+
f"Classification must return a list, got {type(outputs).__name__}."):
|
| 218 |
+
return False
|
| 219 |
+
|
| 220 |
+
if not _check(len(outputs) == num_images,
|
| 221 |
+
f"Classification returned {num_images} result(s) for {num_images} image(s).",
|
| 222 |
+
f"Expected {num_images} results, got {len(outputs)}."):
|
| 223 |
+
return False
|
| 224 |
+
|
| 225 |
+
for idx, out in enumerate(outputs):
|
| 226 |
+
prefix = f"cls_output[{idx}]"
|
| 227 |
+
if not _check(isinstance(out, dict),
|
| 228 |
+
f"{prefix} is a dict.",
|
| 229 |
+
f"{prefix} must be a dict, got {type(out).__name__}."):
|
| 230 |
+
ok = False
|
| 231 |
+
continue
|
| 232 |
+
|
| 233 |
+
if not _check("labels" in out,
|
| 234 |
+
f"{prefix} has 'labels' key.",
|
| 235 |
+
f"{prefix} missing 'labels' key. Keys found: {list(out.keys())}"):
|
| 236 |
+
ok = False
|
| 237 |
+
continue
|
| 238 |
+
|
| 239 |
+
labels = out["labels"]
|
| 240 |
+
if not _check(isinstance(labels, list),
|
| 241 |
+
f"{prefix}['labels'] is a list.",
|
| 242 |
+
f"{prefix}['labels'] must be a list, got {type(labels).__name__}."):
|
| 243 |
+
ok = False
|
| 244 |
+
continue
|
| 245 |
+
|
| 246 |
+
if not _check(len(labels) == num_classes,
|
| 247 |
+
f"{prefix}['labels'] has length {num_classes}.",
|
| 248 |
+
f"{prefix}['labels'] must have length {num_classes}, got {len(labels)}."):
|
| 249 |
+
ok = False
|
| 250 |
+
continue
|
| 251 |
+
|
| 252 |
+
all_valid = True
|
| 253 |
+
for i, l in enumerate(labels):
|
| 254 |
+
if not isinstance(l, int):
|
| 255 |
+
_fail(f"{prefix}['labels'][{i}] must be int, got {type(l).__name__}.")
|
| 256 |
+
ok = False
|
| 257 |
+
all_valid = False
|
| 258 |
+
elif l not in (0, 1):
|
| 259 |
+
_fail(f"{prefix}['labels'][{i}] must be 0 or 1, got {l}.")
|
| 260 |
+
ok = False
|
| 261 |
+
all_valid = False
|
| 262 |
+
|
| 263 |
+
if all_valid:
|
| 264 |
+
_pass(f"{prefix}: all label values are valid (binary 0/1). Output: {labels}")
|
| 265 |
+
return ok
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
# βββ Detection output validation ββββββββββββββββββββββββββββββββ
|
| 269 |
+
|
| 270 |
+
def validate_det_output(outputs: list, num_images: int, img_sizes: list, max_label: int) -> bool:
|
| 271 |
+
ok = True
|
| 272 |
+
if not _check(isinstance(outputs, list),
|
| 273 |
+
"Detection returns a list.",
|
| 274 |
+
f"Detection must return a list, got {type(outputs).__name__}."):
|
| 275 |
+
return False
|
| 276 |
+
|
| 277 |
+
if not _check(len(outputs) == num_images,
|
| 278 |
+
f"Detection returned {num_images} result(s) for {num_images} image(s).",
|
| 279 |
+
f"Expected {num_images} results, got {len(outputs)}."):
|
| 280 |
+
return False
|
| 281 |
+
|
| 282 |
+
for idx, out in enumerate(outputs):
|
| 283 |
+
w, h = img_sizes[idx]
|
| 284 |
+
prefix = f"det_output[{idx}]"
|
| 285 |
+
|
| 286 |
+
if not _check(isinstance(out, dict),
|
| 287 |
+
f"{prefix} is a dict.",
|
| 288 |
+
f"{prefix} must be a dict."):
|
| 289 |
+
ok = False
|
| 290 |
+
continue
|
| 291 |
+
|
| 292 |
+
required_keys = {"boxes", "scores", "labels", "masks"}
|
| 293 |
+
present_keys = set(out.keys())
|
| 294 |
+
missing_keys = required_keys - present_keys
|
| 295 |
+
if not _check(len(missing_keys) == 0,
|
| 296 |
+
f"{prefix} has all required keys (boxes, scores, labels, masks).",
|
| 297 |
+
f"{prefix} missing keys: {missing_keys}"):
|
| 298 |
+
ok = False
|
| 299 |
+
continue
|
| 300 |
+
|
| 301 |
+
n = len(out["boxes"])
|
| 302 |
+
lengths_ok = (len(out["scores"]) == n and len(out["labels"]) == n
|
| 303 |
+
and len(out["masks"]) == n)
|
| 304 |
+
if not _check(lengths_ok,
|
| 305 |
+
f"{prefix}: all arrays have same length ({n} detections).",
|
| 306 |
+
f"{prefix}: length mismatch β boxes={n}, scores={len(out['scores'])}, "
|
| 307 |
+
f"labels={len(out['labels'])}, masks={len(out['masks'])}."):
|
| 308 |
+
ok = False
|
| 309 |
+
continue
|
| 310 |
+
|
| 311 |
+
if n == 0:
|
| 312 |
+
_warn(f"{prefix}: zero detections β model may be undertrained or image has no objects.")
|
| 313 |
+
continue
|
| 314 |
+
|
| 315 |
+
# Boxes
|
| 316 |
+
boxes_valid = True
|
| 317 |
+
for i, box in enumerate(out["boxes"]):
|
| 318 |
+
if not (isinstance(box, (list, tuple)) and len(box) == 4):
|
| 319 |
+
_fail(f"{prefix}/boxes[{i}] must be [x1,y1,x2,y2].")
|
| 320 |
+
ok = False
|
| 321 |
+
boxes_valid = False
|
| 322 |
+
else:
|
| 323 |
+
x1, y1, x2, y2 = [float(c) for c in box]
|
| 324 |
+
if not (x1 < x2 and y1 < y2):
|
| 325 |
+
_fail(f"{prefix}/boxes[{i}]: need x1<x2 and y1<y2, got [{x1:.1f},{y1:.1f},{x2:.1f},{y2:.1f}].")
|
| 326 |
+
ok = False
|
| 327 |
+
boxes_valid = False
|
| 328 |
+
if boxes_valid:
|
| 329 |
+
_pass(f"{prefix}: all {n} boxes have valid [x1,y1,x2,y2] format.")
|
| 330 |
+
|
| 331 |
+
# Scores
|
| 332 |
+
scores_valid = True
|
| 333 |
+
for i, s in enumerate(out["scores"]):
|
| 334 |
+
if not isinstance(s, (int, float)):
|
| 335 |
+
_fail(f"{prefix}/scores[{i}] must be numeric, got {type(s).__name__}.")
|
| 336 |
+
ok = False
|
| 337 |
+
scores_valid = False
|
| 338 |
+
elif not (0.0 <= float(s) <= 1.0):
|
| 339 |
+
_fail(f"{prefix}/scores[{i}] must be in [0,1], got {s}.")
|
| 340 |
+
ok = False
|
| 341 |
+
scores_valid = False
|
| 342 |
+
if scores_valid:
|
| 343 |
+
_pass(f"{prefix}: all {n} scores in [0, 1].")
|
| 344 |
+
|
| 345 |
+
# Labels
|
| 346 |
+
labels_valid = True
|
| 347 |
+
for i, l in enumerate(out["labels"]):
|
| 348 |
+
if not isinstance(l, int):
|
| 349 |
+
_fail(f"{prefix}/labels[{i}] must be int, got {type(l).__name__}.")
|
| 350 |
+
ok = False
|
| 351 |
+
labels_valid = False
|
| 352 |
+
elif not (0 <= l <= max_label):
|
| 353 |
+
_fail(f"{prefix}/labels[{i}] must be in [0, {max_label}], got {l}.")
|
| 354 |
+
ok = False
|
| 355 |
+
labels_valid = False
|
| 356 |
+
if labels_valid:
|
| 357 |
+
_pass(f"{prefix}: all {n} labels are valid integers in [0, {max_label}].")
|
| 358 |
+
|
| 359 |
+
# Masks
|
| 360 |
+
masks_valid = True
|
| 361 |
+
for i, mask in enumerate(out["masks"]):
|
| 362 |
+
arr = np.asarray(mask)
|
| 363 |
+
if arr.ndim != 2:
|
| 364 |
+
_fail(f"{prefix}/masks[{i}] must be 2D, got {arr.ndim}D shape={arr.shape}.")
|
| 365 |
+
ok = False
|
| 366 |
+
masks_valid = False
|
| 367 |
+
continue
|
| 368 |
+
if arr.shape != (h, w):
|
| 369 |
+
_fail(f"{prefix}/masks[{i}] shape {arr.shape} != image size ({h}, {w}). "
|
| 370 |
+
"You must resize masks back to the original image resolution!")
|
| 371 |
+
ok = False
|
| 372 |
+
masks_valid = False
|
| 373 |
+
uniq = set(np.unique(arr).tolist())
|
| 374 |
+
if not uniq.issubset({0, 1}):
|
| 375 |
+
_fail(f"{prefix}/masks[{i}] must be binary (0/1), got values {uniq}.")
|
| 376 |
+
ok = False
|
| 377 |
+
masks_valid = False
|
| 378 |
+
if masks_valid and n > 0:
|
| 379 |
+
_pass(f"{prefix}: all {n} masks are binary and match image size ({h}x{w}).")
|
| 380 |
+
|
| 381 |
+
return ok
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 385 |
+
# Main
|
| 386 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 387 |
+
|
| 388 |
+
def main():
|
| 389 |
+
folder = Path(__file__).resolve().parent
|
| 390 |
+
print("=" * 60)
|
| 391 |
+
print(f" VALIDATOR β {folder.name}")
|
| 392 |
+
print("=" * 60)
|
| 393 |
+
|
| 394 |
+
# βββ 1. Required files ββββββββββββββββββββββββββββββββββββββββ
|
| 395 |
+
print("\n[1/6] Checking required files ...")
|
| 396 |
+
abort = False
|
| 397 |
+
if not _check((folder / "predictor.py").exists(),
|
| 398 |
+
"predictor.py found.",
|
| 399 |
+
"predictor.py NOT found!"):
|
| 400 |
+
abort = True
|
| 401 |
+
|
| 402 |
+
has_cls_weights = (folder / "model_files" / "cls.pt").exists() or \
|
| 403 |
+
(folder / "model_files" / "cls.pth").exists()
|
| 404 |
+
has_seg_weights = (folder / "model_files" / "seg.pt").exists() or \
|
| 405 |
+
(folder / "model_files" / "seg.pth").exists()
|
| 406 |
+
|
| 407 |
+
if has_cls_weights:
|
| 408 |
+
_pass("model_files/cls.pt(h) found.")
|
| 409 |
+
else:
|
| 410 |
+
_warn("model_files/cls.pt(h) not found β OK if classification reuses the seg model.")
|
| 411 |
+
|
| 412 |
+
if not _check(has_seg_weights,
|
| 413 |
+
"model_files/seg.pt(h) found.",
|
| 414 |
+
"model_files/seg.pt(h) NOT found!"):
|
| 415 |
+
abort = True
|
| 416 |
+
|
| 417 |
+
if abort:
|
| 418 |
+
print("\n[ABORT] Fix missing files before continuing.")
|
| 419 |
+
sys.exit(1)
|
| 420 |
+
|
| 421 |
+
# βββ 2. Find test image + annotation ββββββββββββββββββββββββββ
|
| 422 |
+
print("\n[2/6] Locating test image ...")
|
| 423 |
+
test_img_path = _find_test_image(folder)
|
| 424 |
+
if test_img_path is None:
|
| 425 |
+
_fail("Cannot find hidden_dataset/images/000001.jpg β "
|
| 426 |
+
"make sure hidden_dataset/ is in the parent directory.")
|
| 427 |
+
sys.exit(1)
|
| 428 |
+
else:
|
| 429 |
+
test_img = Image.open(test_img_path).convert("RGB")
|
| 430 |
+
img_w, img_h = test_img.size
|
| 431 |
+
_pass(f"Using real test image: {test_img_path.name} ({img_w}x{img_h})")
|
| 432 |
+
|
| 433 |
+
anno_path = _find_test_annotation(test_img_path)
|
| 434 |
+
|
| 435 |
+
# βββ 3. Import predictor βββββββββββββββββββββββββββββββββββββ
|
| 436 |
+
print("\n[3/6] Importing predictor.py ...")
|
| 437 |
+
try:
|
| 438 |
+
spec = importlib.util.spec_from_file_location("predictor", folder / "predictor.py")
|
| 439 |
+
predictor = importlib.util.module_from_spec(spec)
|
| 440 |
+
spec.loader.exec_module(predictor)
|
| 441 |
+
_pass("predictor.py imported successfully.")
|
| 442 |
+
except Exception as e:
|
| 443 |
+
_fail(f"predictor.py import error: {e}")
|
| 444 |
+
traceback.print_exc()
|
| 445 |
+
sys.exit(1)
|
| 446 |
+
|
| 447 |
+
# βββ 4. Validate class mappings + function existence βββββββββ
|
| 448 |
+
print("\n[4/6] Validating class mappings and function signatures ...")
|
| 449 |
+
|
| 450 |
+
has_cls_map = hasattr(predictor, "CLS_CLASS_MAPPING")
|
| 451 |
+
has_seg_map = hasattr(predictor, "SEG_CLASS_MAPPING")
|
| 452 |
+
|
| 453 |
+
if not _check(has_cls_map,
|
| 454 |
+
"CLS_CLASS_MAPPING attribute exists.",
|
| 455 |
+
"CLS_CLASS_MAPPING not found in predictor.py!"):
|
| 456 |
+
sys.exit(1)
|
| 457 |
+
if not _check(has_seg_map,
|
| 458 |
+
"SEG_CLASS_MAPPING attribute exists.",
|
| 459 |
+
"SEG_CLASS_MAPPING not found in predictor.py!"):
|
| 460 |
+
sys.exit(1)
|
| 461 |
+
|
| 462 |
+
# Build canonical class structures from CLS_CLASS_MAPPING
|
| 463 |
+
global CANONICAL_CLASSES, CANONICAL_CLASSES_LIST, NUM_CLASSES
|
| 464 |
+
global CANONICAL_NAME_TO_IDX, CATEGORY_ID_TO_CANONICAL
|
| 465 |
+
|
| 466 |
+
cls_names = []
|
| 467 |
+
for idx in sorted(predictor.CLS_CLASS_MAPPING.keys()):
|
| 468 |
+
name = str(predictor.CLS_CLASS_MAPPING[idx]).strip().lower()
|
| 469 |
+
if name != "background":
|
| 470 |
+
cls_names.append(name)
|
| 471 |
+
CANONICAL_CLASSES_LIST = cls_names
|
| 472 |
+
CANONICAL_CLASSES = set(cls_names)
|
| 473 |
+
NUM_CLASSES = len(cls_names)
|
| 474 |
+
CANONICAL_NAME_TO_IDX = {name: i for i, name in enumerate(cls_names)}
|
| 475 |
+
|
| 476 |
+
CATEGORY_ID_TO_CANONICAL = {}
|
| 477 |
+
for cat_id, cat_name in DEEPFASHION_CATID_TO_NAME.items():
|
| 478 |
+
if cat_name in CANONICAL_NAME_TO_IDX:
|
| 479 |
+
CATEGORY_ID_TO_CANONICAL[cat_id] = CANONICAL_NAME_TO_IDX[cat_name]
|
| 480 |
+
|
| 481 |
+
_pass(f"Derived {NUM_CLASSES} canonical classes from CLS_CLASS_MAPPING: {cls_names}")
|
| 482 |
+
|
| 483 |
+
validate_class_mapping(predictor.CLS_CLASS_MAPPING, "CLS_CLASS_MAPPING", allow_background=False)
|
| 484 |
+
validate_class_mapping(predictor.SEG_CLASS_MAPPING, "SEG_CLASS_MAPPING", allow_background=True)
|
| 485 |
+
|
| 486 |
+
# Load GT annotation now that canonical mapping is ready
|
| 487 |
+
gt_items: List[Dict[str, Any]] = []
|
| 488 |
+
if anno_path is not None:
|
| 489 |
+
gt_items = load_annotation(anno_path)
|
| 490 |
+
_pass(f"Loaded GT annotation: {anno_path.name} ({len(gt_items)} objects)")
|
| 491 |
+
else:
|
| 492 |
+
_warn("No annotation found β metrics (F1, mIoU) will be skipped.")
|
| 493 |
+
|
| 494 |
+
max_label = max(int(k) for k in predictor.SEG_CLASS_MAPPING.keys()) if predictor.SEG_CLASS_MAPPING else 5
|
| 495 |
+
num_cls_classes = len(predictor.CLS_CLASS_MAPPING)
|
| 496 |
+
|
| 497 |
+
# Check all 4 required functions exist and are callable
|
| 498 |
+
required_fns = [
|
| 499 |
+
"load_classification_model",
|
| 500 |
+
"predict_classification",
|
| 501 |
+
"load_detection_model",
|
| 502 |
+
"predict_detection_segmentation",
|
| 503 |
+
]
|
| 504 |
+
for fn_name in required_fns:
|
| 505 |
+
if not _check(hasattr(predictor, fn_name) and callable(getattr(predictor, fn_name)),
|
| 506 |
+
f"{fn_name}() exists and is callable.",
|
| 507 |
+
f"{fn_name}() NOT found or not callable!"):
|
| 508 |
+
sys.exit(1)
|
| 509 |
+
|
| 510 |
+
# βββ 5. Test classification pipeline ββββββββββοΏ½οΏ½οΏ½ββββββββββββββ
|
| 511 |
+
print(f"\n[5/6] Testing classification on real image ({img_w}x{img_h}) ...")
|
| 512 |
+
device = "cpu"
|
| 513 |
+
|
| 514 |
+
# 5a. load_classification_model β must NOT raise NotImplementedError
|
| 515 |
+
cls_model = None
|
| 516 |
+
try:
|
| 517 |
+
cls_model = predictor.load_classification_model(str(folder), device)
|
| 518 |
+
_pass("load_classification_model() returned successfully.")
|
| 519 |
+
except NotImplementedError:
|
| 520 |
+
_fail("load_classification_model() raises NotImplementedError β "
|
| 521 |
+
"you MUST implement this function!")
|
| 522 |
+
except Exception as e:
|
| 523 |
+
_fail(f"load_classification_model() raised: {e}")
|
| 524 |
+
traceback.print_exc()
|
| 525 |
+
|
| 526 |
+
# 5b. predict_classification β must NOT raise NotImplementedError
|
| 527 |
+
cls_out = None
|
| 528 |
+
if cls_model is not None:
|
| 529 |
+
try:
|
| 530 |
+
cls_out = predictor.predict_classification(cls_model, [test_img])
|
| 531 |
+
_pass("predict_classification() returned successfully.")
|
| 532 |
+
validate_cls_output(cls_out, num_images=1, num_classes=num_cls_classes)
|
| 533 |
+
except NotImplementedError:
|
| 534 |
+
_fail("predict_classification() raises NotImplementedError β "
|
| 535 |
+
"you MUST implement this function!")
|
| 536 |
+
cls_out = None
|
| 537 |
+
except Exception as e:
|
| 538 |
+
_fail(f"predict_classification() raised: {e}")
|
| 539 |
+
traceback.print_exc()
|
| 540 |
+
cls_out = None
|
| 541 |
+
|
| 542 |
+
# 5c. Compute macro F1 if GT annotation is available
|
| 543 |
+
if cls_out is not None and anno_path is not None:
|
| 544 |
+
try:
|
| 545 |
+
remap_cls = build_remap(predictor.CLS_CLASS_MAPPING)
|
| 546 |
+
gt_vec = np.zeros(NUM_CLASSES, dtype=np.int32)
|
| 547 |
+
for item in gt_items:
|
| 548 |
+
gt_vec[item["canonical_idx"]] = 1
|
| 549 |
+
pred_vec = np.zeros(NUM_CLASSES, dtype=np.int32)
|
| 550 |
+
student_labels = cls_out[0]["labels"]
|
| 551 |
+
for s_idx, val in enumerate(student_labels):
|
| 552 |
+
canonical = remap_cls.get(s_idx)
|
| 553 |
+
if canonical is not None:
|
| 554 |
+
pred_vec[canonical] = val
|
| 555 |
+
macro_f1 = float(f1_score(
|
| 556 |
+
gt_vec.reshape(1, -1), pred_vec.reshape(1, -1),
|
| 557 |
+
average="macro", zero_division=0.0,
|
| 558 |
+
))
|
| 559 |
+
print(f"\n ** Classification Macro F1: {macro_f1:.4f} **")
|
| 560 |
+
except Exception as e:
|
| 561 |
+
_warn(f"Could not compute macro F1: {e}")
|
| 562 |
+
|
| 563 |
+
# βββ 6. Test detection + segmentation pipeline βββββββββββββββ
|
| 564 |
+
print(f"\n[6/6] Testing detection + segmentation on real image ({img_w}x{img_h}) ...")
|
| 565 |
+
|
| 566 |
+
# 6a. load_detection_model β must NOT raise NotImplementedError
|
| 567 |
+
det_model = None
|
| 568 |
+
try:
|
| 569 |
+
det_model = predictor.load_detection_model(str(folder), device)
|
| 570 |
+
_pass("load_detection_model() returned successfully.")
|
| 571 |
+
except NotImplementedError:
|
| 572 |
+
_fail("load_detection_model() raises NotImplementedError β "
|
| 573 |
+
"you MUST implement this function!")
|
| 574 |
+
except Exception as e:
|
| 575 |
+
_fail(f"load_detection_model() raised: {e}")
|
| 576 |
+
traceback.print_exc()
|
| 577 |
+
|
| 578 |
+
# 6b. predict_detection_segmentation β must NOT raise NotImplementedError
|
| 579 |
+
det_out = None
|
| 580 |
+
if det_model is not None:
|
| 581 |
+
try:
|
| 582 |
+
det_out = predictor.predict_detection_segmentation(det_model, [test_img])
|
| 583 |
+
_pass("predict_detection_segmentation() returned successfully.")
|
| 584 |
+
validate_det_output(
|
| 585 |
+
det_out,
|
| 586 |
+
num_images=1,
|
| 587 |
+
img_sizes=[(img_w, img_h)],
|
| 588 |
+
max_label=max_label,
|
| 589 |
+
)
|
| 590 |
+
except NotImplementedError:
|
| 591 |
+
_fail("predict_detection_segmentation() raises NotImplementedError β "
|
| 592 |
+
"you MUST implement this function!")
|
| 593 |
+
det_out = None
|
| 594 |
+
except Exception as e:
|
| 595 |
+
_fail(f"predict_detection_segmentation() raised: {e}")
|
| 596 |
+
traceback.print_exc()
|
| 597 |
+
det_out = None
|
| 598 |
+
|
| 599 |
+
# 6c. Compute mIoU if GT annotation is available
|
| 600 |
+
if det_out is not None and anno_path is not None and len(det_out) > 0:
|
| 601 |
+
try:
|
| 602 |
+
remap_seg = build_remap(predictor.SEG_CLASS_MAPPING)
|
| 603 |
+
pred = det_out[0]
|
| 604 |
+
IGNORE_LABEL = 255
|
| 605 |
+
|
| 606 |
+
# Build predicted semantic map (highest-confidence per pixel)
|
| 607 |
+
pred_sem = np.full((img_h, img_w), IGNORE_LABEL, dtype=np.uint8)
|
| 608 |
+
pred_conf = np.full((img_h, img_w), -1.0, dtype=np.float32)
|
| 609 |
+
for mask, score, label in zip(
|
| 610 |
+
pred["masks"], pred["scores"], pred["labels"]
|
| 611 |
+
):
|
| 612 |
+
canonical = remap_seg.get(label)
|
| 613 |
+
if canonical is None:
|
| 614 |
+
continue
|
| 615 |
+
binary = np.asarray(mask, dtype=np.uint8)
|
| 616 |
+
if binary.shape != (img_h, img_w):
|
| 617 |
+
mask_pil = Image.fromarray(binary * 255)
|
| 618 |
+
mask_pil = mask_pil.resize((img_w, img_h), Image.NEAREST)
|
| 619 |
+
binary = (np.array(mask_pil) > 127).astype(np.uint8)
|
| 620 |
+
higher = (binary == 1) & (score > pred_conf)
|
| 621 |
+
pred_sem[higher] = canonical
|
| 622 |
+
pred_conf[higher] = score
|
| 623 |
+
|
| 624 |
+
# Build GT semantic map from polygon annotations
|
| 625 |
+
gt_sem = np.full((img_h, img_w), IGNORE_LABEL, dtype=np.uint8)
|
| 626 |
+
for item in gt_items:
|
| 627 |
+
gt_mask = rasterize_polygons(item["segmentation"], img_w, img_h)
|
| 628 |
+
gt_sem[gt_mask == 1] = item["canonical_idx"]
|
| 629 |
+
|
| 630 |
+
# Per-class IoU
|
| 631 |
+
intersection = np.zeros(NUM_CLASSES, dtype=np.float64)
|
| 632 |
+
union = np.zeros(NUM_CLASSES, dtype=np.float64)
|
| 633 |
+
for c in range(NUM_CLASSES):
|
| 634 |
+
pred_c = (pred_sem == c)
|
| 635 |
+
gt_c = (gt_sem == c)
|
| 636 |
+
intersection[c] = np.logical_and(pred_c, gt_c).sum()
|
| 637 |
+
union[c] = np.logical_or(pred_c, gt_c).sum()
|
| 638 |
+
|
| 639 |
+
per_class_iou = []
|
| 640 |
+
for c in range(NUM_CLASSES):
|
| 641 |
+
if union[c] > 0:
|
| 642 |
+
per_class_iou.append(float(intersection[c] / union[c]))
|
| 643 |
+
else:
|
| 644 |
+
per_class_iou.append(float("nan"))
|
| 645 |
+
|
| 646 |
+
valid_ious = [v for v in per_class_iou if not np.isnan(v)]
|
| 647 |
+
miou = float(np.mean(valid_ious)) if valid_ious else 0.0
|
| 648 |
+
|
| 649 |
+
print(f"\n ** Segmentation mIoU: {miou:.4f} **")
|
| 650 |
+
for c in range(NUM_CLASSES):
|
| 651 |
+
iou_str = f"{per_class_iou[c]:.4f}" if not np.isnan(per_class_iou[c]) else "N/A"
|
| 652 |
+
print(f" {CANONICAL_CLASSES_LIST[c]:20s}: {iou_str}")
|
| 653 |
+
except Exception as e:
|
| 654 |
+
_warn(f"Could not compute mIoU: {e}")
|
| 655 |
+
|
| 656 |
+
# βββ Summary βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 657 |
+
print("\n" + "=" * 60)
|
| 658 |
+
print(f" RESULTS: {_pass_count} passed, {_fail_count} failed, "
|
| 659 |
+
f"{_warn_count} warnings")
|
| 660 |
+
print("=" * 60)
|
| 661 |
+
if _fail_count > 0:
|
| 662 |
+
print("\n VALIDATION FAILED β fix the [FAIL] items above before submitting.\n")
|
| 663 |
+
sys.exit(1)
|
| 664 |
+
elif _warn_count > 0:
|
| 665 |
+
print("\n VALIDATION PASSED WITH WARNINGS β review [WARN] items above.\n")
|
| 666 |
+
else:
|
| 667 |
+
print("\n ALL CHECKS PASSED β your submission looks good!\n")
|
| 668 |
+
|
| 669 |
+
|
| 670 |
+
if __name__ == "__main__":
|
| 671 |
+
main()
|