File size: 6,995 Bytes
0bcf514
 
 
 
 
 
 
 
 
 
 
 
 
 
8640453
0bcf514
ebdf21a
0bcf514
 
 
 
 
 
 
 
067cfd6
 
ebdf21a
067cfd6
 
ebdf21a
0bcf514
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8640453
 
 
 
 
 
 
 
0bcf514
 
 
 
 
 
 
 
 
 
 
067cfd6
0bcf514
 
 
 
 
 
 
 
 
 
 
 
 
 
ef7a0dc
 
 
 
 
 
 
0bcf514
ef7a0dc
067cfd6
8640453
 
 
0bcf514
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8640453
 
 
0bcf514
 
 
 
 
 
 
 
 
 
 
8640453
 
 
 
0bcf514
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ebdf21a
0bcf514
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
172
173
174
175
176
177
178
"""Minimal inference example for microsoft/resnet-50 INT8 using ExecuTorch.

Loads a quantized .pte model and runs image classification on a single image.
Outputs top-5 predictions with class labels and probabilities.
"""

import json
from pathlib import Path

import torch
from executorch.runtime import Runtime
from PIL import Image, ImageDraw, ImageFont
from torchvision import transforms


# ── Configuration ──────────────────────────────────────────────────────────────
MODEL_PATH = "resnet-50_raspberry_executorch_optimized.pte"
IMAGE_PATH = "sample_input.jpg"
INPUT_SIZE = (224, 224)
TOP_K = 5

# ImageNet normalization constants
IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]

SCRIPT_DIR = Path(__file__).resolve().parent

# ImageNet class labels (1000 classes)
with (SCRIPT_DIR / "imagenet_classes.json").open(encoding="utf-8") as file:
    IMAGENET_CLASSES = json.load(file)


# ── Model Loading ──────────────────────────────────────────────────────────────
def load_model(pte_path: str):
    """Load ExecuTorch .pte model and return the forward method."""
    runtime = Runtime.get()
    program = runtime.load_program(pte_path)
    return program.load_method("forward")


# ── Preprocessing ──────────────────────────────────────────────────────────────
def preprocess(image_path: str) -> torch.Tensor:
    """Load and preprocess image for model input.

    Resizes to 256, center-crops to 224x224, converts to float32 tensor in
    [0, 1], then applies ImageNet mean/std normalization.
    """
    image = Image.open(image_path).convert("RGB")
    transform = transforms.Compose(
        [
            transforms.Resize(256),
            transforms.CenterCrop(INPUT_SIZE),
            transforms.ToTensor(),
            transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
        ]
    )
    return transform(image).unsqueeze(0)  # [1, 3, 224, 224]


# ── Inference ──────────────────────────────────────────────────────────────────
def run_inference(method, input_tensor: torch.Tensor) -> torch.Tensor:
    """Run forward pass and return raw logits tensor."""
    outputs = method.execute([input_tensor])
    return outputs[0]  # [1, 1000]


# ── Postprocessing ─────────────────────────────────────────────────────────────
def postprocess(raw_output: torch.Tensor, labels: dict[str, str], top_k: int = TOP_K) -> list:
    """Decode raw logits into top-k class predictions with probabilities.

    Applies softmax to convert logits to probabilities, then returns the
    top-k classes sorted by descending probability.
    """
    # Ensure 2D: [1, 1000] β†’ [1000]
    logits = raw_output
    if logits.dim() > 1:
        logits = logits.squeeze(0)

    probabilities = torch.softmax(logits, dim=-1)
    k = min(top_k, logits.shape[-1])
    top_scores, top_indices = probabilities.topk(k)

    # Quantized logits land on a discrete grid, so exact ties are common and
    # topk orders them arbitrarily. Break ties by class id for a stable result.
    ranked = sorted(
        zip(top_scores.tolist(), top_indices.tolist(), strict=False),
        key=lambda item: (-item[0], item[1]),
    )

    results = []
    for score, idx in ranked:
        class_label = labels[str(idx)]
        results.append(
            {"class_index": idx, "class": class_label, "probability": round(score, 6)}
        )
    return results


# ── Save Results ───────────────────────────────────────────────────────────────
def save_results(results: list, script_dir: Path) -> None:
    """Save top-k predictions as predictions.json in the script directory."""
    output_path = script_dir / "predictions.json"
    with open(output_path, "w") as f:
        json.dump(results, f, indent=2)
    print(f"Predictions saved to: {output_path}")


def save_output_image(image_path: str, top_prediction: dict, script_dir: Path) -> None:
    """Overlay the top prediction label on the original image and save as sample_output.jpg."""
    image = Image.open(image_path).convert("RGB")
    draw = ImageDraw.Draw(image)

    label = f"{top_prediction['class']}: {top_prediction['probability'] * 100:.1f}%"

    font_size = max(16, image.width // 20)
    font: ImageFont.ImageFont | ImageFont.FreeTypeFont
    try:
        font = ImageFont.truetype(
            "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", font_size
        )
    except OSError:
        try:
            font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", font_size)
        except OSError:
            font = ImageFont.load_default()

    bbox = draw.textbbox((0, 0), label, font=font)
    text_w, text_h = bbox[2] - bbox[0], bbox[3] - bbox[1]
    padding = 8
    x, y = padding, image.height - text_h - padding * 3

    draw.rectangle(
        [x - padding, y - padding, x + text_w + padding, y + text_h + padding],
        fill=(0, 0, 0, 180),
    )
    draw.text((x, y), label, fill=(255, 255, 255), font=font)

    output_path = script_dir / "sample_output.jpg"
    image.save(output_path, quality=95)
    print(f"Output image saved to: {output_path}")


# ── Main ───────────────────────────────────────────────────────────────────────
def main() -> None:
    script_dir = Path(__file__).parent

    # Resolve paths relative to script location
    model_path = script_dir / MODEL_PATH
    image_path = script_dir / IMAGE_PATH

    # Load model
    method = load_model(str(model_path))

    # Preprocess input image
    input_tensor = preprocess(str(image_path))

    # Run inference
    raw_output = run_inference(method, input_tensor)

    # Decode predictions
    results = postprocess(raw_output, IMAGENET_CLASSES)

    # Print top-k predictions
    print(f"\nTop-{TOP_K} predictions for: {image_path.name}")
    print("-" * 50)
    for rank, pred in enumerate(results, 1):
        print(f"  {rank}. {pred['class']:<40} {pred['probability']*100:.2f}%")

    # Save predictions.json
    save_results(results, script_dir)

    # Save labeled output image
    save_output_image(str(image_path), results[0], script_dir)


if __name__ == "__main__":
    main()