| """ |
| Inference script for QLD-WOB-v1 (Queensland Wet Tropics, WildObs) |
| |
| Camera-trap classifier for the Wet Tropics of Queensland, Australia. |
| |
| Model: WildObs QLD WetTropics |
| Input: 480x480 RGB, NHWC, unnormalised |
| Framework: PyTorch (whole pickled model object, not a state_dict) |
| Classes: 15 Wet Tropics species |
| Developer: Prakash Palanivelu Rajmohan and Renuka Sharma (WildObs) |
| Info: https://huggingface.co/WildObs/WildObs_QLD_WetTropics |
| |
| Ported from AddaxAI's legacy classify_detections.py (wildobs-qld-wettropics), |
| whose crop function came from the authors' own evaluation notebook: |
| https://huggingface.co/WildObs/WildObs_QLD_WetTropics/blob/main/Evaluate_WetTropics_hf.ipynb |
| |
| Author: Peter van Lunteren |
| """ |
|
|
| from __future__ import annotations |
|
|
| from pathlib import Path |
|
|
| import numpy as np |
| import torch |
| from PIL import Image, ImageFile |
|
|
| from torchvision import transforms |
|
|
| |
| ImageFile.LOAD_TRUNCATED_IMAGES = True |
|
|
| |
| |
| |
| CROP_SIZE = 600 |
| INPUT_SIZE = 480 |
|
|
| |
| CLASS_NAMES = [ |
| 'Alectura_lathami', 'Bos_taurus', 'Canis_familiaris', 'Casuarius_casuarius', |
| 'Felis_catus', 'Heteromyias_cinereifrons', 'Homo_sapiens', |
| 'Hypsiprymnodon_moschatus', 'Megapodius_reinwardt', 'Orthonyx_spaldingii', |
| 'Perameles_nasuta', 'Sus_scrofa', 'Thylogale_stigmatica', |
| 'Uromys_caudimaculatus', 'Wallabia_bicolor' |
| ] |
|
|
|
|
| class ModelInference: |
| """WildObs Wet Tropics classifier.""" |
|
|
| def __init__(self, model_dir: Path, model_path: Path) -> None: |
| self.model_dir = Path(model_dir) |
| self.model_path = Path(model_path) |
| self.model = None |
| self.device: torch.device | None = None |
| |
| |
| self.preprocess = transforms.Compose([ |
| transforms.Resize((INPUT_SIZE, INPUT_SIZE)), |
| transforms.ToTensor(), |
| ]) |
|
|
| |
| |
| |
|
|
| def check_gpu(self) -> bool: |
| try: |
| if torch.backends.mps.is_built() and torch.backends.mps.is_available(): |
| return True |
| except Exception: |
| pass |
| return torch.cuda.is_available() |
|
|
| def load_model(self) -> None: |
| if self.check_gpu(): |
| self.device = torch.device( |
| "mps" if torch.backends.mps.is_available() else "cuda" |
| ) |
| else: |
| self.device = torch.device("cpu") |
|
|
| |
| |
| self.model = torch.load( |
| self.model_path, map_location=self.device, weights_only=False |
| ) |
| self.model.eval() |
| self.model.to(self.device) |
|
|
| def get_crop( |
| self, image: Image.Image, bbox: tuple[float, float, float, float] |
| ) -> Image.Image: |
| """Crop the bbox, then square it to 600x600 as the authors do.""" |
| width, height = image.size |
| x, y, w, h = bbox |
| left = int(x * width) |
| top = int(y * height) |
| right = int((x + w) * width) |
| bottom = int((y + h) * height) |
| crop = image.crop((left, top, right, bottom)) |
| return crop.resize((CROP_SIZE, CROP_SIZE), Image.BILINEAR) |
|
|
| def get_classification(self, crop: Image.Image) -> list[list]: |
| """Per-crop inference. Returns [[name, prob], ...] for all classes.""" |
| assert self.model is not None |
| batch = self._to_nhwc(np.stack([self.get_tensor(crop)])) |
| probs = self._forward(batch)[0] |
| return [[CLASS_NAMES[i], float(probs[i])] for i in range(len(probs))] |
|
|
| def get_class_names(self) -> dict[str, str]: |
| """1-indexed mapping {id: class_name} for the output JSON.""" |
| return {str(i + 1): name for i, name in enumerate(CLASS_NAMES)} |
|
|
| |
| |
| |
|
|
| def get_tensor(self, crop: Image.Image) -> np.ndarray: |
| if crop.mode != "RGB": |
| crop = crop.convert("RGB") |
| return self.preprocess(crop).numpy() |
|
|
| def classify_batch(self, batch: np.ndarray) -> list[list[list]]: |
| assert self.model is not None |
| probs = self._forward(self._to_nhwc(batch)) |
| return [ |
| [[CLASS_NAMES[j], float(p[j])] for j in range(len(p))] |
| for p in probs |
| ] |
|
|
| |
| |
| |
|
|
| def _to_nhwc(self, batch: np.ndarray) -> torch.Tensor: |
| """B,C,H,W -> B,H,W,C. This model takes channels last.""" |
| return torch.from_numpy(batch).permute(0, 2, 3, 1) |
|
|
| def _forward(self, batch: torch.Tensor) -> np.ndarray: |
| assert self.model is not None |
| with torch.no_grad(): |
| logits = self.model(batch.to(self.device)) |
| return torch.softmax(logits, dim=1).cpu().numpy() |
|
|