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):
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:
Is there a way to do this with OpenCV
?
EDIT: another more detailed example GIF - 9 passes implementing my example:
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):
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:
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)
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)
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
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))
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]
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.