Making data masks

This example shows how to make pixel masks using simple expressions. These masks can be used on the data without assembling detector images, which is useful as assembly is relatively slow, and reduces accuracy slightly by rounding pixel coordinates.

This example uses AGIPD geometry, but the same technique should work for any supported detector.

%matplotlib inline
import numpy as np
from extra_geom import AGIPD_1MGeometry
geom = AGIPD_1MGeometry.from_quad_positions(quad_pos=[
    (-525, 625),
    (-550, -10),
    (520, -160),
    (542.5, 475),

<AxesSubplot:title={'center':'AGIPD-1M detector geometry (No file)'}, xlabel='metres', ylabel='metres'>

The get_pixel_positions() method gives (x, y, z) coordinates for the centre of each pixel. Here, as is typical, the z coordinates are zero (on the detector plane), so we’ll only use x and y.

pixpos = geom.get_pixel_positions()
px, py, pz = np.moveaxis(pixpos, -1, 0)  # Separate x, y, z coordinates
px.shape  # (modules, slow scan, fast scan)
(16, 512, 128)

A rectangular mask is defined by four limits. These numbers are in metres:

rect_mask = (0.01 < px) & (px < 0.05) & (-0.05 < py) & (py < -0.02)

This makes a mask which is true (1) inside the rectangle. Multiplying mask * data will zero out everything outside the selected area. If you need a mask where 0 indicates pixels to keep, invert it with ~mask.

By plotting the mask itself like an image, we can see what area it includes.

def visualise_mask(mask_arr):
    return geom.plot_data_fast(
        # converting to float allows gaps to be distinguished as NaN.
        mask_arr.astype(float), colorbar=None, axis_units='m'

<AxesSubplot:xlabel='metres', ylabel='metres'>

We can make circular shapes by using Pythagoras’ theorem to get the distance from the centre to each point ( \(c = \sqrt{a^2 + b^2}\) ):

radius = np.sqrt(px**2 + py**2)

ring_mask = (0.04 < radius) & (radius < 0.05)
<AxesSubplot:xlabel='metres', ylabel='metres'>

arctan2() converts x, y coordinates to angles in radians, which enables things like this:

angle = np.arctan2(py, px)

wedge_mask = (np.pi * 5/8 < angle) & (angle < np.pi * 7/8)
<AxesSubplot:xlabel='metres', ylabel='metres'>

We can combine masks with & (intersection) and | (union) to create more complex shapes:

complex_mask = (ring_mask & wedge_mask) | rect_mask
<AxesSubplot:xlabel='metres', ylabel='metres'>

This mask only includes data in four detector modules. If we’re selecting that data, we might be able to skip loading the data from the other modules. We can check which module numbers the mask includes:

modules_included = np.any(complex_mask, axis=(1, 2))
array([2, 3, 8, 9])

Mask AGIPD wide pixels

AGIPD modules contain double width pixels at the boundaries between ASICs. Being larger, these catch more photons, so they can affect results. EXtra-geom contains a function to select them for masking:

from extra_geom import agipd_asic_seams
# Get the mask, and repeat it for 16 modules
module_mask = agipd_asic_seams()
all_modules_mask = np.repeat(module_mask[np.newaxis], 16, axis=0)

ax = visualise_mask(all_modules_mask)

# Zoom in to see the masked edges
ax.set_xlim(0.04, 0)
ax.set_ylim(0, 0.04)
(0.0, 0.04)