[1]:
# Install in Google Colab
try:
    import google.colab
    !pip install -q -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ quantem-widget
except ImportError:
    pass  # Not in Colab, skip
[2]:
try:
    %load_ext autoreload
    %autoreload 2
    %env ANYWIDGET_HMR=1
except Exception:
    pass  # autoreload unavailable (Colab Python 3.12+)
env: ANYWIDGET_HMR=1

Open In Colab # Show3D: Complete Feature Demo Demonstrates all Show3D features using realistic electron microscopy synthetic data. Features: playback, ROI (circle/square/rectangle/annular), FFT with d-spacing, line profile, crosshair, inset lens, ROI sparkline plot, drag-resize handles, method chaining, dimension labels, path animation, bookmarks, log scale, scale bar, timestamps.

[3]:
import os
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
import torch
import numpy as np
import quantem.widget
from quantem.widget import Show3D
device = torch.device("mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
def make_focal_series(n_frames=30, size=256):
    """Through-focus series: nanoparticles with Fresnel fringes at edges."""
    yy, xx = torch.meshgrid(
        torch.arange(size, device=device, dtype=torch.float32),
        torch.arange(size, device=device, dtype=torch.float32),
        indexing="ij",
    )
    xx = xx.unsqueeze(0).unsqueeze(0)  # (1, 1, H, W)
    yy = yy.unsqueeze(0).unsqueeze(0)
    particles = [
        (size * 0.35, size * 0.4, 18, 1.0),
        (size * 0.65, size * 0.55, 25, 0.7),
        (size * 0.45, size * 0.7, 12, 1.2),
        (size * 0.7, size * 0.3, 15, 0.9),
    ]
    n_p = len(particles)
    cx = torch.tensor([p[0] for p in particles], device=device, dtype=torch.float32).reshape(1, n_p, 1, 1)
    cy = torch.tensor([p[1] for p in particles], device=device, dtype=torch.float32).reshape(1, n_p, 1, 1)
    pr = torch.tensor([p[2] for p in particles], device=device, dtype=torch.float32).reshape(1, n_p, 1, 1)
    pz = torch.tensor([p[3] for p in particles], device=device, dtype=torch.float32).reshape(1, n_p, 1, 1)
    defocus = torch.linspace(-60, 60, n_frames, device=device, dtype=torch.float32).reshape(n_frames, 1, 1, 1)
    dist = torch.sqrt((xx - cx) ** 2 + (yy - cy) ** 2)  # (1, n_p, H, W)
    edge = 1.0 / (1 + torch.exp((dist - pr) * 2))
    abs_df = defocus.abs()
    sigma = 3 + abs_df * 0.15
    diff = dist - pr
    fresnel = torch.cos(0.005 * defocus * diff ** 2) * torch.exp(
        -(diff ** 2) / (2 * sigma ** 2)
    )
    defocused = (abs_df > 3).float()
    in_focus = 1.0 - defocused
    contribution = pz * (
        defocused * (edge * 0.3 + fresnel * 0.2 * torch.sign(defocus))
        + in_focus * (edge * 0.4)
    )
    frames = 0.5 + contribution.sum(dim=1)
    noise = torch.from_numpy(
        np.random.normal(0, 0.03, (n_frames, size, size)).astype(np.float32)
    ).to(device)
    frames = frames + noise
    return frames.cpu().numpy()
def make_insitu_growth(n_frames=40, size=128):
    """Nanoparticle nucleation and growth over time."""
    yy, xx = torch.meshgrid(
        torch.arange(size, device=device, dtype=torch.float32),
        torch.arange(size, device=device, dtype=torch.float32),
        indexing="ij",
    )
    xx = xx.unsqueeze(0).unsqueeze(0)  # (1, 1, H, W)
    yy = yy.unsqueeze(0).unsqueeze(0)
    sites = [(30, 40, 5), (80, 60, 8), (50, 90, 12), (100, 100, 3), (60, 30, 18)]
    n_s = len(sites)
    sx = torch.tensor([s[0] for s in sites], device=device, dtype=torch.float32).reshape(1, n_s, 1, 1)
    sy = torch.tensor([s[1] for s in sites], device=device, dtype=torch.float32).reshape(1, n_s, 1, 1)
    st = torch.tensor([s[2] for s in sites], device=device, dtype=torch.float32).reshape(1, n_s, 1, 1)
    # Frame indices: (n_frames, 1, 1, 1)
    f_idx = torch.arange(n_frames, device=device, dtype=torch.float32).reshape(n_frames, 1, 1, 1)
    # Growth factor: clamp((f - t_start) / 15, 0, 1), zero when f < t_start
    elapsed = f_idx - st  # (n_frames, n_s, 1, 1)
    active = (elapsed >= 0).float()
    growth = torch.clamp(elapsed / 15.0, 0.0, 1.0) * active
    # Radius per site per frame
    radius = 3 + growth * 12  # (n_frames, n_s, 1, 1)
    # Distance from each site: (1, n_s, H, W)
    dist = torch.sqrt((xx - sx) ** 2 + (yy - sy) ** 2)
    # Gaussian contribution: (n_frames, n_s, H, W)
    amplitude = (0.5 + 0.3 * growth) * active
    contribution = amplitude * torch.exp(-dist ** 2 / (2 * radius ** 2))
    # Sum over sites -> (n_frames, H, W)
    frames = contribution.sum(dim=1)
    # Background noise
    noise = torch.from_numpy(
        np.random.normal(0.1, 0.02, (n_frames, size, size)).astype(np.float32)
    ).to(device)
    frames = frames + noise
    return frames.cpu().numpy()
def make_lattice_rotation(n_frames=20, size=128):
    """Crystal with rotating lattice fringes (grain rotation in-situ)."""
    yy, xx = torch.meshgrid(
        torch.arange(size, device=device, dtype=torch.float32),
        torch.arange(size, device=device, dtype=torch.float32),
        indexing="ij",
    )
    xx = xx.unsqueeze(0)  # (1, H, W)
    yy = yy.unsqueeze(0)
    # Angles: (n_frames, 1, 1)
    angles = (torch.arange(n_frames, device=device, dtype=torch.float32) * torch.pi / (2 * n_frames)).reshape(n_frames, 1, 1)
    freq = 0.08
    # First set of fringes
    frames = torch.cos(
        2 * torch.pi * freq * (xx * torch.cos(angles) + yy * torch.sin(angles))
    )
    # Second set of fringes (rotated by pi/3)
    angles2 = angles + torch.pi / 3
    frames = frames + 0.5 * torch.cos(
        2 * torch.pi * freq * 1.5 * (
            xx * torch.cos(angles2) + yy * torch.sin(angles2)
        )
    )
    # Add noise
    noise = torch.from_numpy(
        np.random.normal(0, 0.2, (n_frames, size, size)).astype(np.float32)
    ).to(device)
    frames = frames + noise
    return frames.cpu().numpy().astype(np.float32)
def make_haadf_stack(n_frames=25, size=128):
    """HAADF-STEM image stack with Z-contrast columns and scan noise."""
    yy, xx = torch.meshgrid(
        torch.arange(size, device=device, dtype=torch.float32),
        torch.arange(size, device=device, dtype=torch.float32),
        indexing="ij",
    )
    # Generate atomic columns on a grid
    spacing = 16
    columns = []
    for row in range(4, size - 4, spacing):
        for col in range(4, size - 4, spacing):
            z_contrast = np.random.uniform(0.4, 1.0)
            columns.append((col, row, z_contrast))
    n_c = len(columns)
    col_x = torch.tensor([c[0] for c in columns], device=device, dtype=torch.float32).reshape(1, n_c, 1, 1)
    col_y = torch.tensor([c[1] for c in columns], device=device, dtype=torch.float32).reshape(1, n_c, 1, 1)
    col_z = torch.tensor([c[2] for c in columns], device=device, dtype=torch.float32).reshape(1, n_c, 1, 1)
    xx_4d = xx.unsqueeze(0).unsqueeze(0)  # (1, 1, H, W)
    yy_4d = yy.unsqueeze(0).unsqueeze(0)
    # Scan distortions: (n_frames, 1, 1, 1)
    dx = torch.from_numpy(np.random.normal(0, 0.3, n_frames).astype(np.float32)).to(device).reshape(n_frames, 1, 1, 1)
    dy = torch.from_numpy(np.random.normal(0, 0.3, n_frames).astype(np.float32)).to(device).reshape(n_frames, 1, 1, 1)
    # Distance from each column with scan distortion: (n_frames, n_c, H, W)
    dist2 = (xx_4d - col_x - dx) ** 2 + (yy_4d - col_y - dy) ** 2
    # Gaussian columns: (n_frames, n_c, H, W)
    contribution = col_z * torch.exp(-dist2 / (2 * 2.5 ** 2))
    # Sum over columns -> (n_frames, H, W)
    frames = contribution.sum(dim=1)
    # Background noise
    noise = torch.from_numpy(
        np.random.normal(0.05, 0.01, (n_frames, size, size)).astype(np.float32)
    ).to(device)
    frames = frames + noise
    # Bright contamination region (upper-right), same for all frames
    bright_dist = torch.sqrt((xx - size * 0.8) ** 2 + (yy - size * 0.2) ** 2)
    contamination = 0.6 * torch.exp(-bright_dist ** 2 / (2 * 15 ** 2))
    frames = frames + contamination.unsqueeze(0)
    return frames.cpu().numpy()
def make_hdr_stack(n_frames=20, size=128):
    """High dynamic range stack (bright diffraction spots on dark background)."""
    yy, xx = torch.meshgrid(
        torch.arange(size, device=device, dtype=torch.float32),
        torch.arange(size, device=device, dtype=torch.float32),
        indexing="ij",
    )
    cx, cy = size // 2, size // 2
    # Exponential background noise (use NumPy, then convert)
    bg = torch.from_numpy(
        np.random.exponential(0.5, (n_frames, size, size)).astype(np.float32)
    ).to(device)
    # Central beam: same for all frames
    dist_center = (xx - cx) ** 2 + (yy - cy) ** 2
    central = 1000 * torch.exp(-dist_center / (2 * 3 ** 2))  # (H, W)
    frames = bg + central.unsqueeze(0)
    # Diffraction spots: 6 spots rotating with frame
    # angle_offset per frame: (n_frames, 1)
    f_idx = torch.arange(n_frames, device=device, dtype=torch.float32)
    angle_offset = (f_idx * torch.pi / (4 * n_frames)).reshape(n_frames, 1)
    # 6 spot angles: (1, 6)
    k_angles = (torch.arange(6, device=device, dtype=torch.float32) * torch.pi / 3).reshape(1, 6)
    # Total angle per frame per spot: (n_frames, 6)
    theta = k_angles + angle_offset
    # Spot positions: (n_frames, 6)
    spot_x = cx + 30 * torch.cos(theta)
    spot_y = cy + 30 * torch.sin(theta)
    # Reshape for broadcasting: (n_frames, 6, 1, 1) vs (1, 1, H, W)
    spot_x = spot_x.reshape(n_frames, 6, 1, 1)
    spot_y = spot_y.reshape(n_frames, 6, 1, 1)
    xx_4d = xx.unsqueeze(0).unsqueeze(0)  # (1, 1, H, W)
    yy_4d = yy.unsqueeze(0).unsqueeze(0)
    d2 = (xx_4d - spot_x) ** 2 + (yy_4d - spot_y) ** 2  # (n_frames, 6, H, W)
    spots = (200 * torch.exp(-d2 / (2 * 2 ** 2))).sum(dim=1)  # (n_frames, H, W)
    frames = frames + spots
    return frames.cpu().numpy()
print("Data generators ready.")
print(f"quantem.widget {quantem.widget.__version__}")
Using device: mps
Data generators ready.
quantem.widget 0.4.0a3

1. Basic Stack#

Through-focus series of nanoparticles. Fresnel fringes appear at particle edges when defocused.

[4]:
focal_stack = make_focal_series(n_frames=30, size=256)
defocus_values = np.linspace(-60, 60, 30)
labels = [f"C10={df:.0f} nm" for df in defocus_values]
Show3D(
    focal_stack,
    labels=labels,
    title="Through-Focus Series: Nanoparticles",
    cmap="gray",
)
[4]:

2. PyTorch Tensor#

Same focal series converted to a PyTorch tensor. Show3D accepts both NumPy and PyTorch.

[5]:
import torch
focal_torch = torch.from_numpy(focal_stack)
print(f"Tensor shape: {focal_torch.shape}, dtype: {focal_torch.dtype}")
Show3D(
    focal_torch,
    labels=labels,
    title="Through-Focus (PyTorch Tensor)",
    cmap="gray",
)
Tensor shape: torch.Size([30, 256, 256]), dtype: torch.float32
[5]:

3. Playback Controls#

Use .play(), .pause(), and .stop() to control playback programmatically.

[6]:
w_playback = Show3D(
    focal_stack,
    labels=labels,
    title="Playback Demo -- use play/pause/stop buttons",
    cmap="gray",
    fps=10,
)
w_playback
[6]:
[7]:
# Programmatic control:
# w_playback.play()   # Start playing
# w_playback.pause()  # Pause
# w_playback.stop()   # Stop and reset to frame 0

4. Custom FPS, Loop Range, Reverse#

In-situ nanoparticle nucleation and growth. Loop over just the nucleation burst (frames 3-20), play in reverse.

[8]:
growth_stack = make_insitu_growth(n_frames=40, size=128)
w_growth = Show3D(
    growth_stack,
    title="In-Situ Nanoparticle Growth",
    cmap="inferno",
    fps=12,
)
w_growth.reverse = True
w_growth.loop_start = 3
w_growth.loop_end = 20
w_growth
[8]:

5. Labels and Timestamps#

Physical time labels for each frame of the in-situ growth experiment.

[9]:
n_growth = 40
time_seconds = np.linspace(0, 120, n_growth)  # 2 minutes total
time_labels = [f"t={t:.1f} s" for t in time_seconds]
Show3D(
    growth_stack,
    labels=time_labels,
    title="In-Situ Growth with Timestamps",
    cmap="inferno",
    timestamps=time_seconds.tolist(),
    timestamp_unit="s",
    fps=8,
)
[9]:

6. ROI – Circle#

Circular ROI on the HAADF stack, placed over the bright contamination region.

[10]:
haadf_stack = make_haadf_stack(n_frames=25, size=128)
w_roi_circle = Show3D(
    haadf_stack,
    title="HAADF Stack -- Circle ROI on bright region",
    cmap="viridis",
)
# Place circle ROI over the bright contamination spot (upper-right)
w_roi_circle.set_roi(row=26, col=102, radius=18)
w_roi_circle
[10]:

7. ROI – Square and Rectangle#

Square and rectangle ROI shapes on the same HAADF data.

[11]:
w_roi_sq = Show3D(
    haadf_stack,
    title="HAADF Stack -- Square ROI",
    cmap="viridis",
)
w_roi_sq.set_roi(row=64, col=64, radius=20)
w_roi_sq.roi_square(20)
w_roi_sq
[11]:
[12]:
w_roi_rect = Show3D(
    haadf_stack,
    title="HAADF Stack -- Rectangle ROI",
    cmap="viridis",
)
w_roi_rect.set_roi(row=50, col=80)
w_roi_rect.roi_rectangle(width=40, height=20)
w_roi_rect
[12]:

8. FFT Panel#

Crystal lattice with rotating fringes (simulating grain rotation during in-situ heating). The FFT panel reveals the changing spot pattern as the lattice rotates.

[13]:
lattice_stack = make_lattice_rotation(n_frames=20, size=128)
angle_labels = [f"{a:.1f} deg" for a in np.linspace(0, 90, 20)]
Show3D(
    lattice_stack,
    labels=angle_labels,
    title="Lattice Rotation -- FFT shows spot migration",
    cmap="gray",
    show_fft=True,
    fps=4,
)
[13]:

9. Annular ROI#

Donut-shaped ROI with separate inner and outer radii. Drag resize handles to adjust. The ROI sparkline plot shows mean intensity across all frames.

[14]:
w_annular = Show3D(
    haadf_stack,
    title="HAADF Stack -- Annular ROI",
    cmap="viridis",
)
# Place annular ROI centered on a nanoparticle
w_annular.set_roi(row=60, col=80).roi_annular(inner=5, outer=15)
w_annular
[14]:

10. Colormaps#

Different colormaps applied to the HAADF-like Z-contrast stack.

[15]:
Show3D(
    haadf_stack,
    title="HAADF Stack -- inferno colormap",
    cmap="inferno",
)
[15]:

11. Log Scale + Auto Contrast#

High dynamic range diffraction pattern stack. The central beam is ~1000x brighter than the background. Log scale and auto-contrast reveal the weak diffraction spots.

[16]:
hdr_stack = make_hdr_stack(n_frames=20, size=128)
print(f"HDR range: [{hdr_stack.min():.1f}, {hdr_stack.max():.1f}]")
Show3D(
    hdr_stack,
    title="Diffraction Stack -- log scale + auto contrast",
    cmap="hot",
    log_scale=True,
    auto_contrast=True,
    percentile_low=2.0,
    percentile_high=99.5,
)
HDR range: [0.0, 1001.4]
[16]:

12. Scale Bar#

Focal series with a calibrated pixel size of 0.25 nm/px (typical HRTEM).

[17]:
Show3D(
    focal_stack,
    labels=labels,
    title="HRTEM Focal Series -- 0.25 nm/px",
    cmap="gray",
    pixel_size=0.25,
)
[17]:

13. Boomerang (Ping-Pong)#

Plays forward then backward. Useful for oscillating phenomena like beam-induced motion.

[18]:
w_boom = Show3D(
    growth_stack,
    title="In-Situ Growth -- Boomerang mode",
    cmap="inferno",
    fps=15,
)
w_boom.boomerang = True
w_boom
[18]:

14. Bookmarks#

Mark interesting defocus values: the in-focus frame and the two extremes where Fresnel fringes are strongest.

[19]:
w_bm = Show3D(
    focal_stack,
    labels=labels,
    title="Focal Series -- Bookmarked frames",
    cmap="gray",
)
# Bookmark: underfocus extreme, in-focus, overfocus extreme
w_bm.bookmarked_frames = [0, 15, 29]
w_bm
[19]:

15. Manual vmin/vmax#

Clip the display range to highlight subtle contrast differences in the growth series.

[20]:
Show3D(
    growth_stack,
    title="In-Situ Growth -- Manual vmin/vmax",
    cmap="inferno",
    vmin=0.0,
    vmax=0.5,
)
[20]:

16. Hide Stats#

Clean view with the statistics panel hidden.

[21]:
Show3D(
    lattice_stack,
    labels=angle_labels,
    title="Lattice Rotation -- stats hidden",
    cmap="gray",
    show_stats=False,
)
[21]:

17. Line Profile#

Click two points on the image to draw a line profile. The sparkline below shows intensity along the line. Toggle with the “Profile:” switch in the header.

[22]:
w_profile = Show3D(
    lattice_stack,
    title="Lattice Rotation -- Line Profile",
    cmap="gray",
    pixel_size=0.2,  # 0.2 nm/px
)
# Set a profile line programmatically across the lattice fringes
w_profile.set_profile((20, 10), (100, 120))
print(f"Profile distance: {w_profile.profile_distance:.2f} nm")
print(f"Profile values shape: {w_profile.profile_values.shape}")
w_profile
Profile distance: 27.20 nm
Profile values shape: (137,)
[22]:

18. Dimension Label#

Custom axis labels instead of “Frame”. Useful for defocus series, dose fractionation, tilt series, etc.

[23]:
Show3D(
    focal_stack,
    labels=labels,
    title="Through-Focus Series",
    cmap="gray",
    dim_label="Defocus",  # Keyboard shortcuts will say "Prev / Next defocus"
)
[23]:

19. Method Chaining#

All public methods return self for fluent API. Chain goto(), play(), set_roi(), ROI shape methods, profile, and playback path calls.

[24]:
# Fluent API: chain multiple calls
w_chain = (
    Show3D(growth_stack, title="Method Chaining Demo", cmap="inferno")
    .goto(10)
    .set_roi(row=80, col=60, radius=15)
    .roi_circle(12)
)
print(f"Current frame: {w_chain.slice_idx}, ROI active: {w_chain.roi_active}")
w_chain
Current frame: 10, ROI active: True
[24]:

20. Crosshair + Inset Lens#

Toggle “Crosshair:” for full-width/height lines at cursor. Toggle “Lens:” for a magnified inset (2-8x) that follows the cursor. Press C to copy coordinates.

[25]:
# Crosshair and Lens are interactive toggles in the header row.
# Hover over the image to see the crosshair and lens in action.
Show3D(
    haadf_stack,
    title="HAADF -- try Crosshair + Lens toggles",
    cmap="viridis",
)
[25]:

21. Path Animation#

Custom playback order: visit specific frames in any sequence instead of sequential playback.

[26]:
# Play only the "interesting" frames: underfocus extreme, near-focus, overfocus extreme
interesting_frames = [0, 5, 10, 15, 20, 25, 29, 25, 20, 15, 10, 5]
w_path = Show3D(
    focal_stack,
    labels=labels,
    title="Path Animation -- custom frame order",
    cmap="gray",
    fps=3,
)
w_path.set_playback_path(interesting_frames)
print(f"Playback path: {w_path.playback_path}")
# Press play to see the custom sequence
w_path
Playback path: [0, 5, 10, 15, 20, 25, 29, 25, 20, 15, 10, 5]
[26]:

22. ROI Sparkline Plot#

When ROI is active, a sparkline below the image shows the mean intensity across all frames. Toggle with “Plot:” switch. The current frame is marked with a vertical line and dot.

[27]:
# The ROI plot shows how mean intensity evolves across frames
w_plot = Show3D(
    growth_stack,
    title="In-Situ Growth -- ROI sparkline tracks nucleation",
    cmap="inferno",
)
# Place ROI on a nucleation site to see intensity ramp up
w_plot.set_roi(row=60, col=80, radius=10)
w_plot
[27]:

23. Drag-Resize Handles#

All ROI shapes have drag-resize handles (small circles at the edge). Click and drag the handle to resize. Annular ROI has two handles: one for the inner radius (cyan) and one for the outer radius (green).

[28]:
# Try dragging the green/cyan handle dots to resize the ROI interactively
w_handles = Show3D(
    haadf_stack,
    title="Drag-Resize Handles -- grab the dots!",
    cmap="viridis",
)
w_handles.set_roi(row=64, col=64).roi_annular(inner=8, outer=20)
w_handles
[28]:

24. State Persistence#

Save and restore all display settings — colormap, playback config, ROI, bookmarks, profile line — to a JSON file. Resume analysis after a kernel restart or share exact display state with a colleague.

[29]:
# Inspect current state
w_state = Show3D(
    focal_stack, labels=labels,
    title="Focal Series Analysis",
    cmap="viridis", fps=12, boomerang=True,
    pixel_size=0.25,
)
w_state.bookmarked_frames = [0, 15, 29]
w_state.summary()
w_state
Focal Series Analysis
════════════════════════════════
Stack:    30×256×256 (0.25 Å/px)
Frame:    15/29 [C10=2 nm]
Data:     min=0.1712  max=1.167  mean=0.5168
Display:  viridis | manual contrast | linear
Playback: 12.0 fps | loop=on | reverse=off | boomerang=on
[29]:
[30]:
# Save state to JSON
w_state.save("show3d_state.json")
print("Saved to show3d_state.json")
# Inspect the state dict
import json
print(json.dumps(w_state.state_dict(), indent=2))
Saved to show3d_state.json
{
  "title": "Focal Series Analysis",
  "cmap": "viridis",
  "log_scale": false,
  "auto_contrast": false,
  "percentile_low": 1.0,
  "percentile_high": 99.0,
  "show_stats": true,
  "show_controls": true,
  "show_fft": false,
  "show_playback": false,
  "disabled_tools": [],
  "hidden_tools": [],
  "pixel_size": 0.25,
  "scale_bar_visible": true,
  "canvas_size": 0,
  "fps": 12.0,
  "loop": true,
  "reverse": false,
  "boomerang": true,
  "loop_start": 0,
  "loop_end": -1,
  "bookmarked_frames": [
    0,
    15,
    29
  ],
  "playback_path": [],
  "roi_active": false,
  "roi_list": [],
  "roi_selected_idx": -1,
  "profile_line": [],
  "profile_width": 1,
  "diff_mode": "off",
  "dim_label": "Frame",
  "timestamp_unit": "s"
}
[31]:
# Restore from file — all settings come back
w_restored = Show3D(focal_stack, labels=labels, state="show3d_state.json")
print(f"Restored: cmap={w_restored.cmap}, fps={w_restored.fps}, boomerang={w_restored.boomerang}")
print(f"Bookmarks: {w_restored.bookmarked_frames}")
w_restored
Restored: cmap=viridis, fps=12.0, boomerang=True
Bookmarks: [0, 15, 29]
[31]:

25. Diff Mode#

Frame differencing for detecting changes between acquisitions. diff_mode="previous" shows frame[i] - frame[i-1] (frame-to-frame changes). diff_mode="first" shows frame[i] - frame[0] (cumulative change from the first frame). An orange badge appears in the title when diff is active.

[32]:
# Frame-to-frame differencing — highlights what changed between consecutive frames
w_diff_prev = Show3D(
    growth_stack,
    title="In-Situ Growth -- Diff vs Previous",
    cmap="gray",
    diff_mode="previous",
)
w_diff_prev
[32]:
[33]:
# Cumulative change from first frame — shows total accumulated change
w_diff_first = Show3D(
    growth_stack,
    title="In-Situ Growth -- Diff vs First",
    cmap="gray",
    diff_mode="first",
)
w_diff_first
[33]:

26. Profile All Frames#

Extract the same line profile from every frame in one call. Returns a (n_slices, n_points) array — pass directly to Show1D for multi-trace comparison. One-liner replacement for manual per-frame profile extraction loops.

[34]:
from quantem.widget import Show1D
# Set a profile line across the lattice fringes
w_profile_all = Show3D(
    lattice_stack,
    title="Lattice Rotation -- Profile All Frames",
    cmap="gray",
    pixel_size=0.2,
)
w_profile_all.set_profile((20, 10), (100, 120))
# Extract profile from every frame in one call
all_profiles = w_profile_all.profile_all_frames()
print(f"Profile array shape: {all_profiles.shape}")  # (20, n_points)
# Display in Show1D — each frame's profile as a separate trace
Show1D(
    all_profiles,
    labels=angle_labels,
    title="Line profile across all frames",
    x_label="Distance",
    x_unit="nm",
    y_label="Intensity",
    show_grid=True,
)
Profile array shape: (20, 137)
[34]:
[35]:
# Clean up
from pathlib import Path
Path("show3d_state.json").unlink(missing_ok=True)