Spaces:
Running
Running
| import argparse | |
| import json | |
| from pathlib import Path | |
| from typing import Optional | |
| import numpy as np | |
| import torch | |
| import torch.nn.functional as F | |
| from PIL import Image | |
| from src.model import build_model | |
| from src.transforms import get_val_transforms | |
| UNCERTAIN_THRESHOLD = 0.30 | |
| def _normalize(name: str) -> str: | |
| return name.lower().replace(" ", "_").replace("-", "_") | |
| class FishPredictor: | |
| def __init__( | |
| self, | |
| checkpoint_path: str, | |
| metadata_path: str = "data/metadata/fish_info.json", | |
| device: Optional[str] = None, | |
| ): | |
| self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") | |
| ckpt = torch.load(checkpoint_path, map_location=self.device, weights_only=False) | |
| config = ckpt["config"] | |
| classes_path = Path(checkpoint_path).parent / "classes.json" | |
| self.classes = json.loads(classes_path.read_text()) | |
| config["model"]["num_classes"] = len(self.classes) | |
| self.model = build_model(config).to(self.device) | |
| self.model.load_state_dict(ckpt["model_state_dict"]) | |
| self.model.eval() | |
| self.transform = get_val_transforms(config["data"]["image_size"]) | |
| fish_list = json.loads(Path(metadata_path).read_text()) | |
| self.metadata = {_normalize(entry["species"]): entry for entry in fish_list} | |
| def predict(self, image: Image.Image, top_k: int = 3) -> dict: | |
| arr = np.array(image.convert("RGB")) | |
| tensor = self.transform(image=arr)["image"].unsqueeze(0).to(self.device) | |
| probs = F.softmax(self.model(tensor), dim=1)[0] | |
| top_probs, top_idx = probs.topk(min(top_k, len(self.classes))) | |
| predictions = [] | |
| for prob, idx in zip(top_probs.tolist(), top_idx.tolist()): | |
| name = self.classes[idx] | |
| predictions.append({ | |
| "species": name, | |
| "confidence": round(prob, 4), | |
| "metadata": self.metadata.get(_normalize(name), {}), | |
| }) | |
| uncertain = predictions[0]["confidence"] < UNCERTAIN_THRESHOLD | |
| return { | |
| "predictions": predictions, | |
| "uncertain": uncertain, | |
| "message": "Low confidence — species may be outside training distribution." if uncertain else None, | |
| } | |
| def export_onnx(self, output_path: str = "checkpoints/model.onnx", image_size: int = 300): | |
| dummy = torch.randn(1, 3, image_size, image_size).to(self.device) | |
| torch.onnx.export( | |
| self.model, dummy, output_path, | |
| input_names=["image"], output_names=["logits"], | |
| dynamic_axes={"image": {0: "batch"}, "logits": {0: "batch"}}, | |
| opset_version=17, | |
| ) | |
| print(f"ONNX model exported → {output_path}") | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--image", required=True) | |
| parser.add_argument("--checkpoint", default="checkpoints/best.pt") | |
| parser.add_argument("--top-k", type=int, default=3) | |
| args = parser.parse_args() | |
| predictor = FishPredictor(args.checkpoint) | |
| result = predictor.predict(Image.open(args.image), top_k=args.top_k) | |
| for i, p in enumerate(result["predictions"], 1): | |
| print(f"{i}. {p['species']:<30s} {p['confidence'] * 100:.1f}%") | |
| if result["uncertain"]: | |
| print(f"\nWarning: {result['message']}") | |