image-rescaling-api / src /bilinear_interpolation.py
noureqo's picture
Update src/bilinear_interpolation.py
0f26ba2 verified
"""
=============================================================================
Bilinear Interpolation for Image Rescaling (Headless Backend Engine)
------------------------------------------------------------------
A pedagogical Python implementation demonstrating:
1. Inverse affine mapping from destination -> source coordinates
2. Area-weighted Lagrange (bilinear) interpolation
3. Nearest-neighbor baseline for comparison
Mathematical Formulation
~~~~~~~~~~~~~~~~~~~~~~~~
Given sub-pixel source coordinates (x_i, y_i):
x1 = floor(x_i), x2 = x1 + 1
y1 = floor(y_i), y2 = y1 + 1
dx = x_i - x1 (horizontal fractional distance)
dy = y_i - y1 (vertical fractional distance)
The interpolated intensity is:
f(x_i, y_i) = (1-dx)(1-dy) . f(x1,y1)
+ dx (1-dy) . f(x2,y1)
+ (1-dx) dy . f(x1,y2)
+ dx dy . f(x2,y2)
Stack : Python 3, NumPy, OpenCV (cv2), tqdm (No Matplotlib)
=============================================================================
"""
import numpy as np
import cv2
from tqdm import tqdm
import os
import sys
# =========================================================================
# 1. BILINEAR INTERPOLATION (core algorithm - from scratch)
# =========================================================================
def bilinear_interpolation(src, dst_height, dst_width, disable_tqdm=False):
"""
Rescale *src* to (dst_height x dst_width) using bilinear interpolation.
For every pixel (u, v) in the destination image:
1. Inverse affine mapping: x_i = u / S_x , y_i = v / S_y
2. Four integer neighbours with boundary clamping
3. Fractional distances dx, dy
4. Area-weighted Lagrange formula
"""
src_h, src_w = src.shape[:2]
is_color = (src.ndim == 3)
S_x = dst_height / src_h
S_y = dst_width / src_w
if is_color:
dst = np.zeros((dst_height, dst_width, src.shape[2]), dtype=np.float64)
else:
dst = np.zeros((dst_height, dst_width), dtype=np.float64)
print("\n[BILINEAR] Bilinear Interpolation -- processing rows...")
for u in tqdm(range(dst_height), desc="Bilinear", unit="row",
bar_format="{l_bar}{bar:40}{r_bar}", disable=disable_tqdm):
# Step 1 - Inverse mapping: destination row -> source row
x_i = u / S_x
# Step 2a - Integer neighbours (row) & clamping
x1 = int(np.floor(x_i))
x2 = min(x1 + 1, src_h - 1)
x1 = min(x1, src_h - 1)
# Step 3a - Vertical fractional distance
dx = x_i - int(np.floor(x_i))
for v in range(dst_width):
# Step 1 - Inverse mapping: destination col -> source col
y_i = v / S_y
# Step 2b - Integer neighbours (col) & clamping
y1 = int(np.floor(y_i))
y2 = min(y1 + 1, src_w - 1)
y1 = min(y1, src_w - 1)
# Step 3b - Horizontal fractional distance
dy = y_i - int(np.floor(y_i))
# Step 4 - Bilinear (area-weighted Lagrange) formula
# f(x_i,y_i) = (1-dx)(1-dy).f(x1,y1) + dx(1-dy).f(x2,y1)
# + (1-dx)dy.f(x1,y2) + dx.dy.f(x2,y2)
f_x1y1 = src[x1, y1].astype(np.float64)
f_x2y1 = src[x2, y1].astype(np.float64)
f_x1y2 = src[x1, y2].astype(np.float64)
f_x2y2 = src[x2, y2].astype(np.float64)
dst[u, v] = (
(1 - dx) * (1 - dy) * f_x1y1 +
dx * (1 - dy) * f_x2y1 +
(1 - dx) * dy * f_x1y2 +
dx * dy * f_x2y2
)
return np.clip(dst, 0, 255).astype(np.uint8)
# =========================================================================
# 2. NEAREST-NEIGHBOUR BASELINE
# =========================================================================
def nearest_neighbor(src, dst_height, dst_width, disable_tqdm=False):
"""
Rescale using nearest-neighbour: round to closest source pixel.
Produces characteristic "blocky" artefacts.
"""
src_h, src_w = src.shape[:2]
is_color = (src.ndim == 3)
S_x = dst_height / src_h
S_y = dst_width / src_w
if is_color:
dst = np.zeros((dst_height, dst_width, src.shape[2]), dtype=np.uint8)
else:
dst = np.zeros((dst_height, dst_width), dtype=np.uint8)
print("\n[NN] Nearest-Neighbour -- processing rows...")
for u in tqdm(range(dst_height), desc="Nearest ", unit="row",
bar_format="{l_bar}{bar:40}{r_bar}", disable=disable_tqdm):
x_near = min(int(round(u / S_x)), src_h - 1)
for v in range(dst_width):
y_near = min(int(round(v / S_y)), src_w - 1)
dst[u, v] = src[x_near, y_near]
return dst
# =========================================================================
# 3. REAL IMAGE RESCALING
# =========================================================================
def rescale_image(image_path, scale=2.0):
"""Load a real image, upscale by *scale*, return (original, nn, bilinear)."""
src = cv2.imread(image_path)
if src is None:
print(f"[WARN] Could not read '{image_path}'. Skipping.")
return None, None, None
h, w = src.shape[:2]
new_h, new_w = int(h * scale), int(w * scale)
print("=" * 65)
print(f" IMAGE RESCALING : {os.path.basename(image_path)}")
print(f" {w}x{h} -> {new_w}x{new_h} (scale = {scale}x)")
print("=" * 65)
nn_result = nearest_neighbor(src, new_h, new_w)
bil_result = bilinear_interpolation(src, new_h, new_w)
return src, nn_result, bil_result
# =========================================================================
# 4. GENERATE A SAMPLE TEST IMAGE
# =========================================================================
def generate_sample_image(path="sample_input.png", size=64):
"""Create a small colour test image with gradients and shapes."""
img = np.zeros((size, size, 3), dtype=np.uint8)
for r in range(size):
img[r, :, 2] = int(255 * r / (size - 1))
for c in range(size):
img[:, c, 1] = int(255 * c / (size - 1))
for r in range(size):
for c in range(size):
if abs(r - c) < size // 8:
img[r, c, 0] = 200
s = size // 4
img[s:3*s, s:3*s] = [255, 255, 255]
cv2.circle(img, (size // 2, size // 2), size // 8, (30, 30, 30), -1)
cv2.imwrite(path, img)
print(f"[OK] Generated sample image -> {path} ({size}x{size})")
return path
# =========================================================================
# 5. MAIN ENTRY POINT (Headless execution)
# =========================================================================
def main():
"""
Execution flow
~~~~~~~~~~~~~~
Optionally rescale a real image with tqdm progress (no UI rendering).
"""
sample_path = "sample_input.png"
if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]):
sample_path = sys.argv[1]
print(f"\n[INFO] Using user-supplied image: {sample_path}")
else:
sample_path = generate_sample_image(sample_path, size=64)
scale_factor = 3.0
src_img, nn_img, bil_img = rescale_image(sample_path, scale=scale_factor)
print("\n[DONE] All math operations executed successfully in headless mode.")
# =========================================================================
if __name__ == "__main__":
main()