Satellite killer explanation

The functions in question (introduced in this PR) generates a satellite mask that is used to remove satellite energy deposits produced throughout the deconvolution.

Lets step through the logic!

Stepping through the satellite killing process

We start in generate_satellite_mask():

if cut_type is CutType.rel:
        im_deconv = im_deconv / im_deconv.max()

This is a simple check for the cut_type, if it is absolute (abs) no changes are needed, the relative (rel) changes are self explanatory.

labels, component_sizes = collect_component_sizes(im_deconv >= e_cut)

As shown above, the function then uses collect_component_sizes() to collect an array of all the labelled regions, and the sizes of each of those regions. A boolean cut is applied in the argument (im_deconv >= e_cut) to ensure the array exists with only 0s and 1s, with 0s being energies below the e_cut and 1s being above.

So the array should look something like this (as an example):

>>> print(im_mask)
array([[1., 1., 0., 0., 1.],
       [1., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0.],
       [0., 0., 1., 1., 1.],
       [0., 0., 1., 1., 1.]])

The function collect_component_sizes() completes the following steps:

footprint = ndi.generate_binary_structure(im_mask.ndim, 2)

This creates an n-dimensional array of True values that are used to map the connectivity of our 1s in the above array, like such:

>>> print(footprint)
array([[ True,  True,  True],
       [ True,  True,  True],
       [ True,  True,  True]])

Since we’re always working in the 2D case, we could hard code this, but its preferable to be generalised as such for futureproofing purposes (3D beersheba). The next line is:

labels, _ = ndi.label(im_mask, footprint)

Which uses the footprint and the above mask to label the different ‘deposits’ as shown below:

>>> print(labels)
array([[1, 1, 0, 0, 2],
       [1, 1, 0, 0, 0],
       [0, 0, 0, 0, 0],
       [0, 0, 3, 3, 3],
       [0, 0, 3, 3, 3]], dtype=int32)

Next:

component_sizes = np.bincount(labels.ravel())

This counts the occurence of each type within the array labels

>>> print(component_sizes)
array([14,  4,  1,  6])

14 zeros, 4 ones, 1 twos, 6 threes.

The labels and component_sizes are then returned, which is followed by an if statement:

if len(component_sizes) <= 2:
    # Return a fully False array, so that no objects get removed
    return np.full(im_deconv.shape, False)

If there are only 0s and 1s, there are no satellites! So you can pass back a completely False array.

too_small = component_sizes < satellite_max_size

This creates an equivalent array of trues and falses, so lets say satellite_max_size = 3:

>>> print(too_small)
array([False, False, True, False])

This has flagged the 2nd element (corresponding to the 2s above) as a satellite.

We want the first element (0s) to always be false, so we set that: too_small[0] = False

You can then map this true/false map back onto the array to create a mask in which only elements you want to remove from the initial relay are True.

>>> too_small_mask = too_small[label]
>>> print(too_small_mask)
array([[False, False, False, False, True],
       [False, False, False, False, False],
       [False, False, False, False, False],
       [False, False, False, False, False],
       [False, False, False, False, False]])

This mask is then returned, and applied such that all true elements in the original array are zero.

>>> print(im_deconv)
array([[1., 1., 0., 0., 1.],
       [1., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0.],
       [0., 0., 1., 1., 1.],
       [0., 0., 1., 1., 1.]])
>>> im_deconv[too_small_mask] = 0
>>> print(im_deconv)
array([[1., 1., 0., 0., 0.], #<--- satellite gone!
       [1., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0.],
       [0., 0., 1., 1., 1.],
       [0., 0., 1., 1., 1.]])