.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples/02_resize/05_how_bad_aliasing_can_be.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code or to run this example in your browser via Binder. .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_02_resize_05_how_bad_aliasing_can_be.py: How Bad Aliasing Can Be ======================= We construct an A/B corner mix image where, in each 2×2 tile, the top-left pixel comes from image A and the other three come from B. We then: 1. Look at the A/B mix at full resolution. 2. Downsample by 0.5 with standard cubic interpolation (no explicit anti-aliasing) and observe a surprising result: the mix collapses to something very close to A. 3. Downsample by 0.5 with an **antialiased** cubic projection and see how proper low-pass filtering preserves the expected 25%/75% mixture in the ROI. This illustrates why anti-aliasing is essential for faithful downsampling. .. GENERATED FROM PYTHON SOURCE LINES 24-26 Imports ------- .. GENERATED FROM PYTHON SOURCE LINES 26-39 .. code-block:: Python import numpy as np import matplotlib.pyplot as plt from urllib.request import urlopen from PIL import Image from splineops.resize import resize from splineops.utils.plotting import show_roi_zoom # Use float32 for storage / IO (resize still computes internally in float64). DTYPE = np.float32 .. GENERATED FROM PYTHON SOURCE LINES 41-45 Load and Prepare Base ROI ------------------------- Load A and B, convert to grayscale, and define the ROI on the originals. .. GENERATED FROM PYTHON SOURCE LINES 45-81 .. code-block:: Python URL_A = "https://r0k.us/graphics/kodak/kodak/kodim14.png" URL_B = "https://r0k.us/graphics/kodak/kodak/kodim08.png" ROI_SIZE_PX = 64 # original ROI side (pixels) FACE_ROW, FACE_COL = 250, 445 # ROI center (approx) in ORIGINAL coordinates ZOOM = (0.5, 0.5) # 0.5× downsampling demo def to_gray01(img_rgb_uint8: np.ndarray) -> np.ndarray: g = img_rgb_uint8.astype(np.float64) / 255.0 gray = 0.2989 * g[..., 0] + 0.5870 * g[..., 1] + 0.1140 * g[..., 2] return gray.astype(DTYPE) with urlopen(URL_A, timeout=10) as resp: A = to_gray01(np.array(Image.open(resp))) with urlopen(URL_B, timeout=10) as resp: B = to_gray01(np.array(Image.open(resp))) assert A.shape == B.shape, "Images A and B must have identical shape." h_img, w_img = A.shape # ORIGINAL canvas size (e.g., 512×768) # Original ROI (face) — top-left corner (for show_roi_zoom) row_top = int(np.clip(FACE_ROW - ROI_SIZE_PX // 2, 0, h_img - ROI_SIZE_PX)) col_left = int(np.clip(FACE_COL - ROI_SIZE_PX // 2, 0, w_img - ROI_SIZE_PX)) # Keep the ROI center as *relative* position for later (downsampled views) rel_center_r = FACE_ROW / h_img rel_center_c = FACE_COL / w_img roi_kwargs_orig = dict( roi_height_frac=ROI_SIZE_PX / h_img, # keeps height at 64 px (square ROI) grayscale=True, roi_xy=(row_top, col_left), # top-left of the ROI ) .. GENERATED FROM PYTHON SOURCE LINES 82-87 Base Image ---------- Construct the synthetic "corner mix" where A occupies the top-left pixel of every 2×2 block, and B fills the other three pixels. .. GENERATED FROM PYTHON SOURCE LINES 87-97 .. code-block:: Python mixed = B.copy() mixed[0::2, 0::2] = A[0::2, 0::2] _ = show_roi_zoom( mixed, ax_titles=("A/B corner mix (A at TL of each 2×2)", None), **roi_kwargs_orig, ) .. image-sg:: /auto_examples/02_resize/images/sphx_glr_05_how_bad_aliasing_can_be_001.png :alt: A/B corner mix (A at TL of each 2×2) :srcset: /auto_examples/02_resize/images/sphx_glr_05_how_bad_aliasing_can_be_001.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 98-105 Downsampling (No Antialiasing) ------------------------------ We first apply a 0.5× downsampling using standard cubic interpolation. To make the behaviour easier to see, we crop the input so that its height and width are odd: this ensures the 0.5× sampling grid lands exactly on the top-left pixel of each 2×2 tile. .. GENERATED FROM PYTHON SOURCE LINES 105-153 .. code-block:: Python H, W = mixed.shape if (H % 2 == 0) or (W % 2 == 0): mixed_odd = mixed[: H - (H % 2 == 0), : W - (W % 2 == 0)] else: mixed_odd = mixed h_odd, w_odd = mixed_odd.shape assert (h_odd % 2 == 1) and (w_odd % 2 == 1), "Expect odd H×W after the crop." res_std = resize( mixed_odd, zoom_factors=ZOOM, method="cubic", # standard (no explicit anti-aliasing) ) def show_resized_on_original_canvas_same_relpos(resized: np.ndarray, title: str): h_res, w_res = resized.shape # EXACT half-size ROI on the resized image roi_h_res = ROI_SIZE_PX // 2 # 64 → 32 roi_w_res = ROI_SIZE_PX // 2 # SAME RELATIVE CENTER as in originals center_r_res = int(round(rel_center_r * h_res)) center_c_res = int(round(rel_center_c * w_res)) # ROI top-left in RESIZED coords, clipped row_top_res = int(np.clip(center_r_res - roi_h_res // 2, 0, h_res - roi_h_res)) col_left_res = int(np.clip(center_c_res - roi_w_res // 2, 0, w_res - roi_w_res)) # Build ORIGINAL-size white canvas and paste resized at (0,0) canvas = np.ones((h_img, w_img), dtype=resized.dtype) canvas[:h_res, :w_res] = resized # Use ORIGINAL canvas height so 32 px is respected visually (no forced shrinking) roi_kwargs_canvas = dict( roi_height_frac=(ROI_SIZE_PX // 2) / h_img, # 32 / original height grayscale=True, roi_xy=(row_top_res, col_left_res), # ROI within the pasted resized patch ) return show_roi_zoom(canvas, ax_titles=(title, None), **roi_kwargs_canvas) _ = show_resized_on_original_canvas_same_relpos( res_std, "Downsampled 0.5× (standard cubic, no AA)" ) .. image-sg:: /auto_examples/02_resize/images/sphx_glr_05_how_bad_aliasing_can_be_002.png :alt: Downsampled 0.5× (standard cubic, no AA) :srcset: /auto_examples/02_resize/images/sphx_glr_05_how_bad_aliasing_can_be_002.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 154-159 Downsampling (Antialiasing) --------------------------- Now we apply downsampling with antialiasing, which performs a projection that includes a matched low-pass filter before decimation. .. GENERATED FROM PYTHON SOURCE LINES 159-170 .. code-block:: Python res_aa = resize( mixed_odd, zoom_factors=ZOOM, method="cubic-antialiasing", # antialiased (oblique projection) cubic ) _ = show_resized_on_original_canvas_same_relpos( res_aa, "Downsampled 0.5× (cubic antialiasing)" ) .. image-sg:: /auto_examples/02_resize/images/sphx_glr_05_how_bad_aliasing_can_be_003.png :alt: Downsampled 0.5× (cubic antialiasing) :srcset: /auto_examples/02_resize/images/sphx_glr_05_how_bad_aliasing_can_be_003.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 171-177 ROI Comparison -------------- To make the difference as clear as possible, we extract the same 32×32 ROI around the same physical location from both downsampled images and display their nearest-neighbour magnifications side by side. .. GENERATED FROM PYTHON SOURCE LINES 177-220 .. code-block:: Python roi_side_res = ROI_SIZE_PX // 2 # 64 → 32 in the downsampled images # ROI in standard cubic result h_std, w_std = res_std.shape center_r_std = int(round(rel_center_r * h_std)) center_c_std = int(round(rel_center_c * w_std)) row_top_std = int(np.clip(center_r_std - roi_side_res // 2, 0, h_std - roi_side_res)) col_left_std = int(np.clip(center_c_std - roi_side_res // 2, 0, w_std - roi_side_res)) roi_std = res_std[row_top_std : row_top_std + roi_side_res, col_left_std : col_left_std + roi_side_res] # ROI in antialiased result (same physical location) h_aa, w_aa = res_aa.shape center_r_aa = int(round(rel_center_r * h_aa)) center_c_aa = int(round(rel_center_c * w_aa)) row_top_aa = int(np.clip(center_r_aa - roi_side_res // 2, 0, h_aa - roi_side_res)) col_left_aa = int(np.clip(center_c_aa - roi_side_res // 2, 0, w_aa - roi_side_res)) roi_aa = res_aa[row_top_aa : row_top_aa + roi_side_res, col_left_aa : col_left_aa + roi_side_res] def _nearest_big(roi: np.ndarray, target_h: int = 256) -> np.ndarray: h, w = roi.shape mag = max(1, int(round(target_h / h))) return np.repeat(np.repeat(roi, mag, axis=0), mag, axis=1) roi_big_std = _nearest_big(roi_std, 256) roi_big_aa = _nearest_big(roi_aa, 256) fig, axes = plt.subplots(1, 2, figsize=(10, 4)) for ax, im, title in zip( axes, [roi_big_std, roi_big_aa], ["Standard cubic (no AA)", "Cubic antialiasing"], ): ax.imshow(im, cmap="gray", interpolation="nearest") ax.set_title(title) ax.axis("off") ax.set_aspect("equal") fig.tight_layout() plt.show() .. image-sg:: /auto_examples/02_resize/images/sphx_glr_05_how_bad_aliasing_can_be_004.png :alt: Standard cubic (no AA), Cubic antialiasing :srcset: /auto_examples/02_resize/images/sphx_glr_05_how_bad_aliasing_can_be_004.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 221-226 Source Images and ROI --------------------- To understand the behaviour, it is helpful to inspect the two source images separately on the original grid, using the same ROI. .. GENERATED FROM PYTHON SOURCE LINES 228-230 Image A ~~~~~~~ .. GENERATED FROM PYTHON SOURCE LINES 230-233 .. code-block:: Python _ = show_roi_zoom(A, ax_titles=("Image A (with ROI)", None), **roi_kwargs_orig) .. image-sg:: /auto_examples/02_resize/images/sphx_glr_05_how_bad_aliasing_can_be_005.png :alt: Image A (with ROI) :srcset: /auto_examples/02_resize/images/sphx_glr_05_how_bad_aliasing_can_be_005.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 234-236 Image B ~~~~~~~ .. GENERATED FROM PYTHON SOURCE LINES 236-239 .. code-block:: Python _ = show_roi_zoom(B, ax_titles=("Image B (with ROI)", None), **roi_kwargs_orig) .. image-sg:: /auto_examples/02_resize/images/sphx_glr_05_how_bad_aliasing_can_be_006.png :alt: Image B (with ROI) :srcset: /auto_examples/02_resize/images/sphx_glr_05_how_bad_aliasing_can_be_006.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 240-264 Discussion ---------- In this synthetic A/B mix, each 2×2 block has A at the top-left pixel and B elsewhere (i.e., 25% A, 75% B per block). The downsampled images tell two very different stories: • **Standard interpolation (cubic)** does no prefiltering before decimation. With our odd-size tweak, the 0.5× sampling grid lands exactly on the 2×2 block corners (the A pixels). So it effectively *picks* A at every step, producing a result that looks almost like a clean version of A in the ROI. The 75% B content is largely aliased away into lower frequencies, which is why the pattern appears to “collapse” to A there. • **Cubic antialiasing** performs a proper low-pass (anti-aliasing) filtering matched to the downsampling, then decimates. On this pattern, that filter averages over each 2×2 neighbourhood, so the result tends toward 25% A + 75% B — visually “more B”, more like the mix. This is exactly what anti-aliasing should do: remove the high-frequency checkerboard content so it doesn’t fold (alias) into the downsample. In short: interpolation without AA does sample-and-aliasing (here it locks onto A due to phase). Antialiased cubic implements the textbook low-pass-then-sample strategy, preserving the true average content. .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 4.655 seconds) .. _sphx_glr_download_auto_examples_02_resize_05_how_bad_aliasing_can_be.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: binder-badge .. image:: images/binder_badge_logo.svg :target: https://mybinder.org/v2/gh/splineops/splineops.github.io/main?urlpath=lab/tree/notebooks_binder/auto_examples/02_resize/05_how_bad_aliasing_can_be.ipynb :alt: Launch binder :width: 150 px .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: 05_how_bad_aliasing_can_be.ipynb <05_how_bad_aliasing_can_be.ipynb>` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: 05_how_bad_aliasing_can_be.py <05_how_bad_aliasing_can_be.py>` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: 05_how_bad_aliasing_can_be.zip <05_how_bad_aliasing_can_be.zip>` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_