File size: 7,228 Bytes
2bf1f04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6742a66
2bf1f04
2a590a8
2bf1f04
 
 
 
 
 
 
 
15070a2
 
 
 
 
 
6742a66
2bf1f04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6742a66
 
 
 
 
 
 
 
2bf1f04
 
 
 
 
 
 
 
 
 
 
 
 
15070a2
2bf1f04
 
 
 
 
 
 
 
 
 
 
 
 
d38c21b
 
 
 
 
 
 
2bf1f04
d38c21b
6742a66
 
 
15070a2
6742a66
 
 
2bf1f04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6742a66
 
 
2bf1f04
 
 
 
6742a66
 
 
 
 
 
2bf1f04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15070a2
2bf1f04
 
 
 
6742a66
 
 
2bf1f04
 
 
 
 
 
 
 
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
179
180
181
182
183
184
185
186
"""Minimal inference example for microsoft/resnet-18 INT8 using ExecuTorch.

Loads a quantized .pte model and runs classification inference on a single image.
Saves top-5 predictions to predictions.json and an annotated sample_output.jpg
in the same directory as this script.
"""

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-18_raspberry_executorch_optimized.pte"
IMAGE_PATH = "sample_input.jpg"
INPUT_SIZE = (224, 224)
TOP_K = 5

# ImageNet normalization constants (mean and std per channel)
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."""
    script_dir = Path(__file__).parent
    runtime = Runtime.get()
    program = runtime.load_program(str(script_dir / pte_path))
    return program.load_method("forward")


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

    Pipeline: resize to 256 px (shortest edge) -> center crop to 224x224
    -> convert to [0, 1] float32 tensor -> normalize with ImageNet mean/std.
    """
    script_dir = Path(__file__).parent
    image = Image.open(script_dir / image_path).convert("RGB")

    transform = transforms.Compose(
        [
            transforms.Resize(256),
            transforms.CenterCrop(INPUT_SIZE),
            transforms.ToTensor(),
            transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
        ]
    )

    input_tensor = transform(image).unsqueeze(0)  # [1, 3, 224, 224]
    return input_tensor


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


# ── Postprocessing ─────────────────────────────────────────────────────────────
def postprocess(raw_output: torch.Tensor, labels: dict[str, str]) -> list[dict]:
    """Convert raw logits to top-k class predictions with probabilities.

    Applies softmax to get class probabilities, then returns the top-5
    predictions as a list of {class_id, class_name, probability} dicts.
    """
    # Ensure output is 2D [1, 1000]
    if raw_output.dim() == 1:
        raw_output = raw_output.unsqueeze(0)

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

    # 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[0].tolist(), top_indices[0].tolist(), strict=False),
        key=lambda item: (-item[0], item[1]),
    )

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


# ── Save Results ───────────────────────────────────────────────────────────────
def save_results(results: list[dict]) -> None:
    """Save top-k predictions to predictions.json in the script's directory."""
    script_dir = Path(__file__).parent
    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_sample_output(image_path: str, predictions: list[dict]) -> None:
    """Overlay top-5 predictions on the input image and save as sample_output.jpg."""
    script_dir = Path(__file__).parent
    image = Image.open(script_dir / image_path).convert("RGB")
    draw = ImageDraw.Draw(image)

    try:
        font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", size=18)
        small_font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", size=15)
    except OSError:
        font = ImageFont.load_default()
        small_font = font

    # Semi-transparent dark banner at the top
    banner_h = 30 + len(predictions) * 22
    draw.rectangle([(0, 0), (image.width, banner_h)], fill=(0, 0, 0, 180))

    draw.text(
        (10, 6), "Top-5 predictions (ResNet-18 INT8)", font=font, fill=(255, 255, 255)
    )
    for i, pred in enumerate(predictions):
        label = pred["class_name"].split(",")[0]
        pct = pred["probability"] * 100
        y = 30 + i * 22
        draw.text(
            (10, y),
            f"{i + 1}. {label}  {pct:.1f}%",
            font=small_font,
            fill=(255, 220, 50),
        )

    out_path = script_dir / "sample_output.jpg"
    image.save(out_path, quality=95)
    print(f"Sample output saved to {out_path}")


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

    # Load model
    print(f"Loading model: {MODEL_PATH}")
    method = load_model(MODEL_PATH)

    # Preprocess input image
    print(f"Preprocessing image: {IMAGE_PATH}")
    input_tensor = preprocess(IMAGE_PATH)

    # Run inference
    print("Running inference...")
    raw_output = run_inference(method, input_tensor)

    # Postprocess outputs
    results = postprocess(raw_output, IMAGENET_CLASSES)

    # Print top-k predictions
    print(f"\nTop-{TOP_K} predictions:")
    for i, pred in enumerate(results, 1):
        print(
            f"  {i}. {pred['class_name']} ({pred['class_id']}): {pred['probability']:.4f}"
        )

    # Save predictions JSON and annotated output image
    save_results(results)
    save_sample_output(IMAGE_PATH, results)


if __name__ == "__main__":
    main()