@@ -67,7 +67,60 @@ def _find_best_checkpoint(analysis_type: str) -> Optional[Path]:
6767 return candidates [0 ] if candidates else None
6868
6969
70- def _load_model (analysis_type : str = "deforestation" ) -> tuple [UNet , torch .device ]:
70+ def _find_onnx (analysis_type : str ) -> Optional [Path ]:
71+ """
72+ Search for a trained ONNX model for an analysis type.
73+ Priority: config.yaml ``onnx_weights`` > newest models/<analysis_type>_*/model.onnx
74+ > models/unet_<analysis_type>.onnx
75+ """
76+ model_cfg = get_model_config (analysis_type )
77+ config_path = model_cfg .get ("onnx_weights" )
78+ if config_path :
79+ p = _PROJECT_ROOT / config_path
80+ if p .exists ():
81+ return p
82+
83+ typed = sorted (
84+ _MODELS_DIR .glob (f"{ analysis_type } _*/model.onnx" ),
85+ key = lambda p : p .stat ().st_mtime ,
86+ reverse = True ,
87+ )
88+ if typed :
89+ return typed [0 ]
90+
91+ direct = _MODELS_DIR / f"unet_{ analysis_type } .onnx"
92+ return direct if direct .exists () else None
93+
94+
95+ class _ONNXSegmenter :
96+ """
97+ Drop-in replacement for :class:`UNet` in :func:`run_inference`.
98+
99+ Mimics the minimal torch.nn.Module surface that run_inference relies on
100+ (``n_channels``, ``n_classes``, ``.eval()``, ``.to()``, and being callable
101+ on a ``(N, C, H, W)`` tensor returning a logits tensor) so the rest of the
102+ pipeline needs no changes whether the backing model is PyTorch or ONNX.
103+ """
104+
105+ def __init__ (self , session : Any , n_channels : int , n_classes : int , input_name : str ):
106+ self ._session = session
107+ self .n_channels = n_channels
108+ self .n_classes = n_classes
109+ self ._input_name = input_name
110+
111+ def eval (self ) -> "_ONNXSegmenter" :
112+ return self
113+
114+ def to (self , _device : Any ) -> "_ONNXSegmenter" : # device handled by the ORT provider
115+ return self
116+
117+ def __call__ (self , tensor : "torch.Tensor" ) -> "torch.Tensor" :
118+ arr = tensor .detach ().cpu ().numpy ().astype (np .float32 )
119+ logits = self ._session .run (None , {self ._input_name : arr })[0 ]
120+ return torch .from_numpy (np .ascontiguousarray (logits ))
121+
122+
123+ def _load_model (analysis_type : str = "deforestation" ) -> tuple [Any , torch .device ]:
71124 """Load (or return cached) U-Net model configured for the analysis type."""
72125 if analysis_type in _model_cache :
73126 return _model_cache [analysis_type ]
@@ -104,6 +157,17 @@ def _load_model(analysis_type: str = "deforestation") -> tuple[UNet, torch.devic
104157 checkpoint .get ("val_iou" , 0.0 ),
105158 )
106159 else :
160+ onnx_path = _find_onnx (analysis_type )
161+ onnx_model = _try_load_onnx (onnx_path , n_channels , n_classes ) if onnx_path else None
162+ if onnx_model is not None :
163+ logger .info (
164+ "Loaded %s model from ONNX %s (%d channels, %d classes)." ,
165+ analysis_type , onnx_path , n_channels , n_classes ,
166+ )
167+ onnx_model .eval ()
168+ _model_cache [analysis_type ] = (onnx_model , device )
169+ return onnx_model , device
170+
107171 logger .warning (
108172 "No trained model found for %s under %s — using untrained weights (demo)." ,
109173 analysis_type ,
@@ -117,6 +181,28 @@ def _load_model(analysis_type: str = "deforestation") -> tuple[UNet, torch.devic
117181 return model , device
118182
119183
184+ def _try_load_onnx (
185+ onnx_path : Path , n_channels : int , n_classes : int
186+ ) -> Optional ["_ONNXSegmenter" ]:
187+ """Build an ONNX inference session, or return None if onnxruntime is unavailable."""
188+ try :
189+ import onnxruntime as ort
190+ except ImportError :
191+ logger .warning (
192+ "Found ONNX model at %s but onnxruntime is not installed; "
193+ "add 'onnxruntime' to requirements to serve it. Falling back." ,
194+ onnx_path ,
195+ )
196+ return None
197+
198+ providers = ["CPUExecutionProvider" ]
199+ if "CUDAExecutionProvider" in ort .get_available_providers ():
200+ providers = ["CUDAExecutionProvider" , "CPUExecutionProvider" ]
201+ session = ort .InferenceSession (str (onnx_path ), providers = providers )
202+ input_name = session .get_inputs ()[0 ].name
203+ return _ONNXSegmenter (session , n_channels , n_classes , input_name )
204+
205+
120206# ---------------------------------------------------------------------------
121207# Sentinel-2 normalisation statistics (matches preprocessing.py)
122208# Band order: [Red, Green, Blue, NIR]
0 commit comments