Addax-Data-Science commited on
Commit
34b99ce
·
verified ·
1 Parent(s): 6d48b52

Upload 2 files

Browse files
Files changed (2) hide show
  1. inference.py +237 -0
  2. taxonomy.csv +47 -0
inference.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ModelInference for the addax-sppnet model family.
3
+
4
+ Architecture: SpeciesNet GraphModule backbone (frozen) + a thin nn.Linear
5
+ head fine-tuned per region. Originally written for AddaxAI's legacy
6
+ classify_detections.py (Peter van Lunteren, 13 May 2025); ported here to
7
+ the WebUI's class-based ModelInference interface.
8
+
9
+ Files expected in the model directory:
10
+ - <model_fname>.pt fine-tuned head checkpoint, e.g. final-20260317.pt
11
+ - <backbone>.pt frozen SpeciesNet backbone, one of:
12
+ - always_crop_99710272_22x8_v12_epoch_00148.pt
13
+ - full_image_88545560_22x8_v12_epoch_00153.pt
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ # Allow loading checkpoints saved on a Windows runner on a POSIX machine.
19
+ import pathlib
20
+ import platform
21
+ from pathlib import Path
22
+
23
+ import numpy as np
24
+ import torch
25
+ import torch.nn as nn
26
+ import torch.nn.functional as F
27
+ from PIL import Image
28
+ from torchvision import transforms
29
+
30
+ if platform.system() != "Windows":
31
+ pathlib.WindowsPath = pathlib.PosixPath # type: ignore[assignment]
32
+
33
+ # Don't fail on truncated images during inference.
34
+ from PIL import ImageFile
35
+
36
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
37
+
38
+
39
+ _BACKBONE_FILENAMES = (
40
+ "always_crop_99710272_22x8_v12_epoch_00148.pt",
41
+ "full_image_88545560_22x8_v12_epoch_00153.pt",
42
+ )
43
+
44
+
45
+ def _load_fx_checkpoint(weights_path: Path, map_location: str = "cpu") -> nn.Module:
46
+ """Load a SpeciesNet onnx2torch GraphModule.
47
+
48
+ The backbone is shipped as a torch.fx GraphModule. PyTorch 2.4+
49
+ requires `reduce_graph_module` to be in the safe-globals allowlist
50
+ when loading with `weights_only=True`; older versions don't have
51
+ this concept. Try both paths.
52
+ """
53
+ try:
54
+ from torch.fx.graph_module import reduce_graph_module
55
+ from torch.serialization import add_safe_globals
56
+ add_safe_globals([reduce_graph_module])
57
+ except Exception:
58
+ pass
59
+
60
+ try:
61
+ obj = torch.load(weights_path, map_location=map_location, weights_only=True)
62
+ except Exception:
63
+ obj = torch.load(weights_path, map_location=map_location, weights_only=False)
64
+
65
+ if hasattr(obj, "state_dict") and hasattr(obj, "forward"):
66
+ return obj
67
+ raise ValueError(f"{weights_path} is not a torch.nn.Module GraphModule")
68
+
69
+
70
+ class _FXClassifier(nn.Module):
71
+ """SpeciesNet backbone (frozen) + linear head."""
72
+
73
+ def __init__(
74
+ self,
75
+ backbone: nn.Module,
76
+ num_classes: int,
77
+ img_size: int = 480,
78
+ input_layout: str = "nhwc",
79
+ ) -> None:
80
+ super().__init__()
81
+ self.backbone = backbone
82
+ self.input_layout = input_layout.lower()
83
+
84
+ for p in self.backbone.parameters():
85
+ p.requires_grad = False
86
+ self.backbone.eval()
87
+
88
+ # Probe the backbone to discover output feature size at this
89
+ # img_size + layout combo, so the head matches exactly.
90
+ with torch.no_grad():
91
+ x = torch.zeros(1, 3, img_size, img_size)
92
+ if self.input_layout == "nhwc":
93
+ x = x.permute(0, 2, 3, 1).contiguous()
94
+ z = self.backbone(x)
95
+ z = self._pool(z)
96
+ in_features = z.shape[1]
97
+
98
+ self.head = nn.Linear(in_features, num_classes)
99
+
100
+ @staticmethod
101
+ def _pool(z: torch.Tensor) -> torch.Tensor:
102
+ if z.ndim == 4:
103
+ return F.adaptive_avg_pool2d(z, 1).flatten(1)
104
+ if z.ndim == 3:
105
+ return z.mean(dim=1)
106
+ return z.flatten(1)
107
+
108
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
109
+ if self.input_layout == "nhwc":
110
+ x = x.permute(0, 2, 3, 1).contiguous()
111
+ z = self.backbone(x)
112
+ z = self._pool(z)
113
+ return self.head(z)
114
+
115
+
116
+ class ModelInference:
117
+ """ModelInference for the addax-sppnet family (SpeciesNet backbone + linear head)."""
118
+
119
+ def __init__(self, model_dir: Path, model_path: Path) -> None:
120
+ self.model_dir = Path(model_dir)
121
+ self.model_path = Path(model_path)
122
+ self.model: _FXClassifier | None = None
123
+ self.device: torch.device | None = None
124
+ self._class_names: list[str] = []
125
+ self._preprocess: transforms.Compose | None = None
126
+
127
+ # ------------------------------------------------------------------
128
+ # Required interface
129
+ # ------------------------------------------------------------------
130
+
131
+ def check_gpu(self) -> bool:
132
+ try:
133
+ if torch.backends.mps.is_built() and torch.backends.mps.is_available():
134
+ return True
135
+ except Exception:
136
+ pass
137
+ return torch.cuda.is_available()
138
+
139
+ def load_model(self) -> None:
140
+ if self.check_gpu():
141
+ self.device = torch.device(
142
+ "mps" if torch.backends.mps.is_available() else "cuda"
143
+ )
144
+ else:
145
+ self.device = torch.device("cpu")
146
+
147
+ # Load fine-tuned head checkpoint.
148
+ try:
149
+ checkpoint = torch.load(
150
+ self.model_path, map_location=self.device, weights_only=True
151
+ )
152
+ except Exception:
153
+ checkpoint = torch.load(
154
+ self.model_path, map_location=self.device, weights_only=False
155
+ )
156
+
157
+ # Resolve backbone path. The fine-tuned model ships alongside
158
+ # one of two known backbone files, depending on the recipe.
159
+ backbone_path: Path | None = None
160
+ for name in _BACKBONE_FILENAMES:
161
+ candidate = self.model_dir / name
162
+ if candidate.exists():
163
+ backbone_path = candidate
164
+ break
165
+ if backbone_path is None:
166
+ raise FileNotFoundError(
167
+ "Backbone weights not found. Expected one of "
168
+ f"{_BACKBONE_FILENAMES} in {self.model_dir}."
169
+ )
170
+
171
+ backbone = _load_fx_checkpoint(backbone_path, map_location="cpu")
172
+ model = _FXClassifier(
173
+ backbone=backbone,
174
+ num_classes=checkpoint["num_classes"],
175
+ img_size=checkpoint["img_size"],
176
+ input_layout=checkpoint["input_layout"],
177
+ )
178
+ model.load_state_dict(checkpoint["model"])
179
+ self.model = model.to(self.device).eval()
180
+
181
+ self._class_names = list(checkpoint["class_names"])
182
+
183
+ norm = checkpoint["normalize"]
184
+ img_size = checkpoint["img_size"]
185
+ self._preprocess = transforms.Compose([
186
+ transforms.Resize((img_size, img_size), antialias=True),
187
+ transforms.ToTensor(),
188
+ transforms.Normalize(mean=norm["mean"], std=norm["std"]),
189
+ ])
190
+
191
+ def get_crop(
192
+ self, image: Image.Image, bbox: tuple[float, float, float, float]
193
+ ) -> Image.Image:
194
+ """Crop the bbox region. SpeciesNet head was trained on tight crops."""
195
+ W, H = image.size
196
+ x, y, w, h = bbox
197
+ left = max(0, int(round(x * W)))
198
+ top = max(0, int(round(y * H)))
199
+ right = min(W, int(round((x + w) * W)))
200
+ bottom = min(H, int(round((y + h) * H)))
201
+ if right <= left or bottom <= top:
202
+ return image
203
+ return image.crop((left, top, right, bottom))
204
+
205
+ def get_classification(self, crop: Image.Image) -> list[list]:
206
+ """Per-image inference. Returns [[name, prob], ...] for all classes."""
207
+ assert self.model is not None and self._preprocess is not None
208
+ if crop.mode != "RGB":
209
+ crop = crop.convert("RGB")
210
+ tensor = self._preprocess(crop).unsqueeze(0).to(self.device)
211
+ with torch.no_grad():
212
+ probs = F.softmax(self.model(tensor), dim=1).cpu().numpy()[0]
213
+ return [[self._class_names[i], float(probs[i])] for i in range(len(probs))]
214
+
215
+ def get_class_names(self) -> dict[str, str]:
216
+ """1-indexed mapping {id: class_name} for the output JSON."""
217
+ return {str(i + 1): name for i, name in enumerate(self._class_names)}
218
+
219
+ # ------------------------------------------------------------------
220
+ # Optional batch interface (5-15x GPU speedup vs per-crop calls)
221
+ # ------------------------------------------------------------------
222
+
223
+ def get_tensor(self, crop: Image.Image) -> np.ndarray:
224
+ assert self._preprocess is not None
225
+ if crop.mode != "RGB":
226
+ crop = crop.convert("RGB")
227
+ return self._preprocess(crop).numpy()
228
+
229
+ def classify_batch(self, batch: np.ndarray) -> list[list[list]]:
230
+ assert self.model is not None
231
+ tensor = torch.from_numpy(batch).to(self.device)
232
+ with torch.no_grad():
233
+ probs = F.softmax(self.model(tensor), dim=1).cpu().numpy()
234
+ return [
235
+ [[self._class_names[j], float(p[j])] for j in range(len(p))]
236
+ for p in probs
237
+ ]
taxonomy.csv ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model_class,class,order,family,genus,species
2
+ american bullfrog,amphibia,anura,ranidae,lithobates,catesbeianus
3
+ american mink,mammalia,carnivora,mustelidae,neogale,vison
4
+ american toad,amphibia,anura,bufonidae,anaxyrus,americanus
5
+ brown rat,mammalia,rodentia,muridae,rattus,norvegicus
6
+ butler's gartersnake,reptilia,squamata,colubridae,thamnophis,butleri
7
+ common five-linked skink,reptilia,squamata,scincidae,plestiodon,fasciatus
8
+ common yellowthroat,aves,passeriformes,parulidae,geothlypis,trichas
9
+ dekay's brownsnake,reptilia,squamata,colubridae,storeria,dekayi
10
+ eastern bluebird,aves,passeriformes,turdidae,sialia,sialis
11
+ eastern chipmunk,mammalia,rodentia,sciuridae,tamias,striatus
12
+ eastern cottontail,mammalia,lagomorpha,leporidae,sylvilagus,floridanus
13
+ eastern gartersnake,reptilia,squamata,colubridae,thamnophis,sirtalis
14
+ eastern hog-nosed snake,reptilia,squamata,colubridae,heterodon,platirhinos
15
+ eastern massasauga,reptilia,squamata,viperidae,sistrurus,catenatus
16
+ eastern milksnake,reptilia,squamata,colubridae,lampropeltis,triangulum
17
+ eastern racer snake,reptilia,squamata,colubridae,coluber,constrictor
18
+ eastern ribbonsnake,reptilia,squamata,colubridae,thamnophis,saurita
19
+ false detection,,,,,
20
+ gray catbird,aves,passeriformes,mimidae,dumetella,carolinensis
21
+ gray ratsnake,reptilia,squamata,colubridae,pantherophis,spiloides
22
+ green frog,amphibia,anura,ranidae,lithobates,clamitans
23
+ indigo bunting,aves,passeriformes,cardinalidae,passerina,cyanea
24
+ invertebrate,,,,,
25
+ kirtland's snake,reptilia,squamata,colubridae,clonophis,kirtlandii
26
+ long-tailed weasel,mammalia,carnivora,mustelidae,neogale,frenata
27
+ masked shrew,mammalia,soricomorpha,soricidae,sorex,cinereus
28
+ meadow jumping mouse,mammalia,rodentia,dipodidae,zapus,hudsonius
29
+ meadow vole,mammalia,rodentia,cricetidae,microtus,pennsylvanicus
30
+ northern house wren,aves,passeriformes,troglodytidae,troglodytes,aedon
31
+ northern leopard frog,amphibia,anura,ranidae,lithobates,pipiens
32
+ northern short-tailed shrew,mammalia,soricomorpha,soricidae,blarina,brevicauda
33
+ northern watersnake,reptilia,squamata,colubridae,nerodia,sipedon
34
+ painted turtle,reptilia,testudines,emydidae,chrysemys,picta
35
+ plains gartersnake,reptilia,squamata,colubridae,thamnophis,radix
36
+ raccoon,mammalia,carnivora,procyonidae,procyon,lotor
37
+ red-bellied snake,reptilia,squamata,colubridae,storeria,occipitomaculata
38
+ smooth greensnake,reptilia,squamata,colubridae,opheodrys,vernalis
39
+ snapping turtle,reptilia,testudines,chelydridae,chelydra,serpentina
40
+ song sparrow,aves,passeriformes,passerellidae,melospiza,melodia
41
+ sora,aves,gruiformes,rallidae,porzana,carolina
42
+ star-nosed mole,mammalia,soricomorpha,talpidae,condylura,cristata
43
+ striped skunk,mammalia,carnivora,mephitidae,mephitis,mephitis
44
+ virginia opossum,mammalia,didelphimorphia,didelphidae,didelphis,virginiana
45
+ white-footed mouse,mammalia,rodentia,cricetidae,peromyscus,leucopus
46
+ woodchuck,mammalia,rodentia,sciuridae,marmota,monax
47
+ woodland jumping mouse,mammalia,rodentia,dipodidae,napaeozapus,insignis