File size: 5,698 Bytes
228add1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
"""
Inference predictor module.
Loads a trained model checkpoint and runs predictions.
"""

import sys
import time
from pathlib import Path
from typing import Optional, Union

import torch
import torch.nn.functional as F
from PIL import Image

PROJECT_ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(PROJECT_ROOT))

from ml.src.inference.preprocess import preprocess_image
from ml.src.data.breed_metadata import BreedMetadataStore


class CattlePredictor:
    """
    End-to-end predictor that loads a model and provides predictions
    with breed metadata enrichment.
    """

    def __init__(
        self,
        checkpoint_path: str | Path,
        class_names: Optional[list[str]] = None,
        metadata_path: Optional[str | Path] = None,
        device: Optional[torch.device] = None,
        img_size: int = 224,
    ):
        self.img_size = img_size
        self.device = device or torch.device('cpu')

        # Load checkpoint
        checkpoint = torch.load(checkpoint_path, map_location=self.device, weights_only=False)

        # Get class names
        if class_names:
            self.class_names = class_names
        elif 'class_names' in checkpoint:
            self.class_names = checkpoint['class_names']
        else:
            raise ValueError("class_names must be provided or stored in checkpoint")

        # Load model
        self.model = self._load_model(checkpoint)
        self.model.eval()

        # Load breed metadata
        self.metadata_store = None
        if metadata_path:
            try:
                self.metadata_store = BreedMetadataStore(metadata_path)
            except Exception as e:
                print(f"Warning: Could not load breed metadata: {e}")

    def _load_model(self, checkpoint: dict) -> torch.nn.Module:
        """
        Load model from checkpoint. Supports both full model and state_dict.
        """
        if 'model_state_dict' in checkpoint:
            # Need to recreate the model architecture
            model_name = checkpoint.get('model_name', 'resnet')
            num_classes = len(self.class_names)

            model = self._create_model(model_name, num_classes)
            model.load_state_dict(checkpoint['model_state_dict'])
        elif isinstance(checkpoint, torch.nn.Module):
            model = checkpoint
        else:
            # Assume it's a full model
            model = checkpoint

        return model.to(self.device)

    def _create_model(self, model_name: str, num_classes: int) -> torch.nn.Module:
        """Create model architecture by name."""
        if model_name == 'mlp':
            from ml.src.models.mlp import CattleMLP
            return CattleMLP(num_classes=num_classes)
        elif model_name == 'cnn':
            from ml.src.models.cnn import CattleCNN
            return CattleCNN(num_classes=num_classes)
        elif model_name in ('resnet', 'resnet50'):
            from ml.src.models.resnet import CattleResNet
            return CattleResNet(num_classes=num_classes, pretrained=False, freeze_backbone=False)
        elif model_name in ('vit', 'vit_base'):
            from ml.src.models.vit import CattleViT
            return CattleViT(num_classes=num_classes, pretrained=False, freeze_backbone=False)
        else:
            raise ValueError(f"Unknown model: {model_name}")

    @torch.no_grad()
    def predict(
        self,
        image: Union[str, bytes, Image.Image],
        top_k: int = 3,
    ) -> dict:
        """
        Run prediction on a single image.

        Args:
            image: file path, URL, base64 string, bytes, or PIL Image
            top_k: number of top predictions to return

        Returns:
            dict with predicted_breed, confidence, top_k predictions, breed_info, etc.
        """
        start_time = time.time()

        # Preprocess
        tensor, pil_image = preprocess_image(image, self.img_size)
        tensor = tensor.to(self.device)

        # Inference
        outputs = self.model(tensor)
        probabilities = F.softmax(outputs, dim=1).squeeze()

        # Get top-k
        top_k_probs, top_k_indices = torch.topk(probabilities, min(top_k, len(self.class_names)))

        top_k_results = []
        for prob, idx in zip(top_k_probs, top_k_indices):
            breed_name = self.class_names[idx.item()]
            top_k_results.append({
                'breed': breed_name,
                'confidence': round(prob.item(), 4),
            })

        # Best prediction
        best = top_k_results[0]
        result = {
            'predicted_breed': best['breed'],
            'confidence': best['confidence'],
            'top_k': top_k_results,
            'inference_time_ms': round((time.time() - start_time) * 1000, 2),
        }

        # Add breed metadata
        if self.metadata_store:
            breed_info = self.metadata_store.get_summary_for_prediction(best['breed'])
            if breed_info:
                result['breed_info'] = breed_info

        # Add confidence warning
        if best['confidence'] < 0.5:
            result['warning'] = (
                'Prediction confidence is low. '
                'Use a clear side-view image for better results.'
            )
        elif best['confidence'] < 0.75:
            result['warning'] = (
                'Prediction confidence is moderate. '
                'Use a clear side-view image for better results.'
            )

        return result

    def predict_batch(
        self,
        images: list[Union[str, bytes, Image.Image]],
        top_k: int = 3,
    ) -> list[dict]:
        """Run predictions on a batch of images."""
        return [self.predict(img, top_k) for img in images]