Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions perceptionmetrics/models/torch_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,22 +236,24 @@ def __init__(

# Load model from file or use passed instance
if isinstance(model, str):
assert os.path.isfile(model), "Torch model file not found"
if not os.path.isfile(model):
raise FileNotFoundError(f"Model file not found: {model}")
model_fname = model
try:
model = torch.jit.load(model, map_location=self.device)
model_type = "compiled"
except Exception:
except (RuntimeError, EOFError) as jit_err:
# TorchScript load failed, try loading as native PyTorch
try:
loaded = torch.load(model, map_location=self.device, weights_only=False)
# Handle Ultralytics/YOLO-style dict checkpoints
if isinstance(loaded, dict):
candidate = loaded.get("ema") or loaded.get("model")
if candidate is None or not hasattr(candidate, "forward"):
raise ValueError(
"""
The loaded .pt file is a dictionary but doesn't contain a valid model under keys 'model' or 'ema'. Please export to TorchScript for better compatibility.
"""
"The loaded .pt file is a dictionary but doesn't contain a "
"valid model under keys 'model' or 'ema'. Please export to "
"TorchScript for better compatibility."
)
model = candidate
else:
Expand All @@ -260,11 +262,22 @@ def __init__(
# Fallback for missing Ultralytics dependency
except (ModuleNotFoundError, AttributeError) as e:
raise ImportError(
f"Failed to load native .pt model. This often happens if the 'ultralytics' "
f"library is missing or incompatible. \nOriginal error: {e}\n"
f"SUGGESTION: 'pip install ultralytics' or export your model to TorchScript."
f"Failed to load native .pt model. This often happens if the "
f"'ultralytics' library is missing or incompatible.\n"
f"Original error: {e}\n"
f"SUGGESTION: 'pip install ultralytics' or export your model to "
f"TorchScript."
) from e

except Exception as load_err:
raise RuntimeError(
f"Failed to load model as TorchScript or PyTorch module. "
f"TorchScript error: {jit_err}. PyTorch error: {load_err}"
) from load_err
elif isinstance(model, torch.nn.Module):
model_fname = None
model_type = "native"
else:
raise ValueError("Model must be a filename or a torch.nn.Module")
# Init parent class
super().__init__(model, model_type, model_cfg, ontology_fname, model_fname)

Expand Down
33 changes: 23 additions & 10 deletions perceptionmetrics/models/torch_segmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,21 @@ def __init__(

# If 'model' contains a string, check that it is a valid filename and load model
if isinstance(model, str):
assert os.path.isfile(model), "TorchScript Model file not found"
if not os.path.isfile(model):
raise FileNotFoundError(f"Model file not found: {model}")
model_fname = model
try:
model = torch.jit.load(model, map_location=self.device)
model_type = "compiled"
except:
print("Model is not a TorchScript model. Loading as a PyTorch module.")
model = torch.load(model, map_location=self.device)
model_type = "native"
except (RuntimeError, EOFError) as jit_err:
try:
model = torch.load(model, map_location=self.device, weights_only=False)
model_type = "native"
except Exception as load_err:
raise RuntimeError(
f"Failed to load model as TorchScript or PyTorch module. "
f"TorchScript error: {jit_err}. PyTorch error: {load_err}"
) from load_err
# Otherwise, check that it is a PyTorch module
elif isinstance(model, torch.nn.Module):
model_fname = None
Expand Down Expand Up @@ -579,15 +585,22 @@ def __init__(

# If 'model' contains a string, check that it is a valid filename and load model
if isinstance(model, str):
assert os.path.isfile(model), "TorchScript Model file not found"
if not os.path.isfile(model):
raise FileNotFoundError(f"Model file not found: {model}")
model_fname = model
try:
model = torch.jit.load(model, map_location=self.device)
model_type = "compiled"
except Exception:
print("Model is not a TorchScript model. Loading as a PyTorch module.")
model = torch.load(model, map_location=self.device)
model_type = "native"
except (RuntimeError, EOFError) as jit_err:
# TorchScript load failed, try loading as native PyTorch
try:
model = torch.load(model, map_location=self.device, weights_only=False)
model_type = "native"
except Exception as load_err:
raise RuntimeError(
f"Failed to load model as TorchScript or PyTorch module. "
f"TorchScript error: {jit_err}. PyTorch error: {load_err}"
) from load_err

# Otherwise, check that it is a PyTorch module
elif isinstance(model, torch.nn.Module):
Expand Down
4 changes: 2 additions & 2 deletions perceptionmetrics/models/utils/o3d/kpconv.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

try:
from open3d._ml3d.torch.models.kpconv import batch_grid_subsampling, batch_neighbors
except Exception:
print("Open3D-ML3D not available")
except ImportError:
print("Warning: Open3D-ML3D not available. KPConv functionality will be limited.")
import torch

import perceptionmetrics.utils.lidar as ul
Expand Down
4 changes: 2 additions & 2 deletions perceptionmetrics/models/utils/o3d/randlanet.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

try:
from open3d._ml3d.datasets.utils import DataProcessing
except Exception:
print("Open3D-ML3D not available")
except ImportError:
print("Warning: Open3D-ML3D not available. RandLANet functionality will be limited.")
import torch

import perceptionmetrics.utils.lidar as ul
Expand Down
100 changes: 100 additions & 0 deletions tests/test_model_loading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Unit tests for model loading robustness"""
import os
import tempfile
import pytest

torch = pytest.importorskip("torch", reason="torch not installed in this environment")


from perceptionmetrics.models.torch_detection import TorchImageDetectionModel
from perceptionmetrics.models.torch_segmentation import TorchImageSegmentationModel


class TestModelLoadingExceptions:
"""Test that model loading raises appropriate exceptions"""

@pytest.fixture
def temp_files(self):
"""Create temporary test files"""
with tempfile.TemporaryDirectory() as tmpdir:
# Create dummy model file (corrupted)
bad_model = os.path.join(tmpdir, "bad_model.pt")
with open(bad_model, "w") as f:
f.write("this is not a pytorch model")

# Create dummy ontology
ontology = os.path.join(tmpdir, "ontology.json")
import json
with open(ontology, "w") as f:
json.dump({
"car": {"idx": 0, "rgb": [0, 0, 0]},
"person": {"idx": 1, "rgb": [255, 0, 0]},
}, f)

# Create dummy config
config = os.path.join(tmpdir, "config.json")
with open(config, "w") as f:
json.dump({
"resize": {"width": 512, "height": 512},
"normalization": {
"mean": [0.485, 0.456, 0.406],
"std": [0.229, 0.224, 0.225]
},
"batch_size": 1,
"model_format": "torchvision"
}, f)

yield {
"bad_model": bad_model,
"ontology": ontology,
"config": config,
"tmpdir": tmpdir
}

def test_detection_model_bad_file_raises_specific_error(self, temp_files):
"""Test that loading corrupted model raises RuntimeError, not generic Exception"""
with pytest.raises(RuntimeError) as exc_info:
TorchImageDetectionModel(
model=temp_files["bad_model"],
model_cfg=temp_files["config"],
ontology_fname=temp_files["ontology"]
)

# Check error message is informative
error_msg = str(exc_info.value)
assert "Failed to load model" in error_msg
assert "TorchScript error" in error_msg or "PyTorch error" in error_msg

def test_segmentation_model_bad_file_raises_specific_error(self, temp_files):
"""Test that loading corrupted segmentation model raises RuntimeError"""
with pytest.raises(RuntimeError) as exc_info:
TorchImageSegmentationModel(
model=temp_files["bad_model"],
model_cfg=temp_files["config"],
ontology_fname=temp_files["ontology"]
)

error_msg = str(exc_info.value)
assert "Failed to load model" in error_msg

def test_detection_model_missing_file_raises_file_not_found(self, temp_files):
"""Test that missing model file raises FileNotFoundError"""
with pytest.raises(FileNotFoundError) as exc_info:
TorchImageDetectionModel(
model="/nonexistent/path/model.pt",
model_cfg=temp_files["config"],
ontology_fname=temp_files["ontology"]
)

assert "Model file not found" in str(exc_info.value)

def test_segmentation_model_missing_file_raises_file_not_found(self, temp_files):
"""Test that missing segmentation model file raises FileNotFoundError"""
with pytest.raises(FileNotFoundError) as exc_info:
TorchImageSegmentationModel(
model="/nonexistent/path/model.pt",
model_cfg=temp_files["config"],
ontology_fname=temp_files["ontology"]
)

assert "Model file not found" in str(exc_info.value)
Loading