Mask Editor and Q-Map Tutorial

This tutorial shows how to use the SimpleMask module to create detector masks, compute Q-maps from geometry metadata, and generate Q-space partitions for XPCS analysis.

What you’ll learn

  • Initialize the SimpleMask kernel in headless (no-GUI) mode

  • Load detector data with geometry metadata

  • Inspect and visualize the Q-map

  • Create and modify masks programmatically

  • Save and load masks in HDF5 format

  • Compute Q-phi partitions for correlation analysis

  • Validate Q-map data with the schema system

For a task-oriented reference of the GUI tools and shortcuts, see Mask Editor Guide.

Overview

The mask editor pipeline has three stages:

  1. Load detector data – Provide a 2D detector image and geometry metadata.

  2. Create a mask – Mark bad pixels, beamstop regions, and detector gaps.

  3. Compute partition – Bin the detector into Q-phi regions for correlation.

The kernel class SimpleMaskKernel handles all computation. The GUI-level SimpleMaskWindow wraps it with interactive drawing tools.

Step 1 – Initialize the Kernel

The kernel can run without a GUI, which is useful for scripted pipelines and batch processing:

import numpy as np
from xpcsviewer.simplemask.simplemask_kernel import SimpleMaskKernel

# Create kernel without GUI handles (headless mode)
kernel = SimpleMaskKernel(pg_hdl=None, infobar=None)

Step 2 – Load Detector Data

Provide a detector image and a metadata dictionary describing the geometry. The geometry parameters control how pixel positions are converted to Q values:

# Example: 256x256 detector with synthetic data
detector_image = np.random.poisson(lam=100, size=(256, 256)).astype(np.float64)

metadata = {
    "bcx": 128.0,        # Beam center X (column), pixels
    "bcy": 128.0,        # Beam center Y (row), pixels
    "det_dist": 5000.0,  # Detector distance, mm
    "pix_dim": 0.075,    # Pixel size, mm
    "energy": 10.0,      # X-ray energy, keV
    "stype": "Transmission",
}

success = kernel.read_data(detector_image, metadata)
print(f"Data loaded: {success}")
print(f"Detector shape: {kernel.shape}")
print(f"Q-map keys: {list(kernel.qmap.keys())}")

The read_data() method automatically computes the Q-map from the geometry and initializes a mask of all ones (all pixels valid).

Geometry Parameters

Parameter

Unit

Description

Beam X

pixels

Beam center X coordinate (column)

Beam Y

pixels

Beam center Y coordinate (row)

Distance

mm

Sample-to-detector distance

Pixel Size

mm

Pixel dimension

Energy

keV

X-ray energy

Step 3 – Inspect the Q-Map

The qmap dictionary contains momentum-transfer and angle maps. Visualizing the Q-map helps verify that the geometry parameters are correct – the Q rings should be centered on the beam position:

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 3, figsize=(14, 4))

# Q magnitude map
im0 = axes[0].imshow(kernel.qmap["q"], origin="lower")
axes[0].set_title(f"Q ({kernel.qmap_unit['q']})")
plt.colorbar(im0, ax=axes[0])

# Azimuthal angle map
im1 = axes[1].imshow(kernel.qmap["phi"], origin="lower")
axes[1].set_title(f"phi ({kernel.qmap_unit['phi']})")
plt.colorbar(im1, ax=axes[1])

# Detector image
im2 = axes[2].imshow(np.log1p(detector_image), origin="lower")
axes[2].set_title("log(1 + I)")
plt.colorbar(im2, ax=axes[2])

plt.tight_layout()
plt.show()

Step 4 – Modify the Mask

The mask is a boolean array where True means valid (unmasked). You modify it by setting regions to False to exclude them from analysis. Common reasons to mask pixels include beamstop shadows, dead pixels, and hot pixels:

# Mask a rectangular beamstop region
kernel.mask[120:136, 120:136] = False

# Mask pixels with zero counts
kernel.mask[detector_image == 0] = False

# Mask hot pixels (intensity > threshold)
hot_threshold = np.percentile(detector_image, 99.9)
kernel.mask[detector_image > hot_threshold] = False

valid_pixels = kernel.mask.sum()
total_pixels = kernel.mask.size
print(f"Valid pixels: {valid_pixels}/{total_pixels} "
      f"({100 * valid_pixels / total_pixels:.1f}%)")

In the GUI, you use the drawing tools instead of array indexing:

Tool

Shortcut

Description

Rectangle

R

Draw rectangular mask regions

Circle

C

Draw circular mask regions

Polygon

P

Draw polygonal mask regions (click to add vertices)

Line

L

Draw line mask regions with adjustable width

Ellipse

E

Draw elliptical mask regions

Eraser

X

Remove (include) previously masked regions

Step 5 – Save and Load Masks

Masks are saved to HDF5 files for reuse across sessions and scripts:

# Save mask
kernel.save_mask("my_mask.h5")

# Load mask in a new kernel
kernel2 = SimpleMaskKernel(pg_hdl=None, infobar=None)
kernel2.read_data(detector_image, metadata)
kernel2.load_mask("my_mask.h5", key="mask")

Step 6 – Recompute Q-Map

If geometry parameters change (for example, after refining the beam center), recompute the Q-map:

# Update beam center
kernel.metadata["bcx"] = 130.0
kernel.metadata["bcy"] = 126.0

qmap, qmap_unit = kernel.compute_qmap()
print(f"Recomputed Q range: [{qmap['q'].min():.4f}, {qmap['q'].max():.4f}] {qmap_unit['q']}")

Step 7 – Compute a Q-Phi Partition

The partition divides the detector into Q-phi bins for correlation analysis. There are two levels: dynamic (fewer bins, used for G2 correlation) and static (finer bins, used for SAXS averaging).

partition = kernel.compute_partition(
    mode="q-phi",       # Partition in Q and phi
    dq_num=10,          # 10 dynamic Q bins
    sq_num=100,         # 100 static Q bins
    dp_num=1,           # 1 dynamic phi bin (full azimuthal average)
    sp_num=1,           # 1 static phi bin
    style="linear",     # Linear Q spacing ("logarithmic" also supported)
)

if partition is not None:
    print(f"Partition keys: {list(partition.keys())}")
    print(f"Dynamic ROI map shape: {partition['dynamic_roi_map'].shape}")
    print(f"Number of dynamic bins: {partition['dynamic_roi_map'].max()}")

    # Visualize the dynamic partition
    fig, ax = plt.subplots()
    roi = partition["dynamic_roi_map"].astype(float)
    roi[roi == 0] = np.nan  # Hide masked regions
    ax.imshow(roi, origin="lower", cmap="tab20")
    ax.set_title("Dynamic Q-Phi Partition")
    plt.tight_layout()
    plt.show()

The dynamic partition determines how many G2 curves you get – one per bin. More bins give finer Q resolution but fewer pixels per bin, increasing statistical noise. A good starting point is 10-20 dynamic Q bins with 1 phi bin (full azimuthal average).

Step 8 – Validate with Schemas

Use QMapSchema to validate Q-map data at I/O boundaries:

from xpcsviewer.schemas import QMapSchema

# Create a validated QMapSchema from the compute_qmap output
qmap_schema = QMapSchema.from_compute_qmap(kernel.qmap, kernel.qmap_unit)

print(f"Schema sqmap shape: {qmap_schema.sqmap.shape}")
print(f"Schema unit: {qmap_schema.sqmap_unit}")

# Convert back to dict for legacy interfaces
qmap_dict = qmap_schema.to_dict()

Note

The QMapSchema is a frozen dataclass. It validates shapes, dtypes, NaN values, and units at construction time. Arrays are defensively copied to prevent external mutation.

GUI Workflow

In the full GUI application, the mask editor is launched from the Mask Editor tab. It provides:

  • Drawing tools: Rectangle, Circle, Polygon, Line, Ellipse, Eraser

  • Undo/redo history via the MaskAssemble class

  • Export signals: mask_exported(np.ndarray) and qmap_exported(dict)

The GUI calls the same SimpleMaskKernel methods demonstrated above. See Mask Editor Guide for the full GUI reference with keyboard shortcuts and parameter tables.

Next Steps