Spaces:
Sleeping
Sleeping
| """ | |
| ============================================================================= | |
| 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() |