2

Recently I am writing a script to manipulate the pixel values in an image. The idea is to set the pixels that fall into the given range to a specific value. Instead of using the command "for" which loops from pixel to pixel, an image expression was utilized, for example:

Img = (Img>=thresh_Low && Img<=thresh_Up ? 0 : Img)

Here comes the question: if I would like to replace the pixel value with the average of neighbouring pixels, rather than just a fixed value such as 0 in the above case, pixel-looping seems not avoidable anymore. Does anyone know any workaround that the method of image expression can still be used here?

Thanks in advance.

1 Answers1

1

Computing images expressions is much more efficient than any pixel-by-pixel operation. Even if you thereby compute some averages that are not needed will the script perform a lot faster. Therefore:

You should compute an average-image (for all pixels, not just the masked ones) and then use it in the masked assignment.

The following example illustrates this. Only the last two lines are the direct answer to your question. The condition is used to either copy the orignal or the averaged value:

number aver_NN = 3      // Next neighbor averaging. 1 = 3x3, 2 = 5x5 etc.)
number maskRad = 0.3    // just a radius to show masking

image img := GetFrontImage()
if ( 2 != img.ImageGetNumDimensions() ) Throw( "Only 2D images are supported." )

// Create average image (ignoring border region for simplicity)
image av := img * 0
for( number dx=-aver_NN; dx<=aver_NN; dx++ )
    for( number dy=-aver_NN; dy<=aver_NN; dy++ )
        av += img.offset(dx,dy)

av /= (2*aver_NN + 1) ** 2

// Apply masked replacement
image replaced = iradius < iwidth*maskrad ? av : img
replaced.ShowImage()

Further explanations because of comments below

The averaging is done by shifting the whole image by one pixel using the offset command. This command will replace border pixels with the 0 value. Summing all the shifted images and dividing by the number of images therefore gives at each pixel the average value of the neighbor pixels, but the normalization in the border pixels is incorrect. The following script shows this using explicit images instead of the for-loop:


number size = 25
image test := realimage("Source",4,size,size)
test = 1 + random()
test.ShowImage()

image offset_N = test.offset(  0, -1 )
image offset_S = test.offset(  0,  1 )
image offset_W = test.offset( -1,  0 )
image offset_E = test.offset(  1,  0 )
offset_N.ShowImage()
offset_N.SetName("N")
offset_S.ShowImage()
offset_S.SetName("S")
offset_W.ShowImage()
offset_W.SetName("W")
offset_E.ShowImage()
offset_E.SetName("E")

image average = test + offset_N + offset_S + offset_W + offset_E
average /= 5
average.SetName("Average")
average.ShowImage()

EGUPerformActionWithAllShownImages("Arrange")

Output

To fix the issue with the borders, two strategies could be used for the normalization.

  • Explicitly normalize subsections of the sum-image, knowing how many images where summed:
...

image average = test + offset_N + offset_S + offset_W + offset_E
average.SetName("Average")
// Divide corners by 3
// Divide edges by 4
// Divide rest by 5
average.slice2(0,0,0 ,0,2,size-1, 1,2,size-1) /= 3
average.slice2(1,0,0 ,0,size-2,1, 1,2,size-1) /= 4
average.slice2(0,1,0 ,0,2,size-1, 1,size-2,1) /= 4
average.slice2(1,1,0 ,0,size-2,1, 1,size-2,1) /= 5

...
  • Create a second image which 'counts' automatically and used it for normalization. For this, simple create a 1-valued image of the same size as the source and perform the same summing steps! This makes the script from above into:
number aver_NN = 2      // Next neighbor averaging. 1 = 3x3, 2 = 5x5 etc.)
number maskRad = 1    // just a radius to show masking

image img := GetFrontImage()
if ( 2 != img.ImageGetNumDimensions() ) Throw( "Only 2D images are supported." )

// Create average image 
image av = img * 0
image weight = av 
image proxy = av + 1

for( number dx=-aver_NN; dx<=aver_NN; dx++ )
{
    for( number dy=-aver_NN; dy<=aver_NN; dy++ )
    {
        av += img.offset(dx,dy)
        weight += proxy.offset(dx,dy)
    }
}

weight.SetName("Sum weight")
weight.showImage()

av /= weight

// Apply masked replacement
image replaced = iradius < iwidth*maskrad ? av : img
replaced.ShowImage()
  • One can also create the average image by relying on the inbuilt Convolution() command, which correctly handles the border cases right away. Here, one would just create the average image as:
// Create average image 
// Define an averaging kernel
image kernel := [5,5] : {
 { 0, 0, 1, 0, 0 },
 { 0, 1, 1, 1, 0 },
 { 1, 1, 1, 1, 1 },
 { 0, 1, 1, 1, 0 },
 { 0, 0, 1, 0, 0 } 
}

image av = img.Convolution(kernel)
av.ShowImage()
BmyGuest
  • 6,331
  • 1
  • 21
  • 35
  • Many thanks for this useful information. Do you mean that the "for" loop is still necessary for preparing the mask, i.e. average image? By the way, I'm quite struggling in how to calculate the average value of nearby pixels as a mask, especially at the border, since for loop will report an error message. Further example will be very appreciated. – DM Adventurer Jan 09 '21 at 20:18
  • @DMAdventurer Yes, but this is a for-loop over the averaging shifts, not the pixels. To compute the average image, you sum over images shifted by one or two pixels in each direction (so 9 or 25 iterations). Thus, this image then has in each pixel the value from the original pixels and it's neighbors. The for-loop in the above script does not produce an error in the border regions, because the 'offset' command works on images and just zero-pads the missing pixels. I'll add a further example in an edit above. – BmyGuest Jan 10 '21 at 19:07
  • Also... Calculating the pixel average image seems to use only simple math but it is efficient. My brain is too complicated. Meanwhile, the convolution method for me is impressively COOL. Owe you a cup of beer. – DM Adventurer Jan 11 '21 at 16:50
  • A subsidiary question. I am trying to add an additional function to apply this kind of image manipulation to a local area instead of the whole image, for example, an preliminarily assigned ROI. In fact, what first comes to my mind is again using the for loop but with a give x and y coordinate. Is it possible to incorporate this idea but still use the image expression? – DM Adventurer Jan 26 '21 at 16:22
  • @DMAdventurer If that region is a regular rect-subarray, you just use the slice2() commmand and then use that like you would use the input image. – BmyGuest Jan 26 '21 at 17:55