File size: 8,900 Bytes
9ab5e32 | 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 | #!/usr/bin/env python3
"""Upscale di un'immagine con lo Student 9x distillato (ONNX) alla scala richiesta.
Lo Student esportato dal notebook Colab e` nativamente 9x (staged upsample 3x3).
Per una scala diversa il modello non viene riaddestrato: si lavora sull'output 9x
* scala < 9 -> inferenza a 9x, poi downsampling d'area sulla scala richiesta;
* scala > 9 -> inferenza a 9x, poi upsampling bicubico.
L'inferenza e` a tile in spazio LR (tile e pad si intendono in pixel LR), come nella
cella "Usare il modello esportato" del notebook. L'eventuale canale alfa dei PNG non
attraversa la rete, che e` a 3 canali: viene ricampionato a parte e riaccodato.
Esempio:
python scripts/upscale_onnx.py a.png --scale 6
"""
from __future__ import annotations
import argparse
import time
from pathlib import Path
import cv2
import numpy as np
import onnxruntime as ort
def build_session(onnx_path: Path, providers_mode: str) -> ort.InferenceSession:
"""Session ONNX con arena disattivata: senza, la RAM cresce a ogni tile.
Con l'inferenza a tile le shape d'input cambiano da tile a tile; il pattern
allocator di ONNX Runtime trattiene i buffer del tile piu` grande e la RSS sale
run dopo run fino a esaurire la memoria (processo ucciso senza traceback).
"""
options = ort.SessionOptions()
options.enable_cpu_mem_arena = False
options.enable_mem_pattern = False
options.log_severity_level = 3
providers = (["CUDAExecutionProvider", "CPUExecutionProvider"]
if providers_mode == "auto" else ["CPUExecutionProvider"])
return ort.InferenceSession(str(onnx_path), sess_options=options, providers=providers)
def load_image(path: Path) -> tuple[np.ndarray, np.ndarray | None]:
"""Carica l'immagine in BGR separando l'alpha, se presente."""
img = cv2.imread(str(path), cv2.IMREAD_UNCHANGED)
if img is None:
raise SystemExit(f"Impossibile leggere {path}")
if img.dtype != np.uint8:
img = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
if img.ndim == 2:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
alpha = None
if img.shape[2] == 4:
img, alpha = img[:, :, :3].copy(), img[:, :, 3].copy()
return img, alpha
def native_scale(session: ort.InferenceSession) -> int:
"""Deduce la scala nativa del modello da una probe 8x9 (evita di fissarla a mano)."""
name = session.get_inputs()[0].name
out = session.run(None, {name: np.zeros((1, 3, 8, 9), dtype=np.float32)})[0]
ratio = max(out.shape[-2] / 8.0, out.shape[-1] / 9.0)
if abs(ratio - round(ratio)) > 1e-6 or ratio < 1:
raise SystemExit(f"Scala nativa non intera: input 8x9 -> output {out.shape}")
return int(round(ratio))
def upscale_tiled(session, bgr: np.ndarray, scale: float, native: int,
tile: int, pad: int) -> np.ndarray:
"""Inferenza a tile in spazio LR; ogni tile viene ricampionato native -> scale."""
input_name = session.get_inputs()[0].name
height, width = bgr.shape[:2]
out_h, out_w = int(round(height * scale)), int(round(width * scale))
canvas = np.zeros((out_h, out_w, 3), dtype=np.uint8)
interp = cv2.INTER_AREA if native > scale else cv2.INTER_CUBIC
tiles = 0
t0 = time.time()
for top in range(0, height, tile):
for left in range(0, width, tile):
bottom, right = min(top + tile, height), min(left + tile, width)
# Tile + bordo di padding: il padding viene usato dall'interpolazione e
# poi tagliato, cosi` le giunzioni tra tile non restano visibili.
y0, y1 = max(top - pad, 0), min(bottom + pad, height)
x0, x1 = max(left - pad, 0), min(right + pad, width)
patch = bgr[y0:y1, x0:x1]
tensor = (cv2.cvtColor(patch, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0)
tensor = np.ascontiguousarray(tensor.transpose(2, 0, 1)[None, :, :, :])
sr = np.clip(session.run(None, {input_name: tensor})[0][0], 0.0, 1.0)
sr = cv2.cvtColor((sr.transpose(1, 2, 0) * 255.0).round().astype(np.uint8),
cv2.COLOR_RGB2BGR)
# Il tile paddato va ricampionato alla SUA dimensione, non a quella del
# ritaglio interno: cosi` l'offset del ritaglio resta dentro i confini.
full_h, full_w = int(round((y1 - y0) * scale)), int(round((x1 - x0) * scale))
sr = cv2.resize(sr, (full_w, full_h), interpolation=interp)
th, tw = int(round((bottom - top) * scale)), int(round((right - left) * scale))
oy = min(int(round((top - y0) * scale)), max(0, full_h - th))
ox = min(int(round((left - x0) * scale)), max(0, full_w - tw))
th = min(th, full_h - oy)
tw = min(tw, full_w - ox)
oy_px, ox_px = int(round(top * scale)), int(round(left * scale))
th = min(th, out_h - oy_px)
tw = min(tw, out_w - ox_px)
canvas[oy_px:oy_px + th, ox_px:ox_px + tw] = sr[oy:oy + th, ox:ox + tw]
tiles += 1
print(f" tile {tiles}: LR {y1 - y0}x{x1 - x0} -> {th}x{tw} "
f"[{time.time() - t0:.1f}s]", flush=True)
return canvas
def psnr(a: np.ndarray, b: np.ndarray) -> float:
mse = float(np.mean((a.astype(np.float64) - b.astype(np.float64)) ** 2))
return 99.0 if mse == 0 else float(10.0 * np.log10(255.0 ** 2 / mse))
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("input", help="immagine da ingrandire")
ap.add_argument("--onnx", default="student_sr9x.onnx", help="ONNX dello Student distillato")
ap.add_argument("--scale", type=float, default=6.0, help="scala richiesta (il modello e` 9x)")
ap.add_argument("--output", default=None, help="percorso output (default output_<nome>_<scala>x.png)")
ap.add_argument("--tile", type=int, default=64, help="lato tile in pixel LR (0 = immagine intera)")
ap.add_argument("--pad", type=int, default=6, help="overlap di padding in pixel LR")
ap.add_argument("--alpha-interp", choices=["nearest", "linear"], default="nearest",
help="ricampionamento dell'alpha (nearest evita aloni sui bordi)")
ap.add_argument("--no-baseline", action="store_true", help="non generare il confronto bicubico")
ap.add_argument("--providers", choices=["auto", "cpu"], default="auto")
args = ap.parse_args()
src = Path(args.input)
if not src.is_file():
raise SystemExit(f"File non trovato: {src}")
onnx_path = Path(args.onnx)
if not onnx_path.is_file():
raise SystemExit(f"ONNX non trovato: {onnx_path}")
if args.scale <= 0:
raise SystemExit("--scale deve essere > 0")
session = build_session(onnx_path, args.providers)
native = native_scale(session)
bgr, alpha = load_image(src)
height, width = bgr.shape[:2]
out_h, out_w = int(round(height * args.scale)), int(round(width * args.scale))
tile = height if args.tile <= 0 else min(int(args.tile), height)
print(f"[modello] {onnx_path} | scala nativa {native}x | tile {tile}+pad {args.pad}")
print(f"[input] {src} {width}x{height}"
+ (" (con alpha)" if alpha is not None else ""))
print(f"[output] {out_w}x{out_h} ({args.scale}x)")
result = upscale_tiled(session, bgr, args.scale, native, tile, int(args.pad))
out_path = Path(args.output) if args.output else src.with_name(
f"output_{src.stem}_{args.scale:g}x.png")
if alpha is not None:
interp = cv2.INTER_NEAREST if args.alpha_interp == "nearest" else cv2.INTER_LINEAR
alpha_out = cv2.resize(alpha, (out_w, out_h), interpolation=interp)
result = np.dstack([result, alpha_out]) # BGR + alpha = BGRA, da salvare cosi` com'e`
if not cv2.imwrite(str(out_path), result):
raise SystemExit(f"Scrittura fallita: {out_path}")
print(f"[salvato] {out_path}")
if not args.no_baseline:
interp = cv2.INTER_AREA if native > args.scale else cv2.INTER_CUBIC
bicubic = cv2.resize(bgr, (out_w, out_h), interpolation=interp)
bic_path = out_path.with_name(f"{out_path.stem}_bicubic{out_path.suffix}")
if alpha is not None:
bicubic = np.dstack([bicubic, cv2.resize(alpha, (out_w, out_h),
interpolation=cv2.INTER_NEAREST)])
cv2.imwrite(str(bic_path), bicubic)
print(f"[baseline] {bic_path} | PSNR modello vs bicubica "
f"{psnr(result if result.shape[2] == 3 else result[:, :, :3], bicubic[:, :, :3]):.2f} dB")
if __name__ == "__main__":
main() |