5
votes

EDIT: Thanks to Howard, I've corrected the code here and it seems to be working now.

EDIT2: I've updated the code to include a vertical blur as originally intended. Resulting sample output with various settings: Blur comparison images.jpg

Another reference for blur operations (Java): Blurring for Beginners


original post:

I'm trying to learn about basic image processing and duplicate this simple Blur method (the second function BlurHorizontal under "Reusing results") in python. I know there are already blur functions in PIL, but I want to try out the basic pixel operations myself.

This function should take a source image, then average RGB pixel values based on a certain radius and write the processed image to a new file. My problem is that I'm getting a lot of pixels with completely wrong averaged values (for example, bright green lines instead of red in certain areas).

With a blur radius of 2, the averaging method adds up the RGB values for the 5 pixels centered on the input pixel. It uses a "sliding window" to keep a running total, subtracting the outgoing pixel (left side) and adding the new incoming pixel (right side of window). Blur method explained here

Sample: Blur test image output.jpg

Any ideas where I've gone wrong? I'm not sure why some parts of the image blur cleanly while other areas are filled with colors completely unrelated to the surrounding areas.

Thanks for your help.

FIXED WORKING Code (Thanks Howard)

import Image, numpy, ImageFilter
img = Image.open('testimage.jpg')

imgArr = numpy.asarray(img) # readonly

# blur radius in pixels
radius = 2

# blur window length in pixels
windowLen = radius*2+1

# columns (x) image width in pixels
imgWidth = imgArr.shape[1]

# rows (y) image height in pixels
imgHeight = imgArr.shape[0]

#simple box/window blur
def doblur(imgArr):
    # create array for processed image based on input image dimensions
    imgB = numpy.zeros((imgHeight,imgWidth,3),numpy.uint8)
    imgC = numpy.zeros((imgHeight,imgWidth,3),numpy.uint8)

    # blur horizontal row by row
    for ro in range(imgHeight):
        # RGB color values
        totalR = 0
        totalG = 0
        totalB = 0

        # calculate blurred value of first pixel in each row
        for rads in range(-radius, radius+1):
            if (rads) >= 0 and (rads) <= imgWidth-1:
                totalR += imgArr[ro,rads][0]/windowLen
                totalG += imgArr[ro,rads][1]/windowLen
                totalB += imgArr[ro,rads][2]/windowLen

        imgB[ro,0] = [totalR,totalG,totalB]

        # calculate blurred value of the rest of the row based on
        # unweighted average of surrounding pixels within blur radius
        # using sliding window totals (add incoming, subtract outgoing pixels)
        for co in range(1,imgWidth):
            if (co-radius-1) >= 0:
                totalR -= imgArr[ro,co-radius-1][0]/windowLen
                totalG -= imgArr[ro,co-radius-1][1]/windowLen
                totalB -= imgArr[ro,co-radius-1][2]/windowLen
            if (co+radius) <= imgWidth-1:
                totalR += imgArr[ro,co+radius][0]/windowLen
                totalG += imgArr[ro,co+radius][1]/windowLen
                totalB += imgArr[ro,co+radius][2]/windowLen

            # put average color value into imgB pixel

            imgB[ro,co] = [totalR,totalG,totalB]

    # blur vertical

    for co in range(imgWidth):
        totalR = 0
        totalG = 0
        totalB = 0

        for rads in range(-radius, radius+1):
            if (rads) >= 0 and (rads) <= imgHeight-1:
                totalR += imgB[rads,co][0]/windowLen
                totalG += imgB[rads,co][1]/windowLen
                totalB += imgB[rads,co][2]/windowLen

        imgC[0,co] = [totalR,totalG,totalB]

        for ro in range(1,imgHeight):
            if (ro-radius-1) >= 0:
                totalR -= imgB[ro-radius-1,co][0]/windowLen
                totalG -= imgB[ro-radius-1,co][1]/windowLen
                totalB -= imgB[ro-radius-1,co][2]/windowLen
            if (ro+radius) <= imgHeight-1:
                totalR += imgB[ro+radius,co][0]/windowLen
                totalG += imgB[ro+radius,co][1]/windowLen
                totalB += imgB[ro+radius,co][2]/windowLen

            imgC[ro,co] = [totalR,totalG,totalB]

    return imgC

# number of times to run blur operation
blurPasses = 3

# temporary image array for multiple passes
imgTmp = imgArr

for k in range(blurPasses):
    imgTmp = doblur(imgTmp)
    print "pass #",k,"done."

imgOut = Image.fromarray(numpy.uint8(imgTmp))

imgOut.save('testimage-processed.png', 'PNG')
2
Can you post some sample input/output?Blender
I always get worried when I see a-b-c. I never remember the associativity of operators well enough in any language to know whether it will be interpreted as a-(b-c) or (a-b)-csarnold
In every language I've used, addition, subtraction, multiplication, and division associate left to right, as in math. Exponentiation is the only common one that often associates right to left.David Z
only a curiosity: is it fast? there is a way to rewrite the "for r in rows: for c in columns: ..." loop in more efficent way with some numpy trick? i thought that python code should avoid long for loop for computational velocity problem..nkint

2 Answers

2
votes

I suppose you have an issue with the line

for rads in range(-radius, radius):

which runs to radius-1 only (range excludes last). Add one to the second range argument.

Update: There is another small isue within the line

if (co-radius-1) > 0:

which should be

if (co-radius-1) >= 0:
0
votes

I modified/refactored your code just a bit, and thought I'd share. I needed something to do a custom blur that would: 1) work on a data array, and 2) only wrap horizontally and not vertically. As the TODO notes, I'm thinking of further refactoring so it can do partial pixel blends (i.e. 0.5). Hope this helps someone:

def blur_image(image_data, blur_horizontal=True, blur_vertical=True, height=256, width=256, radius=1):
    #TODO: Modify to support partial pixel blending

    # blur window length in pixels
    blur_window = radius*2+1

    out_image_data = image_data

    # blur horizontal row by row, and wrap around edges
    if blur_horizontal:
        for row in range(height):
            for column in range(0, width):
                total_red = 0
                total_green = 0
                total_blue = 0

                for rads in range(-radius, radius+1):
                    pixel = (row*width) + ((column+rads) % width)
                    total_red += image_data[pixel][0]/blur_window
                    total_green += image_data[pixel][1]/blur_window
                    total_blue += image_data[pixel][2]/blur_window

                out_image_data[row*width + column] = (total_red, total_green, total_blue, 255)
        image_data = out_image_data

    # blur vertical, but no wrapping
    if blur_vertical:
        for column in range(width):
            for row in range(0, height):
                total_red = 0
                total_green = 0
                total_blue = 0

                blur_window = 0
                for rads in range(-radius, radius+1):
                    if rads in range(0, height):
                        blur_window += 1

                for rads in range(-radius, radius+1):
                    row_mod = row+rads
                    if row_mod in range(0, height):
                        pixel = (row_mod*width) + column
                        total_red += image_data[pixel][0]/blur_window
                        total_green += image_data[pixel][1]/blur_window
                        total_blue += image_data[pixel][2]/blur_window

                out_image_data[row*width + column] = (total_red, total_green, total_blue, 255)
        image_data = out_image_data

    return image_data

You can use it when you've already got an image that's in an array of RGBA pixels, then run:

image_data = blur_image(image_data, height=height, width=width, radius=2)

im = Image.new('RGB', (width, height))
im.putdata(image_data)