Custom median filters

Source code Author Update time

With median filter as an example, this demo shows how you could construct a custom kernel and pass it to mapwindow for common stencil operations.

Unlike other image processing toolboxes, ImageFiltering.jl does not provide you an out-of-box function for median filters. It instead provides the more general mapwindow function, which allows you to apply any function to the window around each pixel.

using ImageFiltering, ImageCore, ImageShow # or you could just `using Images`
using TestImages
using Statistics

img = Float64[isodd(i) - isodd(j) for i = 1:5, j = 1:5]
img[3, 3] = 1000
5×5 Matrix{Float64}:
  0.0  1.0     0.0  1.0   0.0
 -1.0  0.0    -1.0  0.0  -1.0
  0.0  1.0  1000.0  1.0   0.0
 -1.0  0.0    -1.0  0.0  -1.0
  0.0  1.0     0.0  1.0   0.0
patch_size = (3, 3)
imgm = mapwindow(median, img, patch_size)
5×5 Matrix{Float64}:
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  1.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  1.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0

mapwindow provides a high-level interface to loop over the image and call a function (median in this demo) on each patch/window. It's performing an operation that is nearly equivalent to the loop below, albeit more efficiently:

# For simplicity, border condition is not included here.
imgm = zeros(axes(img))
R = CartesianIndices(img)
I_first, I_last = first(R), last(R)
Δ = CartesianIndex(patch_size .÷ 2)
for I in R
    patch = max(I_first, I-Δ):min(I_last, I+Δ)
    imgm[I] = median(img[patch])
end
imgm
5×5 Matrix{Float64}:
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  1.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  1.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0

Compared to this hand-written loop, mapwindow offers additional flexibility in handling the borders and allows you to process only a subset of the windows. For some functions (e.g., min, max, and extrema), mapwindow uses a custom implementation that is far more efficient than naively computing the function "freshly" on each window.

When the input array has NaN or some unwanted pixel values, a pre-filtering process is needed to exclude them first. This can be done quite easily by compositing a given kernel operation and a filter operation. Let's still take median filter as an example.

img[2,2] = NaN
imgm = mapwindow(median, img, (3,3))
5×5 Matrix{Float64}:
 NaN    NaN    NaN    0.0  0.0
 NaN    NaN    NaN    0.0  0.0
 NaN    NaN    NaN    0.0  0.0
   0.0    0.0    1.0  0.0  0.0
   0.0    0.0    0.0  0.0  0.0

NaN has polluted the output, which is usually not wanted. This can be fixed quite easily by compositing a new filter kernel. This way, we only compute the median value on the non-NaN subset of each patch/window.

_median(patch) = median(filter(x->!isnan(x), patch))
imgm = mapwindow(_median, img, patch_size)
5×5 Matrix{Float64}:
  0.0   0.0  0.5  0.0  0.0
  0.0   0.0  1.0  0.0  0.0
 -0.5  -0.5  0.0  0.0  0.0
  0.0   0.0  1.0  0.0  0.0
  0.0   0.0  0.0  0.0  0.0

Impulse (Salt and Pepper) noise removal

Median filters are quite robust to outliers (unusual values), this property can be used to remove salt&pepper noise. An image with n-level salt&papper noise is defined as: each pixel p has n/2 probabilty to be filled by 0(the minimal gamut value), n/2 probabilty to be filled by 1(the maximal gamut value), and 1-n probablity to be undamaged.

img = testimage("cameraman")

n = 0.2 # noise level
noisy_img = map(img) do p
    sp = rand()
    if sp < n/2
        eltype(img)(gamutmin(eltype(img))...)
    elseif sp < n
        eltype(img)(gamutmax(eltype(img))...)
    else
        p
    end
end

denoised_img = mapwindow(median!, noisy_img, (5, 5))

mosaicview(img, noisy_img, denoised_img; nrow=1)

This page was generated using DemoCards.jl and Literate.jl.