2

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

21
  • 1
    Create a mask using your condition. Then apply the morphology everywhere. Then use the mask to blend between the original and morphology processed image using np.where(mask=mask, processed, original)
    – fmw42
    Commented Nov 6, 2023 at 5:54
  • 2
    Post a real image and the exact condition you want to use.
    – fmw42
    Commented Nov 6, 2023 at 17:12
  • 1
    Most smoothing filters, if you iterate them often enough, will end up flattening the whole image. The opening and the closing are the only idempotent smoothing filters I can think of right now. So, yes, of course, you want to limit how many times you apply the filter. Commented Nov 6, 2023 at 21:10
  • 2
    You could edit your post to show what logic you used to produce the example GIFs. At least then there would be a more substantial basis for discussion.
    – ryyker
    Commented Nov 6, 2023 at 21:15
  • 1
    What was the exact mathematical conditional mean filter that was used. Please show an original image at normal resolution, not one that was expanded to show blocks for pixels and without grid or tick marks.
    – fmw42
    Commented Nov 6, 2023 at 22:11

2 Answers 2

3

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.

1

Here is an alternate approach using Python/OpenCV.

Convert your mathematical condition into mathematical OpenCV methods to produce a mask (binary or otherwise). Then use the mask to blend between the input and the 3x3 averaged input.

Here my condition will be a binary image that is white where the difference between the averaged image and the input is larger than some threshold value and elsewhere the mask is black.

Input (random image created by script and enlarged for viewing):

enter image description here

import cv2
import numpy as np

# create noise input
np.random.seed(1)
noise = np.random.randint(0, 256, size=(16, 16), dtype=np.uint8)

# create 3x3 average
average = cv2.blur(noise, (3, 3))

# create condition "mask" where difference between average and noise is greater than some percent threshold
threshVal = 40   #percent
threshVal = 255*threshVal/100
mask = cv2.subtract(average, noise)
mask = cv2.threshold(mask, threshVal, 255, cv2.THRESH_BINARY)[1]

# process image based upon condition 
result = np.where(mask, average, noise)

# compute percent change
change = cv2.absdiff(result, noise)
pct_change = 100*cv2.divide(change, noise)
print(np.average(pct_change))

# save 10x expanded results
cv2.imwrite('noise.png', cv2.resize(noise, (0,0), fx=10, fy=10, interpolation=cv2.INTER_AREA))
cv2.imwrite('mask.png', cv2.resize(mask, (0,0), fx=10, fy=10, interpolation=cv2.INTER_AREA))
cv2.imwrite('noise_conditional_average.png', cv2.resize(result,(0,0), fx=10, fy=10, interpolation=cv2.INTER_AREA))

# show 10x expanded results
cv2.imshow('noise', cv2.resize(noise, (0,0), fx=10, fy=10, interpolation=cv2.INTER_AREA))
cv2.imshow('mask', cv2.resize(mask, (0,0), fx=10, fy=10, interpolation=cv2.INTER_AREA))
cv2.imshow('result', cv2.resize(result,(0,0), fx=10, fy=10, interpolation=cv2.INTER_AREA))
cv2.waitKey(0)

Mask Image (enlarged for viewing):

enter image description here

Result Image (enlarged for viewing):

enter image description here

Change Percent:  9.671875

Alternating Input and Result to Show Changes:

enter image description here

2
  • This is also a great answer - I want to marked both these are the answer, but I must choose one. And I accept @CrisLuengo's comment. It was not really a fair question as it changed as I added more info. But thank you!
    – skeetastax
    Commented Nov 8, 2023 at 0:55
  • No problem. Don't worry about it. I was answering your original question about conditions on the average. I had not noticed you changes to the center pixel min or max.
    – fmw42
    Commented Nov 8, 2023 at 2:26

Not the answer you're looking for? Browse other questions tagged or ask your own question.