"""Standalone inference for the Tiny Vial CNN Hugging Face release.""" from __future__ import annotations import argparse from pathlib import Path import numpy as np from PIL import Image, ImageDraw import torch from safetensors.torch import load_file from torch import nn IMAGE_SIZE = 64 class TinyVialDetector(nn.Module): def __init__(self) -> None: super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 12, 3, stride=2, padding=1), nn.ReLU(), nn.Conv2d(12, 24, 3, stride=2, padding=1), nn.ReLU(), nn.Conv2d(24, 32, 3, stride=2, padding=1), nn.ReLU(), ) self.head = nn.Sequential( nn.Flatten(), nn.Linear(32 * 8 * 8, 64), nn.ReLU(), nn.Linear(64, 4), nn.Sigmoid(), ) def forward(self, image: torch.Tensor) -> torch.Tensor: return self.head(self.features(image)) def predict(image_path: Path, weights_path: Path) -> tuple[int, int, int, int]: model = TinyVialDetector() model.load_state_dict(load_file(weights_path, device="cpu")) model.eval() with Image.open(image_path) as source: width, height = source.size resized = source.convert("RGB").resize((IMAGE_SIZE, IMAGE_SIZE), Image.Resampling.BILINEAR) array = np.asarray(resized, dtype=np.float32).copy() tensor = torch.from_numpy(array).permute(2, 0, 1).unsqueeze(0) / 255.0 with torch.inference_mode(): cx, cy, box_width, box_height = model(tensor)[0].tolist() x0 = max(0, min(width - 1, round((cx - box_width / 2) * width))) y0 = max(0, min(height - 1, round((cy - box_height / 2) * height))) x1 = max(x0, min(width - 1, round((cx + box_width / 2) * width) - 1)) y1 = max(y0, min(height - 1, round((cy + box_height / 2) * height) - 1)) return x0, y0, x1, y1 def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("image", type=Path) parser.add_argument("--weights", type=Path, default=Path("model.safetensors")) parser.add_argument("--output", type=Path, default=Path("prediction.png")) args = parser.parse_args() bbox = predict(args.image, args.weights) image = Image.open(args.image).convert("RGB") ImageDraw.Draw(image).rectangle(bbox, outline="#76b900", width=2) image.save(args.output) print({"bbox_2d_tight": list(bbox), "output": str(args.output)}) if __name__ == "__main__": main()