File size: 6,737 Bytes
b4ed648 4cc023c b4ed648 1602bca b4ed648 4cc023c b4ed648 4cc023c b4ed648 4cc023c b4ed648 | 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 | """Minimal inference example for ESRGAN 4x Super-Resolution using ExecuTorch.
Loads a quantized .pte model and runs super-resolution inference on a single image.
The model upscales the input image by 4x. For images larger than 128x128, the
input is automatically split into overlapping tiles, each tile is super-resolved,
and the results are blended and stitched into the final output.
"""
import argparse
import json
from pathlib import Path
import numpy as np
import torch
from executorch.runtime import Runtime
from PIL import Image
from torchvision import transforms
# ββ Configuration ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
MODEL_PATH = "esrgan_x4_graviton_executorch_optimized.pte"
IMAGE_PATH = "sample_input.jpg"
TILE_SIZE = 128 # model was exported with 128x128 input tiles
SCALE = 4 # 4x upscaling factor
TILE_OVERLAP = 8 # pixel overlap between tiles for seamless blending
# ββ 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) -> tuple[torch.Tensor, tuple[int, int]]:
"""Load and preprocess image for model input.
The model expects [0, 1] float32 RGB tensors β no normalization is applied.
Returns the input tensor and the original image size for reference.
"""
image = Image.open(image_path).convert("RGB")
original_size = (image.width, image.height)
to_tensor = transforms.ToTensor() # converts [0-255] uint8 -> [0, 1] float32
input_tensor = to_tensor(image).unsqueeze(0) # [1, 3, H, W]
return input_tensor, original_size
# ββ Tiled Inference ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def run_tiled_inference(method, input_tensor: torch.Tensor) -> torch.Tensor:
"""Run super-resolution inference with overlapping tile stitching.
Splits the input into overlapping 128x128 tiles, runs each through the
model, and blends overlapping regions using pixel-level averaging.
"""
_, _, h, w = input_tensor.shape
out_h, out_w = h * SCALE, w * SCALE
output = torch.zeros(1, 3, out_h, out_w)
weights = torch.zeros(1, 1, out_h, out_w)
stride = TILE_SIZE - TILE_OVERLAP
y_positions = list(range(0, max(1, h - TILE_SIZE + 1), stride))
if not y_positions or y_positions[-1] + TILE_SIZE < h:
y_positions.append(max(0, h - TILE_SIZE))
x_positions = list(range(0, max(1, w - TILE_SIZE + 1), stride))
if not x_positions or x_positions[-1] + TILE_SIZE < w:
x_positions.append(max(0, w - TILE_SIZE))
for y in y_positions:
for x in x_positions:
tile = input_tensor[:, :, y:y + TILE_SIZE, x:x + TILE_SIZE].contiguous()
sr_tile = method.execute([tile])[0]
oy, ox = y * SCALE, x * SCALE
oh, ow = TILE_SIZE * SCALE, TILE_SIZE * SCALE
output[:, :, oy:oy + oh, ox:ox + ow] += sr_tile
weights[:, :, oy:oy + oh, ox:ox + ow] += 1.0
return output / weights.clamp(min=1.0)
# ββ Postprocessing βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def postprocess(raw_output: torch.Tensor) -> np.ndarray:
"""Clamp output to [0, 1] and convert to uint8 numpy array (H, W, 3)."""
sr_tensor = raw_output.clamp(0.0, 1.0).squeeze(0) # [3, H*4, W*4]
sr_array = (sr_tensor.permute(1, 2, 0).numpy() * 255.0).round().astype(np.uint8)
return sr_array
# ββ Save Results ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def save_results(
sr_array: np.ndarray,
original_size: tuple[int, int],
output_path: Path | None,
summary_output_path: Path | None,
) -> None:
"""Save explicitly requested inference outputs."""
if output_path:
Image.fromarray(sr_array).save(output_path)
print(f"Super-resolved image saved to: {output_path}")
if summary_output_path:
summary = {
"input_size": {"width": original_size[0], "height": original_size[1]},
"output_size": {"width": sr_array.shape[1], "height": sr_array.shape[0]},
"scale_factor": SCALE,
}
with summary_output_path.open("w") as f:
json.dump(summary, f, indent=2)
print(f"Summary saved to: {summary_output_path}")
# ββ Main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, help="Path for the super-resolved PNG")
parser.add_argument("--summary-output", type=Path, help="Path for the JSON summary")
args = parser.parse_args()
script_dir = Path(__file__).resolve().parent
model_path = script_dir / MODEL_PATH
image_path = script_dir / IMAGE_PATH
print(f"Loading model from: {model_path}")
method = load_model(str(model_path))
print(f"Preprocessing image: {image_path}")
input_tensor, original_size = preprocess(str(image_path))
_, _, h, w = input_tensor.shape
print(f" Input size: {w}x{h} -> output will be {w * SCALE}x{h * SCALE}")
print("Running tiled super-resolution inference...")
raw_output = run_tiled_inference(method, input_tensor)
sr_array = postprocess(raw_output)
print(f" Output shape: {sr_array.shape[1]}x{sr_array.shape[0]} (WxH)")
save_results(sr_array, original_size, args.output, args.summary_output)
if not args.output and not args.summary_output:
print("No output files written; use --output or --summary-output to save results.")
if __name__ == "__main__":
main()
|