Show2D#

Open in Colab

Show2D renders one or many 2D images with contrast control, an FFT toggle, ROIs, line profiles, and a calibrated scale bar. It accepts a NumPy array, a PyTorch tensor, or a quantem Dataset2d.

This tutorial uses a real gold HAADF image from the public bobleesj/quantem-data Hugging Face dataset. The full image is 4096 by 4096 pixels; show2d_gold(size="small") returns a calibrated preview so the notebook starts quickly in the documentation site and in Colab.

Tip

Run this exact notebook with the Colab badge above, or View or download this notebook on GitHub. For finished results, use HTML and file export to export interactive HTML or share a trusted notebook with widget state.

import numpy as np
import torch
from quantem.widget import Show2D
from quantem.widget.datasets import show2d_gold

dataset2d = show2d_gold(size="small")
image = dataset2d.array
Source: gold-haadf (widget-tutorials/shared) from Hugging Face
Full image: 4096 x 4096 uint16
Preview: 512 x 512, pixel size 0.1489 nm

Single image#

The HAADF image is wrapped in a quantem Dataset2d, so the calibration travels with the array. The widget reads that metadata automatically and draws a physical scale bar without a widget-level pixel-size argument.

Show2D(dataset2d, cmap="inferno")

Compare independent multi-slice reconstructions#

A tomography or multislice study often produces several reconstruction volumes with different slice counts, slice thicknesses, regularization settings, or reconstruction methods. Pass those volumes as a Python list so each (slices, rows, cols) item becomes one Show2D gallery panel with its own slider and play/pause control. You can stop each reconstruction at the depth where a feature or artifact is clearest while linked zoom and pan preserve the side-by-side spatial comparison.

Use panel_playback_fps=4 for a deliberate comparison cadence without adding another on-screen control. With FFT enabled, each panel-and-slice result is cached on first use, and the previous valid FFT stays visible while a new slice is computed. Every FFT panel shows its own live N.N× multiplier, so independent or linked wheel/pinch zoom remains unambiguous.

The outer list is important: a bare 3D array remains a static gallery of 2D images. Use Show3D when every panel should advance together on one global slice axis, and use Show2D pages when each page should replace the entire panel layout. Put the reconstruction method and slice thickness in each panel label so the comparison stays interpretable.

The compact stacks below are derived from the tutorial’s gold image only to demonstrate the API without a large download. Use real reconstruction volumes for scientific comparison and performance signoff.

Hide code cell source

tomography_base = torch.tensor(image[::4, ::4], dtype=torch.float32)
q01, q99 = torch.quantile(tomography_base, torch.tensor([0.01, 0.99]))
tomography_base = ((tomography_base - q01) / (q99 - q01).clamp_min(1e-6)).clamp(0, 1)
ty, tx = torch.meshgrid(
    torch.linspace(-1, 1, tomography_base.shape[0]),
    torch.linspace(-1, 1, tomography_base.shape[1]),
    indexing="ij",
)


def make_tomography_reconstruction(n_slices, phase, axial_width):
    z = torch.linspace(-1, 1, n_slices)[:, None, None]
    depth_center = 0.22 * torch.sin(4 * tx + phase) * torch.cos(3 * ty - phase)
    axial_envelope = torch.exp(-((z - depth_center) / axial_width) ** 2)
    ringing = 0.05 * torch.cos(16 * tx + 2 * torch.pi * z + phase) * torch.sin(12 * ty - phase)
    return (axial_envelope * tomography_base + ringing).numpy().astype(np.float32, copy=False)


tomography_reconstructions = [
    make_tomography_reconstruction(8, phase=0.0, axial_width=0.70),
    make_tomography_reconstruction(16, phase=0.35, axial_width=0.58),
    make_tomography_reconstruction(12, phase=-0.30, axial_width=0.62),
    make_tomography_reconstruction(20, phase=0.70, axial_width=0.50),
]
tomography_labels = [
    "baseline - 8 slices x 20 A",
    "fine z - 16 slices x 10 A",
    "regularized - 12 slices x 13.3 A",
    "alternative - 20 slices x 8 A",
]
tomography_show2d = Show2D(
    tomography_reconstructions,
    labels=tomography_labels,
    panel_frame_indices=[3, 7, -1, 0],
    panel_playback_fps=4,
    show_fft=True,
    ncols=2,
    cmap="inferno",
    sampling=tuple(4 * float(value) for value in dataset2d.sampling),
    units=dataset2d.units,
    link_zoom=True,
    link_pan=True,
    auto_contrast=True,
)
tomography_show2d.set_panel_frame("fine z - 16 slices x 10 A", 8)
tomography_show2d

Page through a reconstruction sweep#

Use pages when each step has the same panel layout. This compact torch-generated example is intentionally small so the documentation and Colab version open quickly, while still showing the workflow used for lambda or iteration sweeps.

Hide code cell source

torch.manual_seed(3)
page_labels = ["lambda 0.01", "lambda 0.03", "lambda 0.10"]
panel_labels = ["raw", "filtered", "residual", "score"]

n = 160
y, x = torch.meshgrid(
    torch.linspace(-1, 1, n),
    torch.linspace(-1, 1, n),
    indexing="ij",
)
centers = torch.tensor([
    [-0.45, -0.35], [-0.05, -0.40], [0.35, -0.30],
    [-0.35, 0.05], [0.05, 0.02], [0.45, 0.10],
    [-0.18, 0.45], [0.28, 0.48],
])
base = torch.zeros((n, n), dtype=torch.float32)
for cy, cx in centers:
    base += torch.exp(-((x - cx) ** 2 + (y - cy) ** 2) / 0.010)
base = base / base.max()
texture = 0.10 * torch.sin(34 * x + 4 * torch.sin(8 * y)) * torch.cos(28 * y)
background = 0.18 * torch.exp(-((x + 0.70) ** 2 + (y - 0.62) ** 2) / 0.18)

pages = []
for page, (lam, denoise, blur_radius, artifact_strength, shift) in enumerate(zip(
    [0.01, 0.03, 0.10],
    [0.10, 0.45, 0.78],
    [0, 3, 7],
    [0.34, 0.16, 0.05],
    [-12, 0, 12],
)):
    shifted_base = torch.roll(base, shifts=(shift, -shift // 2), dims=(0, 1))
    shifted_texture = torch.roll(texture, shifts=(-shift // 2, shift), dims=(0, 1))
    noise = artifact_strength * torch.randn_like(base)
    stripes = artifact_strength * torch.sin((18 + 90 * lam) * x + (10 + 4 * page) * y)
    raw = shifted_base + shifted_texture + background + noise + stripes
    filtered = raw.clone()
    for _ in range(blur_radius):
        filtered = 0.5 * filtered + 0.125 * (
            torch.roll(filtered, 1, 0)
            + torch.roll(filtered, -1, 0)
            + torch.roll(filtered, 1, 1)
            + torch.roll(filtered, -1, 1)
        )
    filtered = (1 - denoise) * raw + denoise * (0.85 * filtered + 0.15 * shifted_base)
    residual = raw - filtered
    score = 2.5 * torch.abs(residual) + lam * shifted_base
    pages.append(torch.stack([raw, filtered, residual, score]))
page_panels = torch.stack(pages).numpy().astype(np.float32)
paged_show2d = Show2D(
    page_panels,
    labels=panel_labels,
    page_labels=page_labels,
    ncols=4,
    cmap="inferno",
    sampling=(0.025, 0.025),
    units="nm",
    link_contrast=False,
)
paged_show2d.star_page(1)
paged_show2d

Trigger a fresh render in an existing widget#

When a notebook loop produces a new image, keep the same widget object and call set_image(). That method is the render trigger: it sends fresh synced image bytes to the browser and resets stale panel-specific state. Mutating the original NumPy array in place is not enough because the frontend only repaints when a synced trait changes.

Use offline=False for acquisition-style updates so each replacement travels through the live Jupyter Comm path instead of the saved/offline notebook representation.

live2d = Show2D(dataset2d, labels=["original"], offline=False, cmap="inferno")
live2d
updated_gallery = np.stack([
    image,
    np.flipud(image),
    np.clip(image * 0.85 + 0.15 * float(image.mean()), 0, None),
])

live2d.set_image(
    updated_gallery,
    labels=["original", "flipped up/down", "contrast adjusted"],
)