TobiasLogic's picture
Upload ObjectModel-v1: code, README, assets (v1 checkpoint)
9b92c75 verified
Raw
History Blame Contribute Delete
6.37 kB
from __future__ import annotations
import json
import random
from collections import defaultdict
from pathlib import Path
from typing import Any
import numpy as np
from PIL import Image, ImageEnhance
import torch
from torch import Tensor
from torch.utils.data import Dataset
from .boxes import box_xyxy_to_cxcywh
class CocoDetectionDataset(Dataset):
"""Minimal COCO detection loader with deterministic category remapping."""
def __init__(
self,
image_dir: str | Path,
annotation_file: str | Path,
input_size: int,
training: bool,
hflip_prob: float = 0.5,
scale_range: tuple[float, float] = (0.65, 1.0),
mean: tuple[float, float, float] = (0.485, 0.456, 0.406),
std: tuple[float, float, float] = (0.229, 0.224, 0.225),
) -> None:
self.image_dir = Path(image_dir)
self.annotation_file = Path(annotation_file)
self.input_size = input_size
self.training = training
self.hflip_prob = hflip_prob
self.scale_range = scale_range
self.mean = torch.tensor(mean, dtype=torch.float32)[:, None, None]
self.std = torch.tensor(std, dtype=torch.float32)[:, None, None]
with self.annotation_file.open("r", encoding="utf-8") as handle:
data = json.load(handle)
self.images = sorted(data["images"], key=lambda item: item["id"])
annotations: dict[int, list[dict[str, Any]]] = defaultdict(list)
for annotation in data["annotations"]:
if annotation.get("iscrowd", 0) == 0 and annotation["bbox"][2] > 0 and annotation["bbox"][3] > 0:
annotations[annotation["image_id"]].append(annotation)
self.annotations = annotations
category_ids = sorted(category["id"] for category in data["categories"])
self.category_to_label = {category_id: label for label, category_id in enumerate(category_ids)}
self.label_to_category = {label: category_id for category_id, label in self.category_to_label.items()}
self.categories = sorted(data["categories"], key=lambda item: self.category_to_label[item["id"]])
def __len__(self) -> int:
return len(self.images)
def _letterbox(
self, image: Image.Image, boxes: Tensor
) -> tuple[Image.Image, Tensor, tuple[float, int, int]]:
width, height = image.size
scale_jitter = random.uniform(*self.scale_range) if self.training else 1.0
ratio = min(self.input_size / width, self.input_size / height) * scale_jitter
resized_width = max(1, round(width * ratio))
resized_height = max(1, round(height * ratio))
image = image.resize((resized_width, resized_height), Image.Resampling.BILINEAR)
max_x = self.input_size - resized_width
max_y = self.input_size - resized_height
if self.training:
offset_x = random.randint(0, max_x) if max_x else 0
offset_y = random.randint(0, max_y) if max_y else 0
else:
offset_x, offset_y = max_x // 2, max_y // 2
canvas = Image.new("RGB", (self.input_size, self.input_size), (114, 114, 114))
canvas.paste(image, (offset_x, offset_y))
if boxes.numel():
boxes = boxes * ratio
boxes[:, [0, 2]] += offset_x
boxes[:, [1, 3]] += offset_y
return canvas, boxes, (ratio, offset_x, offset_y)
def __getitem__(self, index: int) -> tuple[Tensor, dict[str, Tensor]]:
image_info = self.images[index]
image = Image.open(self.image_dir / image_info["file_name"]).convert("RGB")
original_width, original_height = image.size
records = self.annotations.get(image_info["id"], [])
boxes = []
labels = []
for record in records:
x, y, width, height = record["bbox"]
boxes.append((x, y, x + width, y + height))
labels.append(self.category_to_label[record["category_id"]])
box_tensor = torch.tensor(boxes, dtype=torch.float32).reshape(-1, 4)
label_tensor = torch.tensor(labels, dtype=torch.int64)
if self.training and random.random() < self.hflip_prob:
image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
if box_tensor.numel():
old_x0 = box_tensor[:, 0].clone()
box_tensor[:, 0] = original_width - box_tensor[:, 2]
box_tensor[:, 2] = original_width - old_x0
if self.training:
image = ImageEnhance.Color(image).enhance(random.uniform(0.8, 1.2))
image = ImageEnhance.Contrast(image).enhance(random.uniform(0.8, 1.2))
image, box_tensor, (ratio, offset_x, offset_y) = self._letterbox(image, box_tensor)
if box_tensor.numel():
box_tensor = box_xyxy_to_cxcywh(box_tensor) / self.input_size
valid = (box_tensor[:, 2] > 1e-4) & (box_tensor[:, 3] > 1e-4)
box_tensor = box_tensor[valid].clamp(0.0, 1.0)
label_tensor = label_tensor[valid]
image_array = np.asarray(image, dtype=np.float32).copy() / 255.0
image_tensor = torch.from_numpy(image_array).permute(2, 0, 1)
image_tensor = (image_tensor - self.mean) / self.std
target = {
"boxes": box_tensor,
"labels": label_tensor,
"image_id": torch.tensor(image_info["id"], dtype=torch.int64),
"original_size": torch.tensor([original_height, original_width], dtype=torch.int64),
"transform": torch.tensor([ratio, offset_x, offset_y], dtype=torch.float32),
}
return image_tensor, target
def detection_collate(batch):
images, targets = zip(*batch, strict=True)
return torch.stack(images), list(targets)
def build_dataset(config: dict, root: str | Path, split: str) -> CocoDetectionDataset:
data = config["data"]
root = Path(root)
training = split == "train"
return CocoDetectionDataset(
root / data[f"{split}_image_dir"],
root / data[f"{split}_annotations"],
input_size=int(config["model"]["input_size"]),
training=training,
hflip_prob=float(data.get("hflip_prob", 0.5)),
scale_range=tuple(data.get("scale_range", (0.65, 1.0))) if training else (1.0, 1.0),
mean=tuple(data.get("mean", (0.485, 0.456, 0.406))),
std=tuple(data.get("std", (0.229, 0.224, 0.225))),
)