Benchmarking#
This example benchmarks several 2D downsampling methods over a set of test images. For each image we:
Downsample by an image-specific zoom factor, then upsample back (round-trip).
Measure the runtime of the round-trip (forward + backward).
Compute SNR / MSE / SSIM on a local ROI.
Visualise the results with ROI-aware zooms: first a 2x2 figure showing the original image with its magnified ROI and the SplineOps Antialiasing first-pass image with the mapped ROI, then ROI montages of the original ROI and each method’s first-pass ROI, all magnified with nearest-neighbour.
We compare:
SciPy ndimage.zoom (cubic).
SplineOps Standard cubic interpolation.
SplineOps Cubic Antialiasing (oblique projection).
OpenCV INTER_CUBIC.
Pillow BICUBIC.
scikit-image (resize, cubic).
scikit-image (resize, cubic + anti_aliasing).
PyTorch (F.interpolate bicubic, CPU).
PyTorch (F.interpolate bicubic, antialias=True, CPU).
Notes#
All ops run on grayscale images normalized to [0, 1] for metrics.
Methods with missing deps are marked “unavailable” in the console and skipped from the ROI montages.
Imports and Configuration#
from __future__ import annotations
import math
import os
import sys
import time
from typing import Dict, List, Tuple, Optional
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from urllib.request import urlopen
from PIL import Image
# Default storage dtype for comparison (change to np.float64 if desired)
DTYPE = np.float32
# ROI / detail-window configuration
ROI_MAG_TARGET = 256 # target height for nearest-neighbour zoom tiles
ROI_TILE_TITLE_FONTSIZE = 12
ROI_SUPTITLE_FONTSIZE = 14 # currently unused, kept for consistency
# Plot appearance for slide-friendly export
PLOT_FIGSIZE = (14, 7) # same 2:1 ratio as (10, 5), just larger
PLOT_TITLE_FONTSIZE = 18
PLOT_LABEL_FONTSIZE = 18
PLOT_TICK_FONTSIZE = 18
PLOT_LEGEND_FONTSIZE = 18
# Highlight styles (per method) used everywhere (ROI montages + plots)
HIGHLIGHT_STYLE = {
"SplineOps Standard cubic": {
"color": "#C2410C",
"lw": 3.0,
},
"SplineOps Antialiasing cubic": {
"color": "#BE185D",
"lw": 3.0,
},
}
AA_METHOD_LABEL = "SplineOps Antialiasing cubic"
AA_COLOR = HIGHLIGHT_STYLE[AA_METHOD_LABEL]["color"]
def fmt_ms(seconds: float) -> str:
"""Format seconds as a short 'X.X ms' string."""
return f"{seconds * 1000.0:.1f} ms"
# Benchmark configuration
N_TRIALS = 10
# Optional deps
try:
import cv2
_HAS_CV2 = True
# Undo OpenCV's Qt plugin path override to keep using the system/PyQt plugins
os.environ.pop("QT_QPA_PLATFORM_PLUGIN_PATH", None)
except Exception:
_HAS_CV2 = False
try:
from scipy.ndimage import zoom as _ndi_zoom
_HAS_SCIPY = True
except Exception:
_HAS_SCIPY = False
try:
from skimage.transform import resize as _sk_resize
from skimage.metrics import structural_similarity as _ssim # noqa: F401
_HAS_SKIMAGE = True
except Exception:
_HAS_SKIMAGE = False
_ssim = None
try:
import torch
import torch.nn.functional as F
_HAS_TORCH = True
except Exception:
_HAS_TORCH = False
# SplineOps
try:
from splineops.resize import resize as sp_resize
_HAS_SPLINEOPS = True
except Exception as e:
_HAS_SPLINEOPS = False
_SPLINEOPS_IMPORT_ERR = str(e)
try:
from splineops.utils.specs import print_runtime_context
_HAS_SPECS = True
except Exception:
print_runtime_context = None
_HAS_SPECS = False
Kodak Test Set Configuration#
KODAK_BASE = "https://r0k.us/graphics/kodak/kodak"
KODAK_IMAGES = [
("kodim05", f"{KODAK_BASE}/kodim05.png"),
("kodim07", f"{KODAK_BASE}/kodim07.png"),
("kodim14", f"{KODAK_BASE}/kodim14.png"),
("kodim15", f"{KODAK_BASE}/kodim15.png"),
("kodim19", f"{KODAK_BASE}/kodim19.png"),
("kodim22", f"{KODAK_BASE}/kodim22.png"),
("kodim23", f"{KODAK_BASE}/kodim23.png"),
]
# Per-image zoom + ROI config
IMAGE_CONFIG: Dict[str, Dict[str, object]] = {
"kodim05": dict(
zoom=0.15,
roi_size_px=256,
roi_center_frac=(0.75, 0.5),
),
"kodim07": dict(
zoom=0.15,
roi_size_px=256,
roi_center_frac=(0.40, 0.50),
),
"kodim14": dict(
zoom=0.3,
roi_size_px=256,
roi_center_frac=(0.75, 0.75),
),
"kodim15": dict(
zoom=0.3,
roi_size_px=256,
roi_center_frac=(0.30, 0.55),
),
"kodim19": dict(
zoom=0.2,
roi_size_px=256,
roi_center_frac=(0.65, 0.35),
),
"kodim22": dict(
zoom=0.2,
roi_size_px=256,
roi_center_frac=(0.50, 0.25),
),
"kodim23": dict(
zoom=0.15,
roi_size_px=256,
roi_center_frac=(0.40, 0.65),
),
}
def _load_kodak_gray(url: str) -> np.ndarray:
"""
Download a Kodak image, convert to grayscale [0, 1] in DTYPE (float32).
"""
with urlopen(url, timeout=10) as resp:
img = Image.open(resp)
arr = np.asarray(img, dtype=np.float64)
if arr.ndim == 3 and arr.shape[2] >= 3:
arr01 = arr / 255.0
gray = (
0.2989 * arr01[..., 0]
+ 0.5870 * arr01[..., 1]
+ 0.1140 * arr01[..., 2]
)
else:
vmax = float(arr.max()) or 1.0
gray = arr / vmax
return np.clip(gray, 0.0, 1.0).astype(DTYPE)
def _load_kodak_rgb(url: str) -> np.ndarray:
"""
Download a Kodak image as RGB [0, 1] in DTYPE (float32).
Used only for color visualizations; metrics remain on grayscale.
"""
with urlopen(url, timeout=10) as resp:
img = Image.open(resp).convert("RGB")
arr = np.asarray(img, dtype=np.float64) / 255.0 # H×W×3 in [0,1]
return np.clip(arr, 0.0, 1.0).astype(DTYPE, copy=False)
Utilities (Metrics, ROI, Plotting)#
def _snr_db(x: np.ndarray, y: np.ndarray) -> float:
num = float(np.sum(x * x, dtype=np.float64))
den = float(np.sum((x - y) ** 2, dtype=np.float64))
if den == 0.0:
return float("inf")
if num == 0.0:
return -float("inf")
return 10.0 * math.log10(num / den)
def _roi_rect_from_frac(
shape: Tuple[int, int],
roi_size_px: int,
center_frac: Tuple[float, float],
) -> Tuple[int, int, int, int]:
"""Compute a square ROI inside an image, centred at fractional coordinates."""
H, W = shape[:2]
row_frac, col_frac = center_frac
size = int(min(roi_size_px, H, W))
if size < 1:
size = min(H, W)
center_r = int(round(row_frac * H))
center_c = int(round(col_frac * W))
row_top = int(np.clip(center_r - size // 2, 0, H - size))
col_left = int(np.clip(center_c - size // 2, 0, W - size))
return row_top, col_left, size, size
def _crop_roi(arr: np.ndarray, rect: Tuple[int, int, int, int]) -> np.ndarray:
r0, c0, h, w = rect
return arr[r0 : r0 + h, c0 : c0 + w]
def _nearest_big(roi: np.ndarray, target_h: int = ROI_MAG_TARGET) -> np.ndarray:
"""Enlarge a small ROI with nearest-neighbour so its height is ~target_h."""
h, w = roi.shape
mag = max(1, int(round(target_h / max(h, 1))))
return np.repeat(np.repeat(roi, mag, axis=0), mag, axis=1)
def _diff_normalized(
orig: np.ndarray,
rec: np.ndarray,
*,
max_abs: Optional[float] = None,
) -> np.ndarray:
"""
Normalize signed difference (rec - orig) into [0,1] for display.
0.5 = no difference, >0.5 positive, <0.5 negative.
If max_abs is provided, it is used as a shared scale (same range across tiles).
"""
diff = rec.astype(np.float64, copy=False) - orig.astype(np.float64, copy=False)
if max_abs is None:
max_abs = float(np.max(np.abs(diff)))
else:
max_abs = float(max_abs)
if max_abs <= 0.0:
return 0.5 * np.ones_like(diff, dtype=DTYPE)
norm = 0.5 + 0.5 * diff / max_abs
norm = np.clip(norm, 0.0, 1.0)
return norm.astype(DTYPE, copy=False)
def _show_initial_original_vs_aa(
gray: np.ndarray,
roi_rect: Tuple[int, int, int, int],
aa_first: np.ndarray,
z: float,
degree_label: str,
) -> None:
"""
Plot a 2x2 figure:
Row 1:
- Original image with red ROI box
- Magnified original ROI
Row 2:
- SplineOps Antialiasing first-pass resized image on white canvas with
mapped ROI box
- Magnified mapped ROI from the Antialiasing first-pass
"""
H, W = gray.shape
row0, col0, roi_h, roi_w = roi_rect
roi_orig = _crop_roi(gray, roi_rect)
roi_orig_big = _nearest_big(roi_orig, ROI_MAG_TARGET)
H1, W1 = aa_first.shape
center_r = row0 + roi_h / 2.0
center_c = col0 + roi_w / 2.0
roi_h_res = max(1, int(round(roi_h * z)))
roi_w_res = max(1, int(round(roi_w * z)))
if roi_h_res > H1 or roi_w_res > W1:
aa_roi = aa_first
row_top_res = 0
col_left_res = 0
roi_h_res = H1
roi_w_res = W1
else:
center_r_res = int(round(center_r * z))
center_c_res = int(round(center_c * z))
row_top_res = int(np.clip(center_r_res - roi_h_res // 2, 0, H1 - roi_h_res))
col_left_res = int(np.clip(center_c_res - roi_w_res // 2, 0, W1 - roi_w_res))
aa_roi = aa_first[
row_top_res : row_top_res + roi_h_res,
col_left_res : col_left_res + roi_w_res,
]
aa_roi_big = _nearest_big(aa_roi, ROI_MAG_TARGET)
canvas_aa = np.ones_like(gray, dtype=aa_first.dtype)
h_copy = min(H, H1)
w_copy = min(W, W1)
canvas_aa[:h_copy, :w_copy] = aa_first[:h_copy, :w_copy]
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
# Row 1, left: original with ROI box
ax = axes[0, 0]
ax.imshow(gray, cmap="gray", interpolation="nearest", aspect="equal")
rect = patches.Rectangle(
(col0, row0),
roi_w,
roi_h,
linewidth=2,
edgecolor="red",
facecolor="none",
)
ax.add_patch(rect)
ax.set_title(
f"Original image with ROI ({H}×{W} px)",
fontsize=ROI_TILE_TITLE_FONTSIZE,
)
ax.axis("off")
# Row 1, right: magnified original ROI
ax = axes[0, 1]
ax.imshow(roi_orig_big, cmap="gray", interpolation="nearest", aspect="equal")
ax.set_title(
f"Original ROI ({roi_h}×{roi_w} px, NN magnified)",
fontsize=ROI_TILE_TITLE_FONTSIZE,
)
ax.axis("off")
# Row 2, left: Antialiasing first-pass on canvas with mapped ROI box
ax = axes[1, 0]
ax.imshow(canvas_aa, cmap="gray", interpolation="nearest", aspect="equal")
if row_top_res < h_copy and col_left_res < w_copy:
box_h = min(roi_h_res, h_copy - row_top_res)
box_w = min(roi_w_res, w_copy - col_left_res)
rect_aa = patches.Rectangle(
(col_left_res, row_top_res),
box_w,
box_h,
linewidth=2,
edgecolor="red",
facecolor="none",
)
ax.add_patch(rect_aa)
ax.set_title(
f"{AA_METHOD_LABEL}\n(zoom ×{z:g}, {H1}×{W1} px)",
fontsize=ROI_TILE_TITLE_FONTSIZE,
color=AA_COLOR,
fontweight="bold",
multialignment="center",
)
ax.axis("off")
# Row 2, right: magnified Antialiasing ROI
ax = axes[1, 1]
ax.imshow(aa_roi_big, cmap="gray", interpolation="nearest", aspect="equal")
ax.set_title(
f"Antialiasing ROI ({roi_h_res}×{roi_w_res} px, NN magnified)",
fontsize=ROI_TILE_TITLE_FONTSIZE,
)
ax.axis("off")
fig.tight_layout()
plt.show()
def _nearest_big_color(roi: np.ndarray, target_h: int = ROI_MAG_TARGET) -> np.ndarray:
"""
Enlarge a small color ROI (H×W×3) with nearest-neighbour so its height
is ~target_h pixels.
"""
h, w, _ = roi.shape
mag = max(1, int(round(target_h / max(h, 1))))
return np.repeat(np.repeat(roi, mag, axis=0), mag, axis=1)
def show_intro_color(
original_rgb: np.ndarray,
shrunk_rgb: np.ndarray,
roi_rect: Tuple[int, int, int, int],
zoom: float,
label: str,
degree_label: str,
) -> None:
"""
2×2 figure mirroring the benchmarking intro style, but in color.
"""
H, W, _ = original_rgb.shape
row0, col0, roi_h, roi_w = roi_rect
roi_orig = original_rgb[row0:row0 + roi_h, col0:col0 + roi_w, :]
roi_orig_big = _nearest_big_color(roi_orig, target_h=ROI_MAG_TARGET)
Hs, Ws, _ = shrunk_rgb.shape
center_r = row0 + roi_h / 2.0
center_c = col0 + roi_w / 2.0
roi_h_res = max(1, int(round(roi_h * zoom)))
roi_w_res = max(1, int(round(roi_w * zoom)))
if roi_h_res > Hs or roi_w_res > Ws:
roi_shrunk = shrunk_rgb
row_top_res = 0
col_left_res = 0
roi_h_res = Hs
roi_w_res = Ws
else:
center_r_res = int(round(center_r * zoom))
center_c_res = int(round(center_c * zoom))
row_top_res = int(np.clip(center_r_res - roi_h_res // 2, 0, Hs - roi_h_res))
col_left_res = int(np.clip(center_c_res - roi_w_res // 2, 0, Ws - roi_w_res))
roi_shrunk = shrunk_rgb[
row_top_res:row_top_res + roi_h_res,
col_left_res:col_left_res + roi_w_res,
:
]
roi_shrunk_big = _nearest_big_color(roi_shrunk, target_h=ROI_MAG_TARGET)
canvas = np.ones_like(original_rgb)
h_copy = min(H, Hs)
w_copy = min(W, Ws)
canvas[:h_copy, :w_copy, :] = shrunk_rgb[:h_copy, :w_copy, :]
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
ax = axes[0, 0]
ax.imshow(np.clip(original_rgb, 0.0, 1.0))
rect = patches.Rectangle(
(col0, row0),
roi_w,
roi_h,
linewidth=2,
edgecolor="red",
facecolor="none",
)
ax.add_patch(rect)
ax.set_title(
f"Original image with ROI ({H}×{W} px)",
fontsize=ROI_TILE_TITLE_FONTSIZE,
)
ax.axis("off")
ax = axes[0, 1]
ax.imshow(np.clip(roi_orig_big, 0.0, 1.0))
ax.set_title(
f"Original ROI ({roi_h}×{roi_w} px, NN magnified)",
fontsize=ROI_TILE_TITLE_FONTSIZE,
)
ax.axis("off")
ax = axes[1, 0]
ax.imshow(np.clip(canvas, 0.0, 1.0))
if row_top_res < h_copy and col_left_res < w_copy:
box_h = min(roi_h_res, h_copy - row_top_res)
box_w = min(roi_w_res, w_copy - col_left_res)
rect2 = patches.Rectangle(
(col_left_res, row_top_res),
box_w,
box_h,
linewidth=2,
edgecolor="red",
facecolor="none",
)
ax.add_patch(rect2)
# Bottom-left: resized image on canvas (change title only here)
if label == "Antialiasing":
title_kw = dict(
fontsize=ROI_TILE_TITLE_FONTSIZE,
fontweight="bold",
color=AA_COLOR,
multialignment="center",
)
ax.set_title(
f"{AA_METHOD_LABEL}\n(zoom ×{zoom:g}, {Hs}×{Ws} px)",
**title_kw,
)
ax.axis("off")
ax = axes[1, 1]
ax.imshow(np.clip(roi_shrunk_big, 0.0, 1.0))
if label == "Antialiasing":
title_kw = dict(fontsize=ROI_TILE_TITLE_FONTSIZE, fontweight="bold")
if AA_COLOR is not None:
title_kw["color"] = AA_COLOR
ax.set_title(
f"{label} ROI ({roi_h_res}×{roi_w_res} px, NN magnified)",
**title_kw,
)
else:
ax.set_title(
f"{label} ROI ({roi_h_res}×{roi_w_res} px, NN magnified)",
fontsize=ROI_TILE_TITLE_FONTSIZE,
)
ax.axis("off")
fig.tight_layout()
plt.show()
def _smart_ylim(
values: np.ndarray,
*,
hi_cap: float | None = None,
lo_cap: float | None = None,
pad_frac: float = 0.06,
iqr_k: float = 1.5,
q_floor: float = 10.0, # percentile used when min is an outlier
min_span: float | None = None,
) -> tuple[float, float] | None:
"""
Robust y-limits for plots.
- If the minimum is a strong outlier (below Q1 - k*IQR), use q_floor percentile as the lower bound.
- Otherwise use the true min.
- Add a small padding.
- Optionally clamp/cap.
"""
v = np.asarray(values, dtype=np.float64)
v = v[np.isfinite(v)]
if v.size == 0:
return None
vmin = float(v.min())
vmax = float(v.max())
if vmin == vmax:
span = float(min_span) if min_span is not None else (1e-3 if vmax <= 1.0 else 1.0)
lo, hi = vmin - 0.5 * span, vmax + 0.5 * span
else:
q1, q3 = np.percentile(v, [25.0, 75.0])
iqr = float(q3 - q1)
lo0 = vmin
if iqr > 0.0:
low_outlier_thr = float(q1 - iqr_k * iqr)
if vmin < low_outlier_thr:
lo0 = float(np.percentile(v, q_floor))
span = vmax - lo0
pad = pad_frac * span
lo, hi = lo0 - pad, vmax + pad
if min_span is not None and (hi - lo) < float(min_span):
mid = 0.5 * (hi + lo)
lo = mid - 0.5 * float(min_span)
hi = mid + 0.5 * float(min_span)
if lo_cap is not None:
lo = max(lo, float(lo_cap))
if hi_cap is not None:
hi = min(hi, float(hi_cap))
if lo >= hi:
hi = lo + (float(min_span) if min_span is not None else 1e-6)
return lo, hi
def _highlight_tile(ax, *, color: str, lw: float = 3.0) -> None:
# Full-axes border, works even with ax.axis("off")
rect = patches.Rectangle(
(0, 0), 1, 1,
transform=ax.transAxes,
fill=False,
edgecolor=color,
linewidth=lw,
clip_on=False,
)
ax.add_patch(rect)
Round-Trip Backends and Time#
def _rt_splineops(
gray: np.ndarray, z: float, preset: str
) -> Tuple[np.ndarray, np.ndarray, Optional[str]]:
"""SplineOps standard / antialiasing cubic."""
if not _HAS_SPLINEOPS:
return gray, gray, f"SplineOps unavailable: {_SPLINEOPS_IMPORT_ERR}"
try:
first = sp_resize(gray, zoom_factors=(z, z), method=preset)
rec = sp_resize(first, output_size=gray.shape, method=preset)
first = np.clip(first, 0.0, 1.0).astype(gray.dtype, copy=False)
rec = np.clip(rec, 0.0, 1.0).astype(gray.dtype, copy=False)
return first, rec, None
except Exception as e:
return gray, gray, str(e)
def _rt_scipy(
gray: np.ndarray, z: float
) -> Tuple[np.ndarray, np.ndarray, Optional[str]]:
"""SciPy ndimage.zoom, cubic, reflect boundary."""
if not _HAS_SCIPY:
return gray, gray, "SciPy not installed"
try:
order = 3
need_prefilter = True
first = _ndi_zoom(
gray,
(z, z),
order=order,
prefilter=need_prefilter,
mode="reflect",
grid_mode=False,
)
Hz, Wz = first.shape
back = (gray.shape[0] / Hz, gray.shape[1] / Wz)
rec = _ndi_zoom(
first,
back,
order=order,
prefilter=need_prefilter,
mode="reflect",
grid_mode=False,
)
first = np.clip(first, 0.0, 1.0)
rec = np.clip(rec, 0.0, 1.0)
if rec.shape != gray.shape:
h = min(rec.shape[0], gray.shape[0])
w = min(rec.shape[1], gray.shape[1])
r0 = (rec.shape[0] - h) // 2
r1 = r0 + h
c0 = (rec.shape[1] - w) // 2
c1 = c0 + w
rc = rec[r0:r1, c0:c1]
g0 = (gray.shape[0] - h) // 2
g1 = g0 + h
g2 = (gray.shape[1] - w) // 2
g3 = g2 + w
tmp = np.zeros_like(gray)
tmp[g0:g1, g2:g3] = rc
rec = tmp
return first.astype(gray.dtype, copy=False), rec.astype(gray.dtype, copy=False), None
except Exception as e:
return gray, gray, str(e)
def _rt_opencv(
gray: np.ndarray, z: float
) -> Tuple[np.ndarray, np.ndarray, Optional[str]]:
"""OpenCV INTER_CUBIC."""
if not _HAS_CV2:
return gray, gray, "OpenCV not installed"
try:
H, W = gray.shape
H1 = int(round(H * z))
W1 = int(round(W * z))
first = cv2.resize(gray, (W1, H1), interpolation=cv2.INTER_CUBIC)
rec = cv2.resize(first, (W, H), interpolation=cv2.INTER_CUBIC)
first = np.clip(first, 0.0, 1.0).astype(gray.dtype, copy=False)
rec = np.clip(rec, 0.0, 1.0).astype(gray.dtype, copy=False)
return first, rec, None
except Exception as e:
return gray, gray, str(e)
def _rt_pillow(
gray: np.ndarray, z: float
) -> Tuple[np.ndarray, np.ndarray, Optional[str]]:
"""Pillow BICUBIC on float32 images."""
try:
from PIL import Image as _Image
H, W = gray.shape
H1 = int(round(H * z))
W1 = int(round(W * z))
im = _Image.fromarray(gray.astype(np.float32, copy=False), mode="F")
first_im = im.resize((W1, H1), resample=_Image.Resampling.BICUBIC)
rec_im = first_im.resize((W, H), resample=_Image.Resampling.BICUBIC)
first = np.asarray(first_im, dtype=np.float32)
rec = np.asarray(rec_im, dtype=np.float32)
first = np.clip(first, 0.0, 1.0).astype(gray.dtype, copy=False)
rec = np.clip(rec, 0.0, 1.0).astype(gray.dtype, copy=False)
return first, rec, None
except Exception as e:
return gray, gray, str(e)
def _rt_skimage(
gray: np.ndarray, z: float
) -> Tuple[np.ndarray, np.ndarray, Optional[str]]:
"""scikit-image resize, cubic, anti_aliasing=False."""
if not _HAS_SKIMAGE:
return gray, gray, "scikit-image not installed"
try:
H, W = gray.shape
H1 = int(round(H * z))
W1 = int(round(W * z))
first = _sk_resize(
gray,
(H1, W1),
order=3,
anti_aliasing=False,
preserve_range=True,
mode="reflect",
).astype(np.float64)
rec = _sk_resize(
first,
(H, W),
order=3,
anti_aliasing=False,
preserve_range=True,
mode="reflect",
).astype(np.float64)
first = np.clip(first, 0.0, 1.0).astype(gray.dtype, copy=False)
rec = np.clip(rec, 0.0, 1.0).astype(gray.dtype, copy=False)
return first, rec, None
except Exception as e:
return gray, gray, str(e)
def _rt_skimage_aa(
gray: np.ndarray, z: float
) -> Tuple[np.ndarray, np.ndarray, Optional[str]]:
"""scikit-image resize, cubic, anti_aliasing=True on shrink."""
if not _HAS_SKIMAGE:
return gray, gray, "scikit-image not installed"
try:
H, W = gray.shape
H1 = int(round(H * z))
W1 = int(round(W * z))
first = _sk_resize(
gray,
(H1, W1),
order=3,
anti_aliasing=True,
preserve_range=True,
mode="reflect",
).astype(np.float64)
rec = _sk_resize(
first,
(H, W),
order=3,
anti_aliasing=False,
preserve_range=True,
mode="reflect",
).astype(np.float64)
first = np.clip(first, 0.0, 1.0).astype(gray.dtype, copy=False)
rec = np.clip(rec, 0.0, 1.0).astype(gray.dtype, copy=False)
return first, rec, None
except Exception as e:
return gray, gray, str(e)
def _rt_torch(
gray: np.ndarray, z: float
) -> Tuple[np.ndarray, np.ndarray, Optional[str]]:
"""PyTorch F.interpolate bicubic (antialias=False)."""
if not _HAS_TORCH:
return gray, gray, "PyTorch not installed"
try:
arr = gray
if arr.dtype == np.float32:
t_dtype = torch.float32
elif arr.dtype == np.float64:
t_dtype = torch.float64
else:
t_dtype = torch.float32
arr = arr.astype(np.float32, copy=False)
H, W = arr.shape
H1 = int(round(H * z))
W1 = int(round(W * z))
x = torch.from_numpy(arr).to(t_dtype).unsqueeze(0).unsqueeze(0)
first_t = F.interpolate(
x,
size=(H1, W1),
mode="bicubic",
align_corners=False,
antialias=False,
)
rec_t = F.interpolate(
first_t,
size=(H, W),
mode="bicubic",
align_corners=False,
antialias=False,
)
first = first_t[0, 0].detach().cpu().numpy()
rec = rec_t[0, 0].detach().cpu().numpy()
first = np.clip(first, 0.0, 1.0).astype(gray.dtype, copy=False)
rec = np.clip(rec, 0.0, 1.0).astype(gray.dtype, copy=False)
return first, rec, None
except Exception as e:
return gray, gray, str(e)
def _rt_torch_aa(
gray: np.ndarray, z: float
) -> Tuple[np.ndarray, np.ndarray, Optional[str]]:
"""PyTorch F.interpolate bicubic (antialias=True)."""
if not _HAS_TORCH:
return gray, gray, "PyTorch not installed"
try:
arr = gray
if arr.dtype == np.float32:
t_dtype = torch.float32
elif arr.dtype == np.float64:
t_dtype = torch.float64
else:
t_dtype = torch.float32
arr = arr.astype(np.float32, copy=False)
H, W = arr.shape
H1 = int(round(H * z))
W1 = int(round(W * z))
x = torch.from_numpy(arr).to(t_dtype).unsqueeze(0).unsqueeze(0)
first_t = F.interpolate(
x,
size=(H1, W1),
mode="bicubic",
align_corners=False,
antialias=True,
)
rec_t = F.interpolate(
first_t,
size=(H, W),
mode="bicubic",
align_corners=False,
antialias=True,
)
first = first_t[0, 0].detach().cpu().numpy()
rec = rec_t[0, 0].detach().cpu().numpy()
first = np.clip(first, 0.0, 1.0).astype(gray.dtype, copy=False)
rec = np.clip(rec, 0.0, 1.0).astype(gray.dtype, copy=False)
return first, rec, None
except Exception as e:
return gray, gray, str(e)
def _avg_time(rt_fn, repeats: int = N_TRIALS, warmup: bool = True):
"""
Run `rt_fn()` (which must return (first, rec, err)) `repeats` times.
Returns (last_first, last_rec, mean_time, std_time, err).
"""
if warmup:
try:
first, rec, err = rt_fn()
if err is not None:
return np.array([]), np.array([]), float("nan"), float("nan"), err
except Exception as e:
return np.array([]), np.array([]), float("nan"), float("nan"), str(e)
times: List[float] = []
last_first: Optional[np.ndarray] = None
last_rec: Optional[np.ndarray] = None
for _ in range(max(1, repeats)):
t0 = time.perf_counter()
first, rec, err = rt_fn()
if err is not None:
return np.array([]), np.array([]), float("nan"), float("nan"), err
dt = time.perf_counter() - t0
times.append(dt)
last_first = first
last_rec = rec
t_arr = np.asarray(times, dtype=np.float64)
mean_t = float(t_arr.mean())
sd_t = float(t_arr.std(ddof=1 if len(t_arr) > 1 else 0))
assert last_first is not None and last_rec is not None
return last_first, last_rec, mean_t, sd_t, None
# Methods and backend keys
BENCH_METHODS: List[Tuple[str, str]] = [
("SplineOps Standard cubic", "spl_standard"),
("SplineOps Antialiasing cubic", "spl_aa"),
("OpenCV INTER_CUBIC", "opencv"),
("SciPy cubic", "scipy"),
("Pillow BICUBIC", "pillow"),
("scikit-image cubic", "skimage"),
("scikit-image cubic (AA)", "skimage_aa"),
("PyTorch bicubic (CPU)", "torch"),
("PyTorch bicubic (AA, CPU)", "torch_aa"),
]
# Subsets for ROI main vs AA
MAIN_METHOD_LABELS = [
"SplineOps Standard cubic",
"SplineOps Antialiasing cubic",
"OpenCV INTER_CUBIC",
"SciPy cubic",
"scikit-image cubic",
"PyTorch bicubic (CPU)",
]
AA_METHOD_LABELS = [
"SplineOps Standard cubic",
"SplineOps Antialiasing cubic",
"Pillow BICUBIC",
"scikit-image cubic (AA)",
"PyTorch bicubic (AA, CPU)",
]
Benchmarking Helpers#
def benchmark_image(
img_name: str,
gray: np.ndarray,
zoom: float,
roi_size_px: int,
roi_center_frac: Tuple[float, float],
degree_label: str = "Cubic",
) -> Dict[str, object]:
"""
Run the full round-trip benchmark on one image.
Returns a dict with:
- gray, z, roi_rect, roi, degree_label
- aa_first (for intro plot)
- roi_tiles (list of (name, tile) for first-pass ROI montages, grayscale)
- diff_tiles (list of (name, tile) for ROI error montages, grayscale)
- rows (per-method metrics)
- diff_max_abs (shared max abs diff used for all diff tiles)
"""
H, W = gray.shape
z = zoom
roi_rect = _roi_rect_from_frac(gray.shape, roi_size_px, roi_center_frac)
roi = _crop_roi(gray, roi_rect)
roi_h = roi_rect[2]
roi_w = roi_rect[3]
center_r = roi_rect[0] + roi_h / 2.0
center_c = roi_rect[1] + roi_w / 2.0
print(
f"\n=== {img_name} | zoom={z:.3f} | shape={H}×{W} "
f"| ROI size≈{roi_size_px} px at center_frac={roi_center_frac} ===\n"
)
rows: List[Dict[str, object]] = []
roi_tiles: List[Tuple[str, np.ndarray]] = []
diff_tiles: List[Tuple[str, np.ndarray]] = []
# Original ROI tile
orig_tile = _nearest_big(roi, ROI_MAG_TARGET)
roi_tiles.append(("Original", orig_tile))
# Zero-diff baseline (stays at mid-gray)
diff_zero = 0.5 * np.ones_like(roi, dtype=DTYPE)
diff_zero_big = _nearest_big(diff_zero, ROI_MAG_TARGET)
diff_tiles.append(("Original (no diff)", diff_zero_big))
# Collect per-method recovered ROI for a shared error scale (per image)
rec_roi_store: List[Tuple[str, np.ndarray]] = []
diff_max_abs = 0.0
aa_first_for_plot: Optional[np.ndarray] = None
header = (
f"{'Method':<32} {'Time (mean)':>14} {'± SD':>10} "
f"{'SNR (dB)':>10} {'MSE':>14} {'SSIM':>8}"
)
print(header)
print("-" * len(header))
for label, backend in BENCH_METHODS:
if backend == "spl_standard":
rt_fn = lambda gray=gray, z=z: _rt_splineops(gray, z, "cubic")
elif backend == "spl_aa":
rt_fn = lambda gray=gray, z=z: _rt_splineops(gray, z, "cubic-antialiasing")
elif backend == "opencv":
rt_fn = lambda gray=gray, z=z: _rt_opencv(gray, z)
elif backend == "scipy":
rt_fn = lambda gray=gray, z=z: _rt_scipy(gray, z)
elif backend == "pillow":
rt_fn = lambda gray=gray, z=z: _rt_pillow(gray, z)
elif backend == "skimage":
rt_fn = lambda gray=gray, z=z: _rt_skimage(gray, z)
elif backend == "skimage_aa":
rt_fn = lambda gray=gray, z=z: _rt_skimage_aa(gray, z)
elif backend == "torch":
rt_fn = lambda gray=gray, z=z: _rt_torch(gray, z)
elif backend == "torch_aa":
rt_fn = lambda gray=gray, z=z: _rt_torch_aa(gray, z)
else:
continue
first, rec, t_mean, t_sd, err = _avg_time(rt_fn, repeats=N_TRIALS, warmup=True)
if err is not None or first.size == 0 or rec.size == 0:
print(
f"{label:<32} {'unavailable':>14} {'':>10} "
f"{'—':>10} {'—':>14} {'—':>8}"
)
rows.append(
dict(
name=label,
time=np.nan,
sd=np.nan,
snr=np.nan,
mse=np.nan,
ssim=np.nan,
err=err,
)
)
continue
if backend == "spl_aa":
aa_first_for_plot = first.copy()
rec_roi = _crop_roi(rec, roi_rect)
snr = _snr_db(roi, rec_roi)
mse = float(np.mean((roi - rec_roi) ** 2, dtype=np.float64))
if _HAS_SKIMAGE and _ssim is not None:
try:
dr = float(roi.max() - roi.min())
if dr <= 0:
dr = 1.0
ssim_val = float(_ssim(roi, rec_roi, data_range=dr))
except Exception:
ssim_val = float("nan")
else:
ssim_val = float("nan")
print(
f"{label:<32} {fmt_ms(t_mean):>14} {fmt_ms(t_sd):>10} "
f"{snr:>10.2f} {mse:>14.3e} {ssim_val:>8.4f}"
)
rows.append(
dict(
name=label,
time=t_mean,
sd=t_sd,
snr=snr,
mse=mse,
ssim=ssim_val,
err=None,
)
)
# First-pass ROI tile (in the resized domain)
H1, W1 = first.shape
roi_h_res = max(1, int(round(roi_h * z)))
roi_w_res = max(1, int(round(roi_w * z)))
if roi_h_res > H1 or roi_w_res > W1:
first_roi = first
else:
center_r_res = int(round(center_r * z))
center_c_res = int(round(center_c * z))
row_top_res = int(np.clip(center_r_res - roi_h_res // 2, 0, H1 - roi_h_res))
col_left_res = int(np.clip(center_c_res - roi_w_res // 2, 0, W1 - roi_w_res))
first_roi = first[
row_top_res : row_top_res + roi_h_res,
col_left_res : col_left_res + roi_w_res,
]
tile = _nearest_big(first_roi, ROI_MAG_TARGET)
roi_tiles.append((label, tile))
# Store recovered ROI (copy) for shared-scale diff montage
rec_roi_store.append((label, rec_roi.astype(DTYPE, copy=True)))
# Update shared max(|diff|) across all methods for this ROI
d = rec_roi.astype(np.float64, copy=False) - roi.astype(np.float64, copy=False)
diff_max_abs = max(diff_max_abs, float(np.max(np.abs(d))))
# Build diff tiles with ONE shared scale (per image) so all tiles are comparable
diff_max_abs = max(diff_max_abs, 1e-12)
for label, rec_roi_m in rec_roi_store:
diff_roi = _diff_normalized(roi, rec_roi_m, max_abs=diff_max_abs)
diff_tile = _nearest_big(diff_roi, ROI_MAG_TARGET)
diff_tiles.append((label, diff_tile))
return dict(
img_name=img_name,
gray=gray,
z=z,
roi_rect=roi_rect,
roi=roi,
degree_label=degree_label,
aa_first=aa_first_for_plot,
roi_tiles=roi_tiles,
diff_tiles=diff_tiles,
diff_max_abs=diff_max_abs,
rows=rows,
)
def show_intro_from_bench(bench: Dict[str, object]) -> None:
"""2×2 introductory figure for SplineOps AA (grayscale)."""
aa_first = bench["aa_first"]
if aa_first is None:
return
_show_initial_original_vs_aa(
gray=bench["gray"], # type: ignore[arg-type]
roi_rect=bench["roi_rect"], # type: ignore[arg-type]
aa_first=aa_first, # type: ignore[arg-type]
z=bench["z"], # type: ignore[arg-type]
degree_label=bench["degree_label"], # type: ignore[arg-type]
)
def show_roi_montage_main_from_bench(bench: Dict[str, object]) -> None:
"""
Grayscale ROI montage (main subset):
Original +
SplineOps Standard cubic
SplineOps Antialiasing cubic
OpenCV INTER_CUBIC
SciPy cubic
scikit-image cubic
PyTorch bicubic (CPU)
"""
roi_tiles: List[Tuple[str, np.ndarray]] = bench["roi_tiles"] # type: ignore[assignment]
if not roi_tiles:
return
tile_map = {name: tile for name, tile in roi_tiles}
names = ["Original"]
for lbl in MAIN_METHOD_LABELS:
if lbl in tile_map:
names.append(lbl)
rows, cols = 3, 3
fig, axes = plt.subplots(rows, cols, figsize=(3.2 * cols, 3.2 * rows))
axes = np.asarray(axes).reshape(rows, cols)
for ax in axes.ravel():
ax.axis("off")
for idx, name in enumerate(names):
if idx >= rows * cols:
break
tile = tile_map[name]
r, c = divmod(idx, cols)
ax = axes[r, c]
ax.imshow(tile, cmap="gray", interpolation="nearest")
title_kw = dict(fontsize=ROI_TILE_TITLE_FONTSIZE)
style = HIGHLIGHT_STYLE.get(name)
if style is not None:
c = style.get("color", "tab:blue")
lw = float(style.get("lw", 3.0))
title_kw.update(color=c, fontweight="bold")
_highlight_tile(ax, color=c, lw=lw)
ax.set_title(name, **title_kw)
ax.axis("off")
fig.tight_layout()
plt.show()
def show_roi_montage_aa_from_bench(bench: Dict[str, object]) -> None:
"""
Grayscale ROI montage (AA subset):
Original +
SplineOps Standard cubic
SplineOps Antialiasing cubic
Pillow BICUBIC
scikit-image cubic (AA)
PyTorch bicubic (AA, CPU)
"""
roi_tiles: List[Tuple[str, np.ndarray]] = bench["roi_tiles"] # type: ignore[assignment]
if not roi_tiles:
return
tile_map = {name: tile for name, tile in roi_tiles}
names = ["Original"]
for lbl in AA_METHOD_LABELS:
if lbl in tile_map:
names.append(lbl)
cols = 3
n_tiles = len(names)
rows = max(1, (n_tiles + cols - 1) // cols)
fig, axes = plt.subplots(rows, cols, figsize=(3.2 * cols, 3.2 * rows))
axes = np.asarray(axes).reshape(rows, cols)
for ax in axes.ravel():
ax.axis("off")
for idx, name in enumerate(names):
if idx >= rows * cols:
break
tile = tile_map[name]
r, c = divmod(idx, cols)
ax = axes[r, c]
ax.imshow(tile, cmap="gray", interpolation="nearest")
title_kw = dict(fontsize=ROI_TILE_TITLE_FONTSIZE)
style = HIGHLIGHT_STYLE.get(name)
if style is not None:
c = style.get("color", "tab:blue")
lw = float(style.get("lw", 3.0))
title_kw.update(color=c, fontweight="bold")
_highlight_tile(ax, color=c, lw=lw)
ax.set_title(name, **title_kw)
ax.axis("off")
fig.tight_layout()
plt.show()
def show_error_montage_main_from_bench(bench: Dict[str, object]) -> None:
"""
Grayscale error montage (main subset):
(rec - orig) normalized:
0.5 = 0, <0.5 = negative, >0.5 = positive.
"""
diff_tiles: List[Tuple[str, np.ndarray]] = bench["diff_tiles"] # type: ignore[assignment]
if not diff_tiles:
return
tile_map = {name: tile for name, tile in diff_tiles}
names = ["Original (no diff)"]
for lbl in MAIN_METHOD_LABELS:
if lbl in tile_map:
names.append(lbl)
rows, cols = 3, 3
fig, axes = plt.subplots(rows, cols, figsize=(3.2 * cols, 3.2 * rows))
axes = np.asarray(axes).reshape(rows, cols)
for ax in axes.ravel():
ax.axis("off")
max_tile_slots = rows * cols - 1 # reserve bottom-right for legend
num_tiles = min(len(names), max_tile_slots)
for idx in range(num_tiles):
name = names[idx]
tile = tile_map[name]
r, c = divmod(idx, cols)
ax = axes[r, c]
ax.imshow(tile, cmap="gray", interpolation="nearest", vmin=0.0, vmax=1.0)
title_kw = dict(fontsize=ROI_TILE_TITLE_FONTSIZE)
style = HIGHLIGHT_STYLE.get(name)
if style is not None:
c = style.get("color", "tab:blue")
lw = float(style.get("lw", 3.0))
title_kw.update(color=c, fontweight="bold")
_highlight_tile(ax, color=c, lw=lw)
ax.set_title(name, **title_kw)
ax.axis("off")
# Legend (unchanged)
ax_leg = axes[-1, -1]
ax_leg.axis("off")
H_leg = ROI_MAG_TARGET
W_leg = 32
y = np.linspace(1.0, 0.0, H_leg, dtype=np.float32)
legend_img = np.repeat(y[:, None], W_leg, axis=1)
ax_leg.imshow(legend_img, cmap="gray", vmin=0.0, vmax=1.0, aspect="auto")
ax_leg.set_title("Diff legend", fontsize=ROI_TILE_TITLE_FONTSIZE, pad=4)
ax_leg.text(1.05, 0.05, "-1", transform=ax_leg.transAxes,
fontsize=8, va="bottom", ha="left")
ax_leg.text(1.05, 0.50, "0 (no diff)", transform=ax_leg.transAxes,
fontsize=8, va="center", ha="left")
ax_leg.text(1.05, 0.95, "+1", transform=ax_leg.transAxes,
fontsize=8, va="top", ha="left")
fig.suptitle("Normalized signed difference in ROI", fontsize=ROI_SUPTITLE_FONTSIZE)
fig.tight_layout(rect=[0, 0, 1, 0.95])
plt.show()
def show_error_montage_aa_from_bench(bench: Dict[str, object]) -> None:
"""
Grayscale error montage (AA subset):
(rec - orig) normalized:
0.5 = 0, <0.5 = negative, >0.5 = positive.
"""
diff_tiles: List[Tuple[str, np.ndarray]] = bench["diff_tiles"] # type: ignore[assignment]
if not diff_tiles:
return
tile_map = {name: tile for name, tile in diff_tiles}
names = ["Original (no diff)"]
for lbl in AA_METHOD_LABELS:
if lbl in tile_map:
names.append(lbl)
rows, cols = 3, 3
fig, axes = plt.subplots(rows, cols, figsize=(3.2 * cols, 3.2 * rows))
axes = np.asarray(axes).reshape(rows, cols)
for ax in axes.ravel():
ax.axis("off")
max_tile_slots = rows * cols - 1
num_tiles = min(len(names), max_tile_slots)
for idx in range(num_tiles):
name = names[idx]
tile = tile_map[name]
r, c = divmod(idx, cols)
ax = axes[r, c]
ax.imshow(tile, cmap="gray", interpolation="nearest", vmin=0.0, vmax=1.0)
title_kw = dict(fontsize=ROI_TILE_TITLE_FONTSIZE)
style = HIGHLIGHT_STYLE.get(name)
if style is not None:
c = style.get("color", "tab:blue")
lw = float(style.get("lw", 3.0))
title_kw.update(color=c, fontweight="bold")
_highlight_tile(ax, color=c, lw=lw)
ax.set_title(name, **title_kw)
ax.axis("off")
# Legend (unchanged)
ax_leg = axes[-1, -1]
ax_leg.axis("off")
H_leg = ROI_MAG_TARGET
W_leg = 32
y = np.linspace(1.0, 0.0, H_leg, dtype=np.float32)
legend_img = np.repeat(y[:, None], W_leg, axis=1)
ax_leg.imshow(legend_img, cmap="gray", vmin=0.0, vmax=1.0, aspect="auto")
ax_leg.set_title("Diff legend", fontsize=ROI_TILE_TITLE_FONTSIZE, pad=4)
ax_leg.text(1.05, 0.05, "-1", transform=ax_leg.transAxes,
fontsize=8, va="bottom", ha="left")
ax_leg.text(1.05, 0.50, "0 (no diff)", transform=ax_leg.transAxes,
fontsize=8, va="center", ha="left")
ax_leg.text(1.05, 0.95, "+1", transform=ax_leg.transAxes,
fontsize=8, va="top", ha="left")
fig.suptitle("Normalized signed difference in ROI", fontsize=ROI_SUPTITLE_FONTSIZE)
fig.tight_layout(rect=[0, 0, 1, 0.95])
plt.show()
def show_timing_plot_from_bench(bench: Dict[str, object]) -> None:
"""Horizontal bar chart of round-trip timing per method."""
rows: List[Dict[str, object]] = bench["rows"] # type: ignore[assignment]
if not rows:
return
gray = bench["gray"] # type: ignore[index]
H, W = gray.shape
z = float(bench["z"])
degree_label = str(bench["degree_label"])
valid = [r for r in rows if np.isfinite(r.get("time", np.nan))]
if not valid:
return
names = [r["name"] for r in valid]
times = np.array([r["time"] for r in valid], dtype=np.float64)
sds = np.array([r["sd"] for r in valid], dtype=np.float64)
order = np.argsort(times)
names = [names[i] for i in order]
times = times[order]
sds = sds[order]
fig, ax = plt.subplots(figsize=PLOT_FIGSIZE)
y = np.arange(len(names))
bars = ax.barh(y, times, xerr=sds, alpha=0.8)
ax.set_yticks(y)
ax.set_yticklabels(names, fontsize=PLOT_TICK_FONTSIZE)
ax.tick_params(axis="x", labelsize=PLOT_TICK_FONTSIZE)
ax.set_xlabel(
f"Round-trip time (s) mean ± sd over {N_TRIALS} runs",
fontsize=PLOT_LABEL_FONTSIZE,
)
ax.set_title(
f"Timing vs Method (H×W = {H}×{W}, zoom ×{z:g}, degree={degree_label})",
fontsize=PLOT_TITLE_FONTSIZE,
)
ax.grid(axis="x", alpha=0.3)
# --- Color the METHOD NAMES (yticks) + outline highlighted bars ---
for tick, name, bar in zip(ax.get_yticklabels(), names, bars.patches):
style = HIGHLIGHT_STYLE.get(name)
if style is not None:
c = style.get("color", "tab:blue")
lw = float(style.get("lw", 3.0))
tick.set_color(c)
tick.set_fontweight("bold")
# Optional: outline the bar too (nice but not required)
bar.set_edgecolor(c)
bar.set_linewidth(lw)
fig.tight_layout()
plt.show()
def show_snr_ssim_plot_from_bench(bench: Dict[str, object]) -> None:
"""Combined SNR/SSIM bar chart per method."""
if not (_HAS_SKIMAGE and _ssim is not None):
return
rows: List[Dict[str, object]] = bench["rows"] # type: ignore[assignment]
if not rows:
return
gray = bench["gray"] # type: ignore[index]
H, W = gray.shape
z = float(bench["z"])
degree_label = str(bench["degree_label"])
valid = [
r for r in rows
if np.isfinite(r.get("snr", np.nan)) and np.isfinite(r.get("ssim", np.nan))
]
if not valid:
return
names = [r["name"] for r in valid]
snrs = np.array([r["snr"] for r in valid], dtype=np.float64)
ssims = np.array([r["ssim"] for r in valid], dtype=np.float64)
order = np.argsort(-snrs)
names = [names[i] for i in order]
snrs = snrs[order]
ssims = ssims[order]
x = np.arange(len(names))
width = 0.4
fig, ax1 = plt.subplots(figsize=PLOT_FIGSIZE)
snr_color = "tab:blue"
ssim_color = "tab:green"
snr_bars = ax1.bar(
x - width / 2,
snrs,
width,
label="SNR (dB)",
alpha=0.85,
color=snr_color,
)
ax1.set_ylabel("SNR (dB)", color=snr_color, fontsize=PLOT_LABEL_FONTSIZE)
ax1.tick_params(axis="y", labelcolor=snr_color, labelsize=PLOT_TICK_FONTSIZE)
ax1.set_xticks(x)
ax1.set_xticklabels(
names,
rotation=30,
ha="right",
fontsize=PLOT_TICK_FONTSIZE,
)
ax1.grid(axis="y", alpha=0.3)
ax2 = ax1.twinx()
ssim_bars = ax2.bar(
x + width / 2,
ssims,
width,
label="SSIM",
alpha=0.6,
color=ssim_color,
)
ax2.set_ylabel("SSIM", color=ssim_color, fontsize=PLOT_LABEL_FONTSIZE)
ax2.tick_params(axis="y", labelcolor=ssim_color, labelsize=PLOT_TICK_FONTSIZE)
roi_h, roi_w = bench["roi_rect"][2], bench["roi_rect"][3] # type: ignore[index]
ax1.set_title(
f"SNR / SSIM vs Method (ROI = {roi_h}×{roi_w} px, zoom ×{z:g}, degree={degree_label})",
fontsize=PLOT_TITLE_FONTSIZE,
)
handles = [snr_bars[0], ssim_bars[0]]
labels = ["SNR (dB)", "SSIM"]
fig.legend(
handles,
labels,
loc="upper right",
bbox_to_anchor=(1, 1),
fontsize=PLOT_LEGEND_FONTSIZE,
)
# --- Highlight xtick labels (SplineOps methods) ---
for tick, name in zip(ax1.get_xticklabels(), names):
style = HIGHLIGHT_STYLE.get(name)
if style is not None:
col = style.get("color", "tab:blue")
tick.set_color(col)
tick.set_fontweight("bold")
# --- Outline BOTH bars (now safe because ssim_bars exists) ---
for i, name in enumerate(names):
style = HIGHLIGHT_STYLE.get(name)
if style is not None:
col = style.get("color", "tab:blue")
lw = float(style.get("lw", 3.0))
snr_bars.patches[i].set_edgecolor(col)
snr_bars.patches[i].set_linewidth(lw)
ssim_bars.patches[i].set_edgecolor(col)
ssim_bars.patches[i].set_linewidth(lw)
# --- Smart truncated y-limits (robust) ---
snr_lim = _smart_ylim(snrs, pad_frac=0.06, min_span=1.0) # dB
if snr_lim is not None:
ax1.set_ylim(*snr_lim)
ssim_lim = _smart_ylim(ssims, lo_cap=0.0, hi_cap=1.0, pad_frac=0.02, min_span=0.02)
if ssim_lim is not None:
ax2.set_ylim(*ssim_lim)
fig.tight_layout()
plt.show()
Color ROI Montage Helpers#
def _first_pass_color_for_backend(
backend: str,
rgb: np.ndarray,
z: float,
) -> Optional[np.ndarray]:
"""
First-pass color resized image for a given backend, for color ROIs only.
"""
H, W, C = rgb.shape
H1 = int(round(H * z))
W1 = int(round(W * z))
if H1 < 1 or W1 < 1:
return None
try:
if backend in ("spl_standard", "spl_aa"):
if not _HAS_SPLINEOPS:
return None
method = "cubic" if backend == "spl_standard" else "cubic-antialiasing"
zoom_hw = (z, z)
channels = []
for c in range(C):
ch = sp_resize(
rgb[..., c],
zoom_factors=zoom_hw,
method=method,
)
channels.append(ch)
first = np.stack(channels, axis=-1)
elif backend == "scipy":
if not _HAS_SCIPY:
return None
channels = []
for c in range(C):
ch = _ndi_zoom(
rgb[..., c],
(z, z),
order=3,
prefilter=True,
mode="reflect",
grid_mode=False,
)
channels.append(ch)
first = np.stack(channels, axis=-1)
elif backend == "opencv":
if not _HAS_CV2:
return None
arr = rgb.astype(np.float32, copy=False)
first = cv2.resize(arr, (W1, H1), interpolation=cv2.INTER_CUBIC)
elif backend == "pillow":
from PIL import Image as _Image
arr_uint8 = (np.clip(rgb, 0.0, 1.0) * 255).astype(np.uint8)
im = _Image.fromarray(arr_uint8, mode="RGB")
first_im = im.resize((W1, H1), resample=_Image.Resampling.BICUBIC)
first = np.asarray(first_im, dtype=np.float32) / 255.0
elif backend == "skimage":
if not _HAS_SKIMAGE:
return None
first = _sk_resize(
rgb,
(H1, W1, C),
order=3,
anti_aliasing=False,
preserve_range=True,
mode="reflect",
).astype(np.float32)
elif backend == "skimage_aa":
if not _HAS_SKIMAGE:
return None
first = _sk_resize(
rgb,
(H1, W1, C),
order=3,
anti_aliasing=True,
preserve_range=True,
mode="reflect",
).astype(np.float32)
elif backend == "torch":
if not _HAS_TORCH:
return None
arr = rgb
if arr.dtype == np.float32:
t_dtype = torch.float32
elif arr.dtype == np.float64:
t_dtype = torch.float64
else:
t_dtype = torch.float32
arr = arr.astype(np.float32, copy=False)
x = torch.from_numpy(arr).to(t_dtype).permute(2, 0, 1).unsqueeze(0)
first_t = F.interpolate(
x,
size=(H1, W1),
mode="bicubic",
align_corners=False,
antialias=False,
)
first = first_t[0].permute(1, 2, 0).detach().cpu().numpy()
elif backend == "torch_aa":
if not _HAS_TORCH:
return None
arr = rgb
if arr.dtype == np.float32:
t_dtype = torch.float32
elif arr.dtype == np.float64:
t_dtype = torch.float64
else:
t_dtype = torch.float32
arr = arr.astype(np.float32, copy=False)
x = torch.from_numpy(arr).to(t_dtype).permute(2, 0, 1).unsqueeze(0)
first_t = F.interpolate(
x,
size=(H1, W1),
mode="bicubic",
align_corners=False,
antialias=True,
)
first = first_t[0].permute(1, 2, 0).detach().cpu().numpy()
else:
return None
return np.clip(first, 0.0, 1.0).astype(DTYPE, copy=False)
except Exception:
return None
def show_roi_montage_color_main_from_bench(
bench: Dict[str, object],
orig_rgb: np.ndarray,
) -> None:
"""Color ROI montage for the main subset of methods."""
roi_rect = bench["roi_rect"]
z = float(bench["z"])
row0, col0, roi_h, roi_w = roi_rect
roi_orig = orig_rgb[row0:row0 + roi_h, col0:col0 + roi_w, :]
orig_tile = _nearest_big_color(roi_orig, ROI_MAG_TARGET)
tiles: List[Tuple[str, np.ndarray]] = [("Original (color)", orig_tile)]
center_r = row0 + roi_h / 2.0
center_c = col0 + roi_w / 2.0
subset = [
("SplineOps Standard cubic", "spl_standard"),
("SplineOps Antialiasing cubic", "spl_aa"),
("OpenCV INTER_CUBIC", "opencv"),
("SciPy cubic", "scipy"),
("scikit-image cubic", "skimage"),
("PyTorch bicubic (CPU)", "torch"),
]
for label, backend in subset:
first_color = _first_pass_color_for_backend(backend, orig_rgb, z)
if first_color is None:
continue
H1, W1, _ = first_color.shape
roi_h_res = max(1, int(round(roi_h * z)))
roi_w_res = max(1, int(round(roi_w * z)))
if roi_h_res > H1 or roi_w_res > W1:
roi_first = first_color
else:
center_r_res = int(round(center_r * z))
center_c_res = int(round(center_c * z))
row_top_res = int(np.clip(center_r_res - roi_h_res // 2, 0, H1 - roi_h_res))
col_left_res = int(np.clip(center_c_res - roi_w_res // 2, 0, W1 - roi_w_res))
roi_first = first_color[
row_top_res : row_top_res + roi_h_res,
col_left_res : col_left_res + roi_w_res,
:
]
tile = _nearest_big_color(roi_first, ROI_MAG_TARGET)
tiles.append((label, tile))
rows, cols = 3, 3
fig, axes = plt.subplots(rows, cols, figsize=(3.2 * cols, 3.2 * rows))
axes = np.asarray(axes).reshape(rows, cols)
for ax in axes.ravel():
ax.axis("off")
for idx, (name, tile) in enumerate(tiles):
if idx >= rows * cols:
break
r, c = divmod(idx, cols)
ax = axes[r, c]
ax.imshow(np.clip(tile, 0.0, 1.0))
title_kw = dict(fontsize=ROI_TILE_TITLE_FONTSIZE)
style = HIGHLIGHT_STYLE.get(name)
if style is not None:
c = style.get("color", "tab:blue")
lw = float(style.get("lw", 3.0))
title_kw.update(color=c, fontweight="bold")
_highlight_tile(ax, color=c, lw=lw)
ax.set_title(name, **title_kw)
ax.axis("off")
fig.tight_layout()
plt.show()
def show_roi_montage_color_aa_from_bench(
bench: Dict[str, object],
orig_rgb: np.ndarray,
) -> None:
"""Color ROI montage for AA / smoothing subset."""
roi_rect = bench["roi_rect"]
z = float(bench["z"])
row0, col0, roi_h, roi_w = roi_rect
roi_orig = orig_rgb[row0:row0 + roi_h, col0:col0 + roi_w, :]
orig_tile = _nearest_big_color(roi_orig, ROI_MAG_TARGET)
tiles: List[Tuple[str, np.ndarray]] = [("Original (color)", orig_tile)]
center_r = row0 + roi_h / 2.0
center_c = col0 + roi_w / 2.0
subset = [
("SplineOps Standard cubic", "spl_standard"),
("SplineOps Antialiasing cubic", "spl_aa"),
("Pillow BICUBIC", "pillow"),
("scikit-image cubic (AA)", "skimage_aa"),
("PyTorch bicubic (AA, CPU)", "torch_aa"),
]
for label, backend in subset:
first_color = _first_pass_color_for_backend(backend, orig_rgb, z)
if first_color is None:
continue
H1, W1, _ = first_color.shape
roi_h_res = max(1, int(round(roi_h * z)))
roi_w_res = max(1, int(round(roi_w * z)))
if roi_h_res > H1 or roi_w_res > W1:
roi_first = first_color
else:
center_r_res = int(round(center_r * z))
center_c_res = int(round(center_c * z))
row_top_res = int(np.clip(center_r_res - roi_h_res // 2, 0, H1 - roi_h_res))
col_left_res = int(np.clip(center_c_res - roi_w_res // 2, 0, W1 - roi_w_res))
roi_first = first_color[
row_top_res : row_top_res + roi_h_res,
col_left_res : col_left_res + roi_w_res,
:
]
tile = _nearest_big_color(roi_first, ROI_MAG_TARGET)
tiles.append((label, tile))
cols = 3
n_tiles = len(tiles)
rows = max(1, (n_tiles + cols - 1) // cols)
fig, axes = plt.subplots(rows, cols, figsize=(3.2 * cols, 3.2 * rows))
axes = np.asarray(axes).reshape(rows, cols)
for ax in axes.ravel():
ax.axis("off")
for idx, (name, tile) in enumerate(tiles):
if idx >= rows * cols:
break
r, c = divmod(idx, cols)
ax = axes[r, c]
ax.imshow(np.clip(tile, 0.0, 1.0))
title_kw = dict(fontsize=ROI_TILE_TITLE_FONTSIZE)
style = HIGHLIGHT_STYLE.get(name)
if style is not None:
c = style.get("color", "tab:blue")
lw = float(style.get("lw", 3.0))
title_kw.update(color=c, fontweight="bold")
_highlight_tile(ax, color=c, lw=lw)
ax.set_title(name, **title_kw)
ax.axis("off")
fig.tight_layout()
plt.show()
Load All Images#
orig_images: Dict[str, np.ndarray] = {}
orig_images_rgb: Dict[str, np.ndarray] = {}
for name, url in KODAK_IMAGES:
gray = _load_kodak_gray(url)
orig_images[name] = gray
rgb = _load_kodak_rgb(url)
orig_images_rgb[name] = rgb
print(f"Loaded {name} from {url} | gray shape={gray.shape}, rgb shape={rgb.shape}")
print("\nTimings averaged over "
f"{N_TRIALS} runs per method (1 warm-up run not counted).\n")
# Small helper for color intro using SplineOps antialiasing
def _color_intro_for_image(
img_name: str,
bench: Dict[str, object],
) -> None:
if not _HAS_SPLINEOPS:
show_intro_from_bench(bench)
return
rgb = orig_images_rgb[img_name]
roi_rect = bench["roi_rect"]
z = float(bench["z"])
zoom_hw = (z, z)
channels = []
for c in range(rgb.shape[2]):
ch = sp_resize(
rgb[..., c],
zoom_factors=zoom_hw,
method="cubic-antialiasing",
)
channels.append(ch)
aa_rgb = np.stack(channels, axis=-1)
show_intro_color(
original_rgb=rgb,
shrunk_rgb=aa_rgb,
roi_rect=roi_rect,
zoom=z,
label="Antialiasing",
degree_label=bench["degree_label"], # type: ignore[arg-type]
)
Loaded kodim05 from https://r0k.us/graphics/kodak/kodak/kodim05.png | gray shape=(512, 768), rgb shape=(512, 768, 3)
Loaded kodim07 from https://r0k.us/graphics/kodak/kodak/kodim07.png | gray shape=(512, 768), rgb shape=(512, 768, 3)
Loaded kodim14 from https://r0k.us/graphics/kodak/kodak/kodim14.png | gray shape=(512, 768), rgb shape=(512, 768, 3)
Loaded kodim15 from https://r0k.us/graphics/kodak/kodak/kodim15.png | gray shape=(512, 768), rgb shape=(512, 768, 3)
Loaded kodim19 from https://r0k.us/graphics/kodak/kodak/kodim19.png | gray shape=(768, 512), rgb shape=(768, 512, 3)
Loaded kodim22 from https://r0k.us/graphics/kodak/kodak/kodim22.png | gray shape=(512, 768), rgb shape=(512, 768, 3)
Loaded kodim23 from https://r0k.us/graphics/kodak/kodak/kodim23.png | gray shape=(512, 768), rgb shape=(512, 768, 3)
Timings averaged over 10 runs per method (1 warm-up run not counted).
Image: kodim05#
img_name = "kodim05"
img_orig = orig_images[img_name]
cfg = IMAGE_CONFIG[img_name]
zoom = float(cfg["zoom"])
roi_size_px = int(cfg["roi_size_px"])
roi_center_frac = tuple(map(float, cfg["roi_center_frac"])) # type: ignore[arg-type]
bench_kodim05 = benchmark_image(
img_name=img_name,
gray=img_orig,
zoom=zoom,
roi_size_px=roi_size_px,
roi_center_frac=roi_center_frac,
degree_label="Cubic",
)
=== kodim05 | zoom=0.150 | shape=512×768 | ROI size≈256 px at center_frac=(0.75, 0.5) ===
Method Time (mean) ± SD SNR (dB) MSE SSIM
---------------------------------------------------------------------------------------------
SplineOps Standard cubic 2.6 ms 0.0 ms 7.29 1.768e-02 0.3098
SplineOps Antialiasing cubic 4.8 ms 0.0 ms 9.58 1.045e-02 0.3682
OpenCV INTER_CUBIC 0.4 ms 0.0 ms 7.53 1.672e-02 0.3285
SciPy cubic 31.3 ms 0.3 ms 7.36 1.740e-02 0.3106
Pillow BICUBIC 2.9 ms 0.1 ms 9.35 1.101e-02 0.3368
scikit-image cubic 32.1 ms 0.3 ms 7.52 1.678e-02 0.3233
scikit-image cubic (AA) 39.5 ms 0.1 ms 9.17 1.147e-02 0.3185
PyTorch bicubic (CPU) 2.3 ms 0.0 ms 7.53 1.672e-02 0.3285
PyTorch bicubic (AA, CPU) 1.3 ms 0.1 ms 9.35 1.101e-02 0.3368
Original and Resized#
_color_intro_for_image(img_name, bench_kodim05)

ROI Comparison#
show_roi_montage_main_from_bench(bench_kodim05)

ROI Comparison Error#
show_error_montage_main_from_bench(bench_kodim05)

ROI Comparison (Antialiased)#
show_roi_montage_aa_from_bench(bench_kodim05)

ROI Comparison Error (Antialiased)#
show_error_montage_aa_from_bench(bench_kodim05)

ROI Color Comparison#
show_roi_montage_color_main_from_bench(bench_kodim05, orig_images_rgb[img_name])

ROI Color Comparison (Antialiased)#
show_roi_montage_color_aa_from_bench(bench_kodim05, orig_images_rgb[img_name])

Timing Comparison#
show_timing_plot_from_bench(bench_kodim05)

SNR/SSIM Comparison#
show_snr_ssim_plot_from_bench(bench_kodim05)

Image: kodim07#
img_name = "kodim07"
img_orig = orig_images[img_name]
cfg = IMAGE_CONFIG[img_name]
zoom = float(cfg["zoom"])
roi_size_px = int(cfg["roi_size_px"])
roi_center_frac = tuple(map(float, cfg["roi_center_frac"])) # type: ignore[arg-type]
bench_kodim07 = benchmark_image(
img_name=img_name,
gray=img_orig,
zoom=zoom,
roi_size_px=roi_size_px,
roi_center_frac=roi_center_frac,
degree_label="Cubic",
)
=== kodim07 | zoom=0.150 | shape=512×768 | ROI size≈256 px at center_frac=(0.4, 0.5) ===
Method Time (mean) ± SD SNR (dB) MSE SSIM
---------------------------------------------------------------------------------------------
SplineOps Standard cubic 2.6 ms 0.0 ms 13.48 9.393e-03 0.4988
SplineOps Antialiasing cubic 4.7 ms 0.0 ms 15.71 5.618e-03 0.5280
OpenCV INTER_CUBIC 0.4 ms 0.0 ms 13.52 9.304e-03 0.5022
SciPy cubic 31.0 ms 0.1 ms 13.48 9.393e-03 0.4988
Pillow BICUBIC 2.8 ms 0.0 ms 15.51 5.882e-03 0.5091
scikit-image cubic 31.9 ms 0.1 ms 13.49 9.372e-03 0.4960
scikit-image cubic (AA) 39.4 ms 0.1 ms 15.29 6.191e-03 0.4948
PyTorch bicubic (CPU) 3.1 ms 1.1 ms 13.52 9.304e-03 0.5022
PyTorch bicubic (AA, CPU) 1.3 ms 0.0 ms 15.51 5.882e-03 0.5091
Original and Resized#
_color_intro_for_image(img_name, bench_kodim07)

ROI Comparison#
show_roi_montage_main_from_bench(bench_kodim07)

ROI Comparison Error#
show_error_montage_main_from_bench(bench_kodim07)

ROI Comparison (Antialiased)#
show_roi_montage_aa_from_bench(bench_kodim07)

ROI Comparison Error (Antialiased)#
show_error_montage_aa_from_bench(bench_kodim07)

ROI Color Comparison#
show_roi_montage_color_main_from_bench(bench_kodim07, orig_images_rgb[img_name])

ROI Color Comparison (Antialiased)#
show_roi_montage_color_aa_from_bench(bench_kodim07, orig_images_rgb[img_name])

Timing Comparison#
show_timing_plot_from_bench(bench_kodim07)

SNR/SSIM Comparison#
show_snr_ssim_plot_from_bench(bench_kodim07)

Image: kodim14#
img_name = "kodim14"
img_orig = orig_images[img_name]
cfg = IMAGE_CONFIG[img_name]
zoom = float(cfg["zoom"])
roi_size_px = int(cfg["roi_size_px"])
roi_center_frac = tuple(map(float, cfg["roi_center_frac"])) # type: ignore[arg-type]
bench_kodim14 = benchmark_image(
img_name=img_name,
gray=img_orig,
zoom=zoom,
roi_size_px=roi_size_px,
roi_center_frac=roi_center_frac,
degree_label="Cubic",
)
=== kodim14 | zoom=0.300 | shape=512×768 | ROI size≈256 px at center_frac=(0.75, 0.75) ===
Method Time (mean) ± SD SNR (dB) MSE SSIM
---------------------------------------------------------------------------------------------
SplineOps Standard cubic 3.1 ms 0.0 ms 14.61 6.942e-03 0.6010
SplineOps Antialiasing cubic 5.8 ms 0.1 ms 16.58 4.417e-03 0.6797
OpenCV INTER_CUBIC 0.4 ms 0.0 ms 14.64 6.909e-03 0.6114
SciPy cubic 32.6 ms 0.1 ms 14.68 6.844e-03 0.6021
Pillow BICUBIC 3.1 ms 0.1 ms 16.31 4.698e-03 0.6390
scikit-image cubic 33.3 ms 0.1 ms 14.61 6.954e-03 0.6022
scikit-image cubic (AA) 38.0 ms 0.0 ms 16.26 4.757e-03 0.6368
PyTorch bicubic (CPU) 2.4 ms 0.0 ms 14.64 6.909e-03 0.6114
PyTorch bicubic (AA, CPU) 1.3 ms 0.0 ms 16.31 4.698e-03 0.6390
Original and Resized#
_color_intro_for_image(img_name, bench_kodim14)

ROI Comparison#
show_roi_montage_main_from_bench(bench_kodim14)

ROI Comparison Error#
show_error_montage_main_from_bench(bench_kodim14)

ROI Comparison (Antialiased)#
show_roi_montage_aa_from_bench(bench_kodim14)

ROI Comparison Error (Antialiased)#
show_error_montage_aa_from_bench(bench_kodim14)

ROI Color Comparison#
show_roi_montage_color_main_from_bench(bench_kodim14, orig_images_rgb[img_name])

ROI Color Comparison (Antialiased)#
show_roi_montage_color_aa_from_bench(bench_kodim14, orig_images_rgb[img_name])

Time Comparison#
show_timing_plot_from_bench(bench_kodim14)

SNR/SSIM Comparison#
show_snr_ssim_plot_from_bench(bench_kodim14)

Image: kodim15#
img_name = "kodim15"
img_orig = orig_images[img_name]
cfg = IMAGE_CONFIG[img_name]
zoom = float(cfg["zoom"])
roi_size_px = int(cfg["roi_size_px"])
roi_center_frac = tuple(map(float, cfg["roi_center_frac"])) # type: ignore[arg-type]
bench_kodim15 = benchmark_image(
img_name=img_name,
gray=img_orig,
zoom=zoom,
roi_size_px=roi_size_px,
roi_center_frac=roi_center_frac,
degree_label="Cubic",
)
=== kodim15 | zoom=0.300 | shape=512×768 | ROI size≈256 px at center_frac=(0.3, 0.55) ===
Method Time (mean) ± SD SNR (dB) MSE SSIM
---------------------------------------------------------------------------------------------
SplineOps Standard cubic 3.1 ms 0.0 ms 17.97 2.034e-03 0.7223
SplineOps Antialiasing cubic 5.8 ms 0.0 ms 19.89 1.308e-03 0.7791
OpenCV INTER_CUBIC 0.4 ms 0.0 ms 18.08 1.987e-03 0.7288
SciPy cubic 32.5 ms 0.1 ms 17.97 2.034e-03 0.7223
Pillow BICUBIC 3.1 ms 0.0 ms 19.36 1.479e-03 0.7627
scikit-image cubic 33.5 ms 0.4 ms 17.99 2.025e-03 0.7231
scikit-image cubic (AA) 37.8 ms 0.1 ms 19.31 1.494e-03 0.7624
PyTorch bicubic (CPU) 2.4 ms 0.0 ms 18.08 1.987e-03 0.7288
PyTorch bicubic (AA, CPU) 1.5 ms 0.3 ms 19.36 1.479e-03 0.7627
Original and Resized#
_color_intro_for_image(img_name, bench_kodim15)

ROI Comparison#
show_roi_montage_main_from_bench(bench_kodim15)

ROI Comparison Error#
show_error_montage_main_from_bench(bench_kodim15)

ROI Comparison (Antialiased)#
show_roi_montage_aa_from_bench(bench_kodim15)

ROI Comparison Error (Antialiased)#
show_error_montage_aa_from_bench(bench_kodim15)

ROI Color Comparison#
show_roi_montage_color_main_from_bench(bench_kodim15, orig_images_rgb[img_name])

ROI Color Comparison (Antialiased)#
show_roi_montage_color_aa_from_bench(bench_kodim15, orig_images_rgb[img_name])

Time Comparison#
show_timing_plot_from_bench(bench_kodim15)

SNR/SSIM Comparison#
show_snr_ssim_plot_from_bench(bench_kodim15)

Image: kodim19#
img_name = "kodim19"
img_orig = orig_images[img_name]
cfg = IMAGE_CONFIG[img_name]
zoom = float(cfg["zoom"])
roi_size_px = int(cfg["roi_size_px"])
roi_center_frac = tuple(map(float, cfg["roi_center_frac"])) # type: ignore[arg-type]
bench_kodim19 = benchmark_image(
img_name=img_name,
gray=img_orig,
zoom=zoom,
roi_size_px=roi_size_px,
roi_center_frac=roi_center_frac,
degree_label="Cubic",
)
=== kodim19 | zoom=0.200 | shape=768×512 | ROI size≈256 px at center_frac=(0.65, 0.35) ===
Method Time (mean) ± SD SNR (dB) MSE SSIM
---------------------------------------------------------------------------------------------
SplineOps Standard cubic 3.3 ms 0.0 ms 11.44 2.413e-02 0.4145
SplineOps Antialiasing cubic 5.2 ms 0.0 ms 13.82 1.394e-02 0.4540
OpenCV INTER_CUBIC 0.4 ms 0.0 ms 11.52 2.367e-02 0.4287
SciPy cubic 32.9 ms 0.0 ms 11.44 2.413e-02 0.4145
Pillow BICUBIC 2.8 ms 0.0 ms 13.69 1.438e-02 0.4299
scikit-image cubic 33.7 ms 0.1 ms 11.47 2.396e-02 0.4216
scikit-image cubic (AA) 40.8 ms 0.1 ms 13.55 1.483e-02 0.4189
PyTorch bicubic (CPU) 2.4 ms 0.0 ms 11.52 2.367e-02 0.4287
PyTorch bicubic (AA, CPU) 1.3 ms 0.0 ms 13.69 1.438e-02 0.4299
Original and Resized#
_color_intro_for_image(img_name, bench_kodim19)

ROI Comparison#
show_roi_montage_main_from_bench(bench_kodim19)

ROI Comparison Error#
show_error_montage_main_from_bench(bench_kodim19)

ROI Comparison (Antialiased)#
show_roi_montage_aa_from_bench(bench_kodim19)

ROI Comparison Error (Antialiased)#
show_error_montage_aa_from_bench(bench_kodim19)

ROI Color Comparison#
show_roi_montage_color_main_from_bench(bench_kodim19, orig_images_rgb[img_name])

ROI Color Comparison (Antialiased)#
show_roi_montage_color_aa_from_bench(bench_kodim19, orig_images_rgb[img_name])

Time Comparison#
show_timing_plot_from_bench(bench_kodim19)

SNR/SSIM Comparison#
show_snr_ssim_plot_from_bench(bench_kodim19)

Image: kodim22#
img_name = "kodim22"
img_orig = orig_images[img_name]
cfg = IMAGE_CONFIG[img_name]
zoom = float(cfg["zoom"])
roi_size_px = int(cfg["roi_size_px"])
roi_center_frac = tuple(map(float, cfg["roi_center_frac"])) # type: ignore[arg-type]
bench_kodim22 = benchmark_image(
img_name=img_name,
gray=img_orig,
zoom=zoom,
roi_size_px=roi_size_px,
roi_center_frac=roi_center_frac,
degree_label="Cubic",
)
=== kodim22 | zoom=0.200 | shape=512×768 | ROI size≈256 px at center_frac=(0.5, 0.25) ===
Method Time (mean) ± SD SNR (dB) MSE SSIM
---------------------------------------------------------------------------------------------
SplineOps Standard cubic 2.7 ms 0.0 ms 12.26 8.970e-03 0.4499
SplineOps Antialiasing cubic 5.1 ms 0.0 ms 14.70 5.118e-03 0.5099
OpenCV INTER_CUBIC 0.4 ms 0.0 ms 12.46 8.567e-03 0.4574
SciPy cubic 31.4 ms 0.1 ms 12.26 8.970e-03 0.4499
Pillow BICUBIC 2.8 ms 0.0 ms 14.49 5.370e-03 0.4823
scikit-image cubic 32.0 ms 0.1 ms 12.40 8.688e-03 0.4482
scikit-image cubic (AA) 38.1 ms 0.1 ms 14.40 5.482e-03 0.4728
PyTorch bicubic (CPU) 2.5 ms 0.0 ms 12.46 8.567e-03 0.4574
PyTorch bicubic (AA, CPU) 1.3 ms 0.0 ms 14.49 5.370e-03 0.4823
Original and Resized#
_color_intro_for_image(img_name, bench_kodim22)

ROI Comparison#
show_roi_montage_main_from_bench(bench_kodim22)

ROI Comparison Error#
show_error_montage_main_from_bench(bench_kodim22)

ROI Comparison (Antialiased)#
show_roi_montage_aa_from_bench(bench_kodim22)

ROI Comparison Error (Antialiased)#
show_error_montage_aa_from_bench(bench_kodim22)

ROI Color Comparison#
show_roi_montage_color_main_from_bench(bench_kodim22, orig_images_rgb[img_name])

ROI Color Comparison (Antialiased)#
show_roi_montage_color_aa_from_bench(bench_kodim22, orig_images_rgb[img_name])

Time Comparison#
show_timing_plot_from_bench(bench_kodim22)

SNR/SSIM Comparison#
show_snr_ssim_plot_from_bench(bench_kodim22)

Image: kodim23#
img_name = "kodim23"
img_orig = orig_images[img_name]
cfg = IMAGE_CONFIG[img_name]
zoom = float(cfg["zoom"])
roi_size_px = int(cfg["roi_size_px"])
roi_center_frac = tuple(map(float, cfg["roi_center_frac"])) # type: ignore[arg-type]
bench_kodim23 = benchmark_image(
img_name=img_name,
gray=img_orig,
zoom=zoom,
roi_size_px=roi_size_px,
roi_center_frac=roi_center_frac,
degree_label="Cubic",
)
=== kodim23 | zoom=0.150 | shape=512×768 | ROI size≈256 px at center_frac=(0.4, 0.65) ===
Method Time (mean) ± SD SNR (dB) MSE SSIM
---------------------------------------------------------------------------------------------
SplineOps Standard cubic 2.5 ms 0.0 ms 16.45 4.817e-03 0.6700
SplineOps Antialiasing cubic 4.7 ms 0.1 ms 18.58 2.947e-03 0.7029
OpenCV INTER_CUBIC 0.4 ms 0.0 ms 16.39 4.885e-03 0.6758
SciPy cubic 31.5 ms 0.7 ms 16.45 4.817e-03 0.6700
Pillow BICUBIC 2.8 ms 0.0 ms 18.29 3.155e-03 0.6917
scikit-image cubic 31.6 ms 0.1 ms 16.39 4.878e-03 0.6714
scikit-image cubic (AA) 39.3 ms 0.1 ms 18.07 3.316e-03 0.6852
PyTorch bicubic (CPU) 2.3 ms 0.0 ms 16.39 4.885e-03 0.6758
PyTorch bicubic (AA, CPU) 1.3 ms 0.0 ms 18.29 3.155e-03 0.6917
Original and Resized#
_color_intro_for_image(img_name, bench_kodim23)

ROI Comparison#
show_roi_montage_main_from_bench(bench_kodim23)

ROI Comparison Error#
show_error_montage_main_from_bench(bench_kodim23)

ROI Comparison (Antialiased)#
show_roi_montage_aa_from_bench(bench_kodim23)

ROI Comparison Error (Antialiased)#
show_error_montage_aa_from_bench(bench_kodim23)

ROI Color Comparison#
show_roi_montage_color_main_from_bench(bench_kodim23, orig_images_rgb[img_name])

ROI Color Comparison (Antialiased)#
show_roi_montage_color_aa_from_bench(bench_kodim23, orig_images_rgb[img_name])

Time Comparison#
show_timing_plot_from_bench(bench_kodim23)

SNR/SSIM Comparison#
show_snr_ssim_plot_from_bench(bench_kodim23)

Runtime Context#
Finally, we print a short summary of the runtime environment and the storage dtype used for the benchmark.
if _HAS_SPECS and print_runtime_context is not None:
print_runtime_context(include_threadpools=True)
print(f"Benchmark storage dtype: {np.dtype(DTYPE).name}")
Runtime context:
Python : 3.12.12 (CPython)
OS : Linux 6.11.0-1018-azure (x86_64)
CPU : x86_64 | logical cores: 4
Process : pid=2422 | Python threads=1
NumPy/SciPy : 2.2.6/1.16.3
Matplotlib : 3.10.8 | backend: agg
splineops : 1.2.1 | native ext present: True
Extra libs : Pillow=12.0.0, OpenCV=4.12.0, scikit-image=0.25.2, PyTorch=2.9.1+cu128
SPLINEOPS_ACCEL=always
OMP_NUM_THREADS=4
Benchmark storage dtype: float32
Total running time of the script: (0 minutes 53.155 seconds)