Search code examples
pythonopencvcomputer-visionblurgaussianblur

Is there a way to use a conditional kernel in openCV that only changes pixels on an image if the condition is true?


I want to use a kernel that performs a pixel operation based on a conditional expression.

Let's say I have this grayscale image (6x6 resolution):

enter image description here

and I use a 3x3 pixel kernel, how would I change the value of the centre kernel pixel (centre) IF AND ONLY IF the centre pixel is the local minimum or maximum within the 3x3 kernel?

For example, say I wanted to set the centre kernel pixel to the average value of the surrounding 8 pixels, like this:

, then set it to the average value of all surrounding 8 pixels.

Is there a way to do this with OpenCV?

EDIT: another more detailed example GIF - 9 passes implementing my example:

enter image description here

This was produced in Excel using the following formula (not the relative cell references - they show the kernel shape of 3x3 around the focus 'picell':

=IF(OR(C55=MIN(B54:D56),C55=MAX(B54:D56)),(SUM(B54:D56)-C55)/8,C55)

Here is the top left corner of the table with the source values for the first pass (these values control the cell colour):

enter image description here

This table refers to another source table. Each frame in the GIF is the next calculated colour table. There are 3 tables of formulae in between each image frame. Here is more context:

enter image description here


Solution

  • You asked:

    [...] how would I change the value of the centre kernel pixel (centre) IF AND ONLY IF the centre pixel is the local minimum or maximum within the 3x3 kernel? For example, say I wanted to set the centre kernel pixel to the average value of the surrounding 8 pixels [...]

    I'll demonstrate a few things first. I'll work with small arrays, 16 by 16. I'll show them enlarged so you can stare at the pixels conveniently. When I talk about these images, I mean the 16x16 data, not the visualizations in this post.

    Let's start with random noise because that is what you first presented.

    noise = np.random.randint(0, 256, size=(16, 16), dtype=np.uint8)
    

    noise

    Now you need to know about morphology operations. Erosion and dilation are calculating the local minimum/maximum value.

    local_max_values = cv.dilate(noise_img, None, iterations=1)
    local_min_values = cv.erode(noise_img, None, iterations=1)
    

    local_max_values local_min_values

    What's that good for? You can compare pixel values. If a pixel is equal to the local extremum, it must be a local extremum. It's not unique because let's say two adjacent pixels have the same low/high value. They're both extrema.

    Let's compare:

    is_min = (noise_img == local_min_values)
    is_max = (noise_img == local_max_values)
    is_extremum = is_min | is_max
    

    is_min is_max is_extremum

    Those are masks. They're binary, boolean. You can use them either for indexing, or for multiplication. You can imagine what happens when you multiply elementwise by 0 or 1, or do that with an inverted mask.

    I'll demonstrate indexing but first I'll need the local averages.

    averaged = cv.blur(noise_img, (3, 3))
    

    averaged

    Now I can make a copy of the input (or I could work on it directly) and then overwrite all the extremal pixels with the average values at those positions.

    denoised = noise_img.copy()
    denoised[is_extremum] = averaged[is_extremum]
    

    denoised

    Yes, this calculates the average for all pixels, even if you don't need it. Practically, you wouldn't save any time by calculating only some of the averages.

    If you switch back and forth between this and the source image, you'll see local extrema being erased. Other pixels that used to be "second place" have now become extrema. Another round of this will progressively smooth the entire picture until everything is quite flat.