aorabdel's picture
Sync model repo (text/metadata)
15070a2 verified
Raw History Blame Contribute Delete
7.23 kB
"""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()