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 quantem.widget as qw
qw.profile()

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
quantem.widget  0.0.1rc44.dev0
  install       editable checkout
  commit        2d009a7f61ee (dirty checkout)
quantem.gpu     0.0.1rc18.dev0
  install       local build
  commit        not recorded
quantem         0.1.10
  install       installed package
  commit        not recorded
webgpu kernels  fa4a86c (fa4a86cfee85), pinned fa4a86cfee85ea719be2dd98206d2098f0dd3344
torch           2.14.1+cu130  device=cpu
python          3.12.15
show2d_gold(size='small') keeps every 8th pixel of the 4096x4096 image (512x512); size='full' loads native pixels.
Source: gold_haadf_npy 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
/tmp/ipykernel_2819/1252247123.py:1: DeprecationWarning: ncols is deprecated; use columns instead. It remains supported during the transition.
  tomography_show2d = 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
/tmp/ipykernel_2819/141420601.py:1: DeprecationWarning: ncols is deprecated; use columns instead. It remains supported during the transition.
  paged_show2d = 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.

live2d = Show2D(dataset2d, labels=["original"], 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"],
)