Bitmasks

PypeIt has implemented a generalized class for handling bitmasks.

Bitmasks allow you to define a set of bit values signified by strings, and then toggle and interpret bits held by a numpy.ndarray. For example, say you’re processing an image and you want to setup a set of bits that indicate that the pixel is part of a bad-pixel mask, has a cosmic ray, or is saturated. You can define the following:

from pypeit.bitmask import BitMask

bits = {'BPM':'Pixel is part of a bad-pixel mask',
        'COSMIC':'Pixel is contaminated by a cosmic ray',
        'SATURATED':'Pixel is saturated.'}
image_bm = BitMask(list(bits.keys()), descr=list(bits.values()))

Note

Consistency in the order of the dictionary keywords is critical to the repeatability of the BitMask instances. The above is possible because dict objects automatically maintain the order of the provided keywords since Python 3.7.

Or, better yet, define a derived class:

from pypeit.bitmask import BitMask

class ImageBitMask(BitMask):
    def __init__(self):
        bits = {'BPM':'Pixel is part of a bad-pixel mask',
                'COSMIC':'Pixel is contaminated by a cosmic ray',
                'SATURATED':'Pixel is saturated.'}
        super(ImageBitMask, self).__init__(list(bits.keys()), descr=list(bits.values()))

image_bm = ImageBitMask()

In either case, you can see the list of bits and their bit numbers by running:

>>> image_bm.info()
         Bit: BPM = 0
 Description: Pixel is part of a bad-pixel mask

         Bit: COSMIC = 1
 Description: Pixel is contaminated by a cosmic ray

         Bit: SATURATED = 2
 Description: Pixel is saturated.
>>> image_bm.bits
{'BPM': 0, 'COSMIC': 1, 'SATURATED': 2}
>>> image_bm.keys()
['BPM', 'COSMIC', 'SATURATED']

Now you can define a numpy.ndarray to hold the mask value for each image pixel; the minimum_dtype() returns the smallest data type required to represent the list of defined bits. The maximum number of bits that can be defined is 64. Assuming you have an image img:

import numpy
mask = numpy.zeros(img.shape, dtype=image_bm.minimum_dtype())

Assuming you have boolean or integer arrays that identify pixels to mask, you can turn on the mask bits as follows:

mask[cosmics_indx] = image_bm.turn_on(mask[cosmics_indx], 'COSMIC')
mask[saturated_indx] = image_bm.turn_on(mask[saturated_indx], 'SATURATED')

or make sure certain bits are off:

mask[not_a_cosmic] = image_bm.turn_off(mask[not_a_cosmic], 'COSMIC')

The form of these methods is such that the array passed to the method are not altered. Instead the altered bits are returned, which is why the lines above have the form m = bm.turn_on(m, flag).

Some other short usage examples:

  • To find which flags are set for a single value:

    image_bm.flagged_bits(mask[0,10])
    
  • To find the list of unique flags set for any pixel:

    unique_flags = numpy.sort(numpy.unique(numpy.concatenate(
                        [image_bm.flagged_bits(b) for b in numpy.unique(mask)]))).tolist()
    
  • To get a boolean array that selects pixels with one or more mask bits:

    cosmics_indx = image_bm.flagged(mask, flag='COSMIC')
    all_but_bpm_indx = image_bm.flagged(mask, flag=['COSMIC', 'SATURATED'])
    any_flagged = image_bm.flagged(mask)
    
  • To construct masked arrays, following from the examples above:

    masked_img = numpy.ma.MaskedArray(img, mask=image_bm.flagged(mask))
    

BitMask objects can be defined programmatically, as shown above for the ImageBitMask derived class, but they can also be defined by reading formatted files. The current options are:

  1. Fits headers: There are both reading and writing methods for bitmask I/O using astropy.io.fits.Header objects. Using the ImageBitMask class as an example:

    >>> from astropy.io import fits
    >>> hdr = fits.Header()
    >>> image_bm = ImageBitMask()
    >>> image_bm.to_header(hdr)
    >>> hdr
    BIT0    = 'BPM     '           / Pixel is part of a bad-pixel mask
    BIT1    = 'COSMIC  '           / Pixel is contaminated by a cosmic ray
    BIT2    = 'SATURATED'          / Pixel is saturated.
    >>> copy_bm = BitMask.from_header(hdr)
    

Bitmask Arrays

BitMaskArray objects combine the numpy.ndarray that holds the bit values and the BitMask object used to interpret them into a new subclass of DataContainer. The following builds on the example uses of BitMask objects (see Bitmasks).

Defining a new subclass

Given the definition of ImageBitMask, we can implement a relevant BitMaskArray subclass as follows:

from pypeit.images.bitmaskarray import BitMaskArray

class ImageBitMaskArray(BitMaskArray):
    version = '1.0'
    bitmask = ImageBitMask()

The remaining functionality below is all handled by the base class.

To instantiate a new 2D mask array that is 5 pixels on a side:

shape = (5,5)
mask = ImageBitMaskArray(shape)

You can access the bit flag names using:

>>> mask.bit_keys()
['BPM', 'COSMIC', 'SATURATED']

Bit access

You can flag bits using turn_on(). For example, the following code flags the center column of the image as being part of the detector bad-pixel mask:

import numpy as np
mask.turn_on('BPM', select=np.s_[:,2])

The select argument to turn_on() can be anything that is appropriately interpreted as slicing a numpy.ndarray. That is, arr[select] must be valid, where arr is the internal array held by mask.

Similarly, you can flag a pixel with a cosmic ray:

mask.turn_on('COSMIC', select=(0,0))

or multiple pixels as being saturated:

mask.turn_on('SATURATED', select=([0,1,-1,-1],[0,0,-1,-2]))

and you can simultaneously flag pixels for multiple reasons:

mask.turn_on(['COSMIC', 'SATURATED'], select=([-1,-1],[0,1]))

The mask values themselves are accessed using the mask attribute:

>>> mask.mask
array([[6, 0, 1, 0, 0],
       [4, 0, 1, 0, 0],
       [0, 0, 1, 0, 0],
       [0, 0, 1, 0, 0],
       [6, 6, 1, 4, 4]], dtype=int16)

However, more usefully, you can obtain boolean arrays that select pixels flagged by one or more flags:

>>> mask.flagged(flag='SATURATED')
array([[ True, False, False, False, False],
       [ True, False, False, False, False],
       [False, False, False, False, False],
       [False, False, False, False, False],
       [ True,  True, False,  True,  True]])

>>> mask.flagged(flag=['BPM', 'SATURATED'])
array([[ True, False,  True, False, False],
       [ True, False,  True, False, False],
       [False, False,  True, False, False],
       [False, False,  True, False, False],
       [ True,  True,  True,  True,  True]])

If you want to select all pixels that are not flagged by a given flag, you can use the invert option in flagged():

>>> gpm = mask.flagged(flag='BPM', invert=True)
>>> gpm
array([[ True,  True, False,  True,  True],
       [ True,  True, False,  True,  True],
       [ True,  True, False,  True,  True],
       [ True,  True, False,  True,  True],
       [ True,  True, False,  True,  True]])

For individual flags, there is also convenience functionality that allows you to access a boolean array as if it were an attribute of the object:

>>> mask.bpm
array([[False, False,  True, False, False],
       [False, False,  True, False, False],
       [False, False,  True, False, False],
       [False, False,  True, False, False],
       [False, False,  True, False, False]])
>>> mask.saturated
array([[ True, False, False, False, False],
       [ True, False, False, False, False],
       [False, False, False, False, False],
       [False, False, False, False, False],
       [ True,  True, False,  True,  True]])

This convenience operation is identical to calling flagged() for the indicated bit. However bpm is not an array that can be used to change the value of the bits themselves:

>>> indx = np.zeros(shape, dtype=bool)
>>> indx[2,3] = True
>>> mask.bpm = indx # Throws an AttributeError

Instead, you must use the bit toggling functions provided by the class: turn_on(), turn_off(), or toggle().

Tip

Every time flagged() is called, a new array is created. If you need to access to the result of the function multiple times without changing the flags, you’re better of assigning the result to a new array and then using that array so that you’re not continually allocating and deallocating memory (even within the context of how this is done within python).

Input/Output

As a subclass of DataContainer, you can save and read the bitmask data to/from files:

>>> mask.to_file('mask.fits')
>>> _mask = ImageBitMaskArray.from_file('mask.fits')
>>> np.array_equal(mask.mask, _mask.mask)
True

In addition to the mask data, the bit flags and values are also written to the header; see the BIT* entries in the header below:

>>> from astropy.io import fits
>>> hdu = fits.open('mask.fits')
>>> hdu.info()
Filename: mask.fits
No.    Name      Ver    Type      Cards   Dimensions   Format
  0  PRIMARY       1 PrimaryHDU      13   ()
  1  MASK          1 ImageHDU        22   (5, 5)   int16
>>> hdu['MASK'].header
XTENSION= 'IMAGE   '           / Image extension
BITPIX  =                   16 / array data type
NAXIS   =                    2 / number of array dimensions
NAXIS1  =                    5
NAXIS2  =                    5
PCOUNT  =                    0 / number of parameters
GCOUNT  =                    1 / number of groups
VERSPYT = '3.9.13  '           / Python version
VERSNPY = '1.22.3  '           / Numpy version
VERSSCI = '1.8.0   '           / Scipy version
VERSAST = '5.0.4   '           / Astropy version
VERSSKL = '1.0.2   '           / Scikit-learn version
VERSPYP = '1.10.1.dev260+g32de3d6d4' / PypeIt version
DATE    = '2022-11-10'         / UTC date created
DMODCLS = 'ImageBitMaskArray'  / Datamodel class
DMODVER = '1.0     '           / Datamodel version
BIT0    = 'BPM     '
BIT1    = 'COSMIC  '
BIT2    = 'SATURATED'
EXTNAME = 'MASK    '           / extension name
CHECKSUM= 'APGODMFOAMFOAMFO'   / HDU checksum updated 2022-11-10T13:10:27
DATASUM = '1245200 '           / data unit checksum updated 2022-11-10T13:10:27

Note

Currently, when loading a mask, the bit names in the header of the output file are not checked against the bitmask definition in the code itself. This kind of version control should be handled using the version attribute of the class. I.e., anytime the flags in the bitmask are changed, the developers should bump the class version.