Download scripts/detector_lib.py from Benxelua/Detector: direct link, hf CLI and curl.
- Browser
- Download file 9.28 kB
-
https://huggingface.co/Benxelua/Detector/resolve/main/scripts/detector_lib.py
- Command line
-
hf download hf://Benxelua/Detector/scripts/detector_lib.py
-
curl -L -o detector_lib.py https://huggingface.co/Benxelua/Detector/resolve/main/scripts/detector_lib.py
9.28 kB
| """Shared locked-protocol dataset, model, and COCO evaluator utilities.""" | |
| from __future__ import annotations | |
| import contextlib | |
| import io | |
| import json | |
| import xml.etree.ElementTree as ET | |
| from pathlib import Path | |
| import torch | |
| from PIL import Image | |
| from torch.utils.data import Dataset | |
| CLASS_NAMES = ("car", "motorbike", "bicycle", "chair", "diningtable", "bottle", "tvmonitor", "bus") | |
| EXPECTED = {"train": 1647, "val": 183, "test": 400} | |
| def resolve_image(dataset_root: Path, domain: str, basename: str) -> Path: | |
| folder = "RGB_normal" if domain == "bright" else "RGB_Dark" | |
| hits = list(dataset_root.rglob(f"{folder}/{basename}")) | |
| if len(hits) != 1: | |
| hits = [p for p in dataset_root.rglob(basename) if folder.lower() in str(p).lower()] | |
| if len(hits) != 1: | |
| raise RuntimeError(f"expected exactly one {domain} image for {basename}, found {hits}") | |
| return hits[0] | |
| def read_yolo(path: Path, width: int, height: int): | |
| boxes, labels = [], [] | |
| for line in path.read_text().splitlines(): | |
| if not line.strip(): | |
| continue | |
| cls, cx, cy, bw, bh = map(float, line.split()) | |
| if not 0 <= int(cls) < len(CLASS_NAMES): | |
| raise RuntimeError(f"invalid class id {cls} in {path}") | |
| boxes.append([(cx - bw / 2) * width, (cy - bh / 2) * height, | |
| (cx + bw / 2) * width, (cy + bh / 2) * height]) | |
| labels.append(int(cls)) | |
| return (torch.tensor(boxes, dtype=torch.float32).reshape(-1, 4), | |
| torch.tensor(labels, dtype=torch.int64)) | |
| def read_voc(path: Path): | |
| boxes, labels = [], [] | |
| for obj in ET.parse(path).getroot().findall("object"): | |
| name = obj.findtext("name") | |
| if name not in CLASS_NAMES: | |
| raise RuntimeError(f"unknown VOC class {name!r} in {path}") | |
| box = obj.find("bndbox") | |
| boxes.append([float(box.findtext("xmin")), float(box.findtext("ymin")), | |
| float(box.findtext("xmax")), float(box.findtext("ymax"))]) | |
| labels.append(CLASS_NAMES.index(name)) | |
| return (torch.tensor(boxes, dtype=torch.float32).reshape(-1, 4), | |
| torch.tensor(labels, dtype=torch.int64)) | |
| class LockedLODDataset(Dataset): | |
| def __init__(self, manifest: Path, dataset_root: Path, labels_root: Path, limit: int | None = None): | |
| payload = json.loads(Path(manifest).read_text()) | |
| self.domain = payload["domain"] | |
| self.split = payload["split"] | |
| self.items = payload["items"][:limit] | |
| if len(payload["items"]) != EXPECTED[self.split]: | |
| raise RuntimeError(f"{manifest}: expected {EXPECTED[self.split]} locked rows, got {len(payload['items'])}") | |
| self.dataset_root, self.labels_root = Path(dataset_root), Path(labels_root) | |
| def __len__(self): | |
| return len(self.items) | |
| def __getitem__(self, index): | |
| item = self.items[index] | |
| image = Image.open(resolve_image(self.dataset_root, self.domain, item["image_basename"])).convert("RGB") | |
| # The public LOD Kaggle dataset contains the source VOC annotations. | |
| # Prefer them, retaining copied YOLO labels only as a local fallback. | |
| group = "RGB-normal-Annotations" if self.domain == "bright" else "RGB-dark-Annotations" | |
| voc = self.dataset_root / group / group / (Path(item["image_basename"]).stem + ".xml") | |
| if voc.is_file(): | |
| boxes, labels = read_voc(voc) | |
| else: | |
| label_domain = "normal" if self.domain == "bright" else "dark" | |
| label = self.labels_root / label_domain / self.split / "labels" / item["label_basename"] | |
| if not label.is_file(): | |
| raise FileNotFoundError(f"missing VOC and fallback label for {item['pair_id']}") | |
| boxes, labels = read_yolo(label, *image.size) | |
| return image, {"boxes": boxes, "labels": labels, | |
| "image_id": torch.tensor(index), | |
| "orig_size": torch.tensor([image.height, image.width])} | |
| def collate(batch): | |
| return tuple(zip(*batch)) | |
| def build_model(cfg): | |
| detector = cfg["detector"] | |
| if detector == "rtdetr": | |
| from transformers import RTDetrV2ForObjectDetection, RTDetrImageProcessor | |
| processor = RTDetrImageProcessor.from_pretrained(cfg["pretrained"]) | |
| model = RTDetrV2ForObjectDetection.from_pretrained( | |
| cfg["pretrained"], num_labels=len(CLASS_NAMES), | |
| id2label=dict(enumerate(CLASS_NAMES)), label2id={n: i for i, n in enumerate(CLASS_NAMES)}, | |
| ignore_mismatched_sizes=True, | |
| ) | |
| return model, processor, "hf" | |
| if detector == "fasterrcnn_r50_fpn": | |
| from torchvision.models.detection import fasterrcnn_resnet50_fpn_v2, FasterRCNN_ResNet50_FPN_V2_Weights | |
| from torchvision.models.detection.faster_rcnn import FastRCNNPredictor | |
| model = fasterrcnn_resnet50_fpn_v2(weights=FasterRCNN_ResNet50_FPN_V2_Weights.COCO_V1) | |
| model.roi_heads.box_predictor = FastRCNNPredictor(model.roi_heads.box_predictor.cls_score.in_features, len(CLASS_NAMES) + 1) | |
| return model, None, "torchvision" | |
| if detector == "ssdlite320_mobilenet_v3_large": | |
| from torchvision.models.detection import ssdlite320_mobilenet_v3_large, SSDLite320_MobileNet_V3_Large_Weights | |
| from torchvision.models.detection.ssdlite import SSDLiteClassificationHead | |
| model = ssdlite320_mobilenet_v3_large(weights=SSDLite320_MobileNet_V3_Large_Weights.COCO_V1) | |
| # TorchVision exposes each SSDLite predictor as a Sequential block; | |
| # its depthwise Conv2d is nested under block[0][0]. Derive the | |
| # feature widths from the loaded COCO head rather than hard-coding a | |
| # version-specific list. | |
| in_channels = [module[0][0].in_channels for module in model.head.classification_head.module_list] | |
| num_anchors = model.anchor_generator.num_anchors_per_location() | |
| model.head.classification_head = SSDLiteClassificationHead( | |
| in_channels, num_anchors, len(CLASS_NAMES) + 1, torch.nn.BatchNorm2d | |
| ) | |
| return model, None, "torchvision" | |
| raise ValueError(f"unsupported detector: {detector}") | |
| def image_tensors(images, device): | |
| from torchvision.transforms.functional import pil_to_tensor | |
| return [pil_to_tensor(image).float().div(255).to(device) for image in images] | |
| def hf_batch(processor, images, targets, device): | |
| annotations = [] | |
| for index, target in enumerate(targets): | |
| boxes = target["boxes"] | |
| xywh = torch.stack((boxes[:, 0], boxes[:, 1], boxes[:, 2] - boxes[:, 0], boxes[:, 3] - boxes[:, 1]), 1) | |
| annotations.append({"image_id": index, "annotations": [ | |
| {"bbox": box.tolist(), "category_id": int(label), "area": float(box[2] * box[3]), "iscrowd": 0} | |
| for box, label in zip(xywh, target["labels"]) | |
| ]}) | |
| encoded = processor(images=list(images), annotations=annotations, return_tensors="pt") | |
| return {key: (value.to(device) if hasattr(value, "to") else [{k: v.to(device) for k, v in x.items()} for x in value]) | |
| for key, value in encoded.items()} | |
| class CanonicalMAP: | |
| def __init__(self): | |
| self.images, self.annotations, self.predictions, self.annotation_id = {}, [], [], 1 | |
| def _xywh(box): | |
| x1, y1, x2, y2 = map(float, box.tolist()) | |
| return [x1, y1, max(0.0, x2 - x1), max(0.0, y2 - y1)] | |
| def update(self, predictions, targets): | |
| for pred, target in zip(predictions, targets): | |
| image_id = int(target["image_id"]) | |
| h, w = map(int, target["orig_size"].tolist()) | |
| self.images[image_id] = {"id": image_id, "height": h, "width": w} | |
| for box, label in zip(target["boxes"], target["labels"]): | |
| bbox = self._xywh(box) | |
| self.annotations.append({"id": self.annotation_id, "image_id": image_id, "category_id": int(label) + 1, | |
| "bbox": bbox, "area": bbox[2] * bbox[3], "iscrowd": 0}) | |
| self.annotation_id += 1 | |
| for box, score, label in zip(pred["boxes"], pred["scores"], pred["labels"]): | |
| self.predictions.append({"image_id": image_id, "category_id": int(label) + 1, | |
| "bbox": self._xywh(box), "score": float(score)}) | |
| def compute(self): | |
| from pycocotools.coco import COCO | |
| from pycocotools.cocoeval import COCOeval | |
| gt = COCO(); gt.dataset = {"images": list(self.images.values()), "annotations": self.annotations, | |
| "categories": [{"id": i + 1, "name": n} for i, n in enumerate(CLASS_NAMES)]}; gt.createIndex() | |
| dt = gt.loadRes(self.predictions) if self.predictions else COCO() | |
| if not self.predictions: | |
| dt.dataset = {"images": list(self.images.values()), "annotations": [], "categories": gt.dataset["categories"]}; dt.createIndex() | |
| evaluator = COCOeval(gt, dt, "bbox"); evaluator.params.imgIds = sorted(self.images); evaluator.params.catIds = list(range(1, 9)); evaluator.params.maxDets = [1, 10, 100] | |
| with contextlib.redirect_stdout(io.StringIO()): | |
| evaluator.evaluate(); evaluator.accumulate(); evaluator.summarize() | |
| return {"map50_95": float(evaluator.stats[0]), "map50": float(evaluator.stats[1])} | |