Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save tayden/dcc83424ce55bfb970f60db3d4ddad18 to your computer and use it in GitHub Desktop.
Save tayden/dcc83424ce55bfb970f60db3d4ddad18 to your computer and use it in GitHub Desktop.
Scikit-Image histogram matching doesn't work on numpy masked arrays. This gist is a workaround for this issue. It shows how to do histogram matching when you have an image and need to exclude certain values from being considered as part of the image histogram.
from skimage import io, exposure
import numpy as np
# Histogram matching with masked image
def match_histograms(image, reference, image_mask, fill_value=0):
masked_image = np.ma.array(image, mask=image_mask)
matched = np.ma.array(np.empty(image.shape, dtype=image.dtype),
mask=image_mask, fill_value=fill_value)
for channel in range(masked_image.shape[-1]):
matched_channel = exposure.match_histograms(masked_image[...,channel].compressed(),
reference[...,channel].ravel())
# Re-insert masked background
mask_ch = image_mask[...,channel]
matched[..., channel][~mask_ch] = matched_channel.ravel()
return matched.filled()
# Load images
reference_img = io.imread("/path/to/reference_image.jpg") / 255
image = io.imread("/path/to/image.jpg") / 255
# Get a mask that matches image.shape, with mask being where pixel val across channels is 0
mask = np.repeat(np.expand_dims(np.all(img == 0, axis=2), axis=2), repeats=3, axis=2)
# Do the masked histogram matching
output = match_histograms(image, reference_img, mask, fill_value=0)
@JavierGSanchez
Copy link

Tayden this is awesome! I actually need to go from a masked reference image to a masked target image. I'm going to use your gist here as a starting point to getting something working.

@cwlkr
Copy link

cwlkr commented Feb 8, 2023

This is very cool and just what I was looking for! Just one thing I noticed. The fill value passed to the function is not relayed to the masked array, but set to 0.

@tayden
Copy link
Author

tayden commented Feb 8, 2023

Ah good catch! I'll update it

@FabianPlum
Copy link

Thanks so much for sharing this!

I slightly modified the code, in case both images contain black background elements / masks:

# Histogram matching with masked image
def match_histograms(image, reference, image_mask, reference_mask, fill_value=0):
    # adapted from https://gist.github.com/tayden/dcc83424ce55bfb970f60db3d4ddad18
    # to include masks for the input and reference image
    masked_source_image = np.ma.array(image, mask=image_mask)
    masked_reference_image = np.ma.array(reference, mask=reference_mask)
    
    matched = np.ma.array(np.empty(image.shape, dtype=image.dtype),
                          mask=image_mask, fill_value=fill_value)
    
    for channel in range(masked_source_image.shape[-1]):
        matched_channel = exposure.match_histograms(masked_source_image[...,channel].compressed(), 
                                                    masked_reference_image[...,channel].compressed())
        
        # Re-insert masked background
        mask_ch = image_mask[...,channel]
        matched[..., channel][~mask_ch] = matched_channel.ravel() 
    
    return matched.filled()

@EmanuelCastanho
Copy link

Cool, why this is not implemented in the main exposure.match_histograms?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment