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:
Load detector data – Provide a 2D detector image and geometry metadata.
Create a mask – Mark bad pixels, beamstop regions, and detector gaps.
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 |
|
Draw rectangular mask regions |
Circle |
|
Draw circular mask regions |
Polygon |
|
Draw polygonal mask regions (click to add vertices) |
Line |
|
Draw line mask regions with adjustable width |
Ellipse |
|
Draw elliptical mask regions |
Eraser |
|
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
MaskAssembleclassExport signals:
mask_exported(np.ndarray)andqmap_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¶
Getting Started with XPCS Viewer – Load data and perform basic G2 analysis
Fitting G2 Correlation Functions – Fit G2 curves with NLSQ and Bayesian inference
Cookbook: Common Patterns and Recipes – Batch processing and advanced patterns