Open In Colab # Edit2D — All Features Comprehensive demo of the Edit2D crop, pad, and mask tool.

[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
[3]:
import os
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
import numpy as np
import torch
import quantem.widget
from quantem.widget import Edit2D
device = torch.device("mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu")
def make_crystal(size=256, seed=0):
    """Simulate a crystal lattice image with point defects (GPU-accelerated)."""
    gen = torch.Generator(device="cpu").manual_seed(seed)
    y, x = torch.meshgrid(torch.arange(size, device=device, dtype=torch.float32),
                           torch.arange(size, device=device, dtype=torch.float32), indexing="ij")
    # Two-beam lattice fringes
    img = torch.cos(2 * torch.pi * 0.08 * x) + torch.cos(2 * torch.pi * 0.08 * y)
    # Vectorized point defects (no Python loop)
    n_defects = 5
    cy = torch.randint(20, size - 20, (n_defects,), generator=gen).to(device).float()
    cx = torch.randint(20, size - 20, (n_defects,), generator=gen).to(device).float()
    # Broadcast: (n_defects, 1, 1) vs (1, size, size)
    r2 = (y.unsqueeze(0) - cy[:, None, None])**2 + (x.unsqueeze(0) - cx[:, None, None])**2
    img += (3.0 * torch.exp(-r2 / 8)).sum(dim=0)
    noise = torch.randn(size, size, generator=gen).to(device) * 0.2
    img += noise
    return img.cpu().numpy().astype(np.float32)
image = make_crystal(256)
print(f"quantem.widget {quantem.widget.__version__}")
quantem.widget 0.4.0a3

1. Basic Crop#

Drag the crop rectangle interactively, or set bounds programmatically.

[4]:
w = Edit2D(image, title="Basic Crop")
w
[4]:
[5]:
# Read back the crop
print(f"Bounds: {w.crop_bounds}")
print(f"Size:   {w.crop_size}")
print(f"Result: {w.result.shape}")
Bounds: (0, 0, 256, 256)
Size:   (256, 256)
Result: (256, 256)

2. Programmatic Bounds#

Set crop bounds as (top, left, bottom, right) in image coordinates.

[6]:
Edit2D(image, bounds=(50, 50, 200, 200), title="Center Crop (150x150)")
[6]:

3. Padding (Bounds Beyond Image)#

Negative bounds or bounds exceeding image dimensions produce padding with fill_value.

[7]:
w_pad = Edit2D(image, bounds=(-30, -30, 286, 286), fill_value=0.0, title="Padded (30px border)")
w_pad
[7]:
[8]:
result = w_pad.result
print(f"Padded result: {result.shape}")  # 316x316
print(f"Corner value (should be fill): {result[0, 0]}")
print(f"Center value (should be data): {result[30, 30]:.4f}")
Padded result: (316, 316)
Corner value (should be fill): 0.0
Center value (should be data): 2.0983

4. Mask Mode#

Paint a binary mask over the image. Masked pixels are set to fill_value in the result.

[9]:
w_mask = Edit2D(image, mode="mask", fill_value=0.0, title="Mask Mode")
w_mask
[9]:
[10]:
from quantem.widget import Show2D
# Create a programmatic circular mask (e.g., masking a contamination spot)
mask = np.zeros((256, 256), dtype=np.uint8)
cy, cx, r = 128, 128, 40
yy, xx = np.ogrid[:256, :256]
mask[(yy - cy)**2 + (xx - cx)**2 <= r**2] = 1
w_mask.mask_bytes = mask.tobytes()
print(f"Mask shape: {w_mask.mask.shape}")
print(f"Masked pixels: {w_mask.mask.sum()}")
# View the masked result — masked region is replaced with fill_value
Show2D(w_mask.result, title="Masked Result (center spot removed)")
Mask shape: (256, 256)
Masked pixels: 5025
[10]:

5. Multi-Image Mode#

Apply the same crop/mask to multiple images at once.

[11]:
images = [make_crystal(256, seed=i) for i in range(3)]
w_multi = Edit2D(images, labels=["Crystal A", "Crystal B", "Crystal C"],
                 bounds=(30, 30, 220, 220), title="Multi-Image Crop")
w_multi
[11]:
[12]:
results = w_multi.result
print(f"Number of results: {len(results)}")
for i, r in enumerate(results):
    print(f"  Image {i}: {r.shape}")
Number of results: 3
  Image 0: (190, 190)
  Image 1: (190, 190)
  Image 2: (190, 190)

6. Display Options#

Log scale, colormap, and scale bar.

[13]:
Edit2D(image, cmap="viridis", log_scale=True,
       pixel_size=2.5, title="Viridis + Log Scale")
[13]:

7. Replace Data with set_image()#

Swap the underlying data while preserving display settings.

[14]:
w_swap = Edit2D(image, cmap="inferno", title="Original")
print(f"Before: {w_swap.height}x{w_swap.width}")
new_image = make_crystal(128, seed=42)
w_swap.set_image(new_image)
print(f"After:  {w_swap.height}x{w_swap.width}")
print(f"Cmap preserved: {w_swap.cmap}")
w_swap
Before: 256x256
After:  128x128
Cmap preserved: inferno
[14]:

8. State Persistence#

Save and restore widget settings across sessions.

[15]:
w_state = Edit2D(image, cmap="plasma", bounds=(20, 30, 200, 220),
                 fill_value=5.0, title="State Demo")
w_state.summary()
State Demo
════════════════════════════════
Image:    256×256
Mode:     crop
Crop:     (20, 30) → (200, 220)  = 180×190
Fill:     5.0
Display:  plasma | auto | linear
[16]:
# Save and inspect
w_state.save("/tmp/edit2d_state.json")
print(w_state.state_dict())
{'title': 'State Demo', 'cmap': 'plasma', 'mode': 'crop', 'log_scale': False, 'auto_contrast': True, 'show_controls': True, 'show_stats': True, 'show_display_controls': True, 'show_edit_controls': True, 'show_histogram': True, 'disabled_tools': [], 'hidden_tools': [], 'pixel_size': 0.0, 'fill_value': 5.0, 'crop_top': 20, 'crop_left': 30, 'crop_bottom': 200, 'crop_right': 220, 'shared': True}
[17]:
# Restore from file
w2 = Edit2D(image, state="/tmp/edit2d_state.json")
w2.summary()
State Demo
════════════════════════════════
Image:    256×256
Mode:     crop
Crop:     (20, 30) → (200, 220)  = 180×190
Fill:     5.0
Display:  plasma | auto | linear
[18]:
import os
os.remove("/tmp/edit2d_state.json")

9. Per-Image Independent Editing (shared=False)#

By default, Edit2D applies the same crop/mask to all images (shared=True). Set shared=False to edit each image independently — different crop regions, different masks. Toggle the Link switch in the navigation bar to switch modes interactively. Use cases:

  • Different contamination regions to mask per image

  • Different features to center on per sample

  • Asymmetric cropping for images with shifted fields of view

[19]:
# Three crystal images with defects in different locations
images = [make_crystal(256, seed=i) for i in range(3)]
# Initial bounds apply to all images as starting state
w_indep = Edit2D(images, shared=False,
                 bounds=(30, 30, 220, 220),
                 labels=["Sample A", "Sample B", "Sample C"],
                 title="Independent Crop")
w_indep
[19]:
[20]:
# Each image gets its own crop — result sizes can differ
results = w_indep.result
print(f"Number of results: {len(results)}")
for i, r in enumerate(results):
    print(f"  {w_indep.labels[i]}: {r.shape}")
print(f"\nShared: {w_indep.shared}")
print(f"Current image bounds: {w_indep.crop_bounds}")
print(f"Current image size:   {w_indep.crop_size}")
# Programmatic per-image crop adjustment via setter
w_indep.selected_idx = 1
w_indep.crop_bounds = (50, 50, 200, 200)
print(f"\nAfter adjusting Sample B:")
print(f"  Sample B bounds: {w_indep.crop_bounds}")
w_indep.selected_idx = 0
print(f"  Sample A bounds: {w_indep.crop_bounds}  (unchanged)")
Number of results: 3
  Sample A: (190, 190)
  Sample B: (190, 190)
  Sample C: (190, 190)

Shared: False
Current image bounds: (30, 30, 220, 220)
Current image size:   (190, 190)

After adjusting Sample B:
  Sample B bounds: (50, 50, 200, 200)
  Sample A bounds: (30, 30, 220, 220)  (unchanged)
[21]:
# Independent mask mode — each image gets its own mask
w_indep_mask = Edit2D(images, shared=False, mode="mask",
                      labels=["Sample A", "Sample B", "Sample C"],
                      title="Independent Mask")
w_indep_mask
[21]:
[22]:
# State persistence works with independent mode
w_indep.summary()
print()
print(repr(w_indep))
Independent Crop
════════════════════════════════
Image:    256×256 (3 images, independent)
Mode:     crop
Crop:     (30, 30) → (220, 220)  = 190×190
Fill:     0.0
Display:  gray | auto | linear

Edit2D(256x256, 3 images, crop=190x190 at (30,30), fill=0.0, independent)