"""Standalone Tiny Vial CNN v2 inference using Safetensors.""" from __future__ import annotations import argparse import json from pathlib import Path import numpy as np from PIL import Image, ImageDraw from safetensors.torch import load_file import torch from torch import nn class TinyVialDetectorV2(nn.Module): def __init__(self): 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.shared = nn.Sequential(nn.Flatten(), nn.Linear(32 * 8 * 8, 64), nn.ReLU()) self.objectness = nn.Linear(64, 1) self.box = nn.Sequential(nn.Linear(64, 4), nn.Sigmoid()) def forward(self, image): hidden = self.shared(self.features(image)) return self.objectness(hidden), self.box(hidden) def pixel_box(parameters, width, height): cx, cy, box_width, box_height = (float(value) for value in parameters) 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(): parser = argparse.ArgumentParser() parser.add_argument("image", type=Path) parser.add_argument("--weights", type=Path, default=Path("model.safetensors")) parser.add_argument("--config", type=Path, default=Path("config.json")) parser.add_argument("--output", type=Path, default=Path("prediction.png")) args = parser.parse_args() config = json.loads(args.config.read_text()) model = TinyVialDetectorV2() model.load_state_dict(load_file(args.weights)) model.eval() image = Image.open(args.image).convert("RGB") resized = image.resize((config["image_size"], config["image_size"]), Image.Resampling.BILINEAR) tensor = torch.from_numpy(np.asarray(resized, dtype=np.float32).copy()).permute(2, 0, 1).unsqueeze(0) / 255.0 with torch.inference_mode(): logits, parameters = model(tensor) score = float(torch.sigmoid(logits[0, 0])) detected = score >= float(config["objectness_threshold"]) bbox = pixel_box(parameters[0], *image.size) if detected else None if bbox: ImageDraw.Draw(image).rectangle(bbox, outline=(118, 185, 0), width=max(1, image.width // 128)) args.output.parent.mkdir(parents=True, exist_ok=True) image.save(args.output) print(json.dumps({"detected": detected, "objectness_score": round(score, 6), "threshold": config["objectness_threshold"], "bbox": list(bbox) if bbox else None, "output": str(args.output)}, indent=2)) if __name__ == "__main__": main()