2
\$\begingroup\$

I'm a hobby programer and I had an idea for pixelating images and I wrote it.

I was wondering how would a better programer write the same function.

I want to learn and I think that this would help me a lot to see how code is supposed to be written. Also my code is quite slow and I would like to make it quicker.

I use PIL for image manipulation, pygame for window and numpy for math. after this code I return img that is in PIL format and pass it to pygame with fromstring.

This code divides the image into blocks defined by pixSize and then takes RGB value from all the pixels in that block and finds median value with which then that block is colored and then it moves on to next block and and does the same.

Here is my code

def pixelateImage(self, img, pixSize):
        print(img.size[1])
        
        n = 0
        ny = 0
        blockInfo = []
        for i in range(int(img.size[0]/pixSize)*int(img.size[1]/pixSize)):
            if n+1 > int(img.size[0]/pixSize):
                n = 0
                ny += 1
            xs = range(pixSize*n, pixSize*(n+1))
            ys = range(pixSize*ny, pixSize*(ny+1))
            RGB = []
           
            for x in xs:
                for y in ys:
                   
                    cModeRGB = []
                    rgb = img.getpixel((x,y))
                    for i in rgb:
                        if i == 0:
                            cModeRGB.append(0)
                            continue
                        cMode = i / 255
                        cModeRGB.append(cMode)
                    
                    RGB.append(cModeRGB)
            RGBc = np.array(RGB)
           
            RGB = []
            for i in np.median(RGBc, axis=0):
                RGB.append(int(i * 255))

            rgbMid = (RGB[0], RGB[1], RGB[2])    
            
            for x in xs:
                for y in ys:
                    img.putpixel((x,y), rgbMid)
            n += 1
        return img
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Please show the entire program if you're able. This is not enough review context. \$\endgroup\$
    – Reinderien
    Commented Oct 7, 2022 at 17:15
  • 1
    \$\begingroup\$ Just FIY, this is called a median filter :) \$\endgroup\$
    – IEatBagels
    Commented Oct 8, 2022 at 19:18
  • \$\begingroup\$ I wrote a 2-liner here, with slightly different approach (mean vs median)... stackoverflow.com/a/51547855/2836621 \$\endgroup\$ Commented Dec 3, 2023 at 11:19

2 Answers 2

1
\$\begingroup\$
def pixelateImage(self, img, pixSize):

You are writing a class method, but the method has nothing to do with the class. Make it a free function. I know a lot of people swear by OOP (as opposed to swear at it, like I do), but the only difference for this particular function being a class method or a free function is in how you use it:

obj = MyImageProcessingClass()
img = obj.pixelateImage(img, 3)

vs

img = pixelateImage(img, 3)

Creating that object really has no purpose, other than making the functionality harder to use.


if i == 0:
   cModeRGB.append(0)
   continue
cMode = i / 255
cModeRGB.append(cMode)

You are duplicating code here (two append calls) for the sake of avoiding the computation of 0/255. The conditional expression itself is a lot more expensive than the division you're avoiding. There really is no reason to avoid this division (note that it's not a division by zero, which would lead to warnings and NaN results and so on). It is best to simplify this code to just cModeRGB.append(i / 255).


int(img.size[0]/pixSize)

The other answer already mentioned that this is equivalent to, and better written as, img.size[0] // pixSize.


rgb = img.getpixel((x,y))

Calling functions like this is expensive. I would recommend converting the PIL image to a NumPy array at the start of your function, and converting it back at the end. This conversion is cheap (data is typically not copied), but allows for much easier pixel access:

img = np.asarray(img).copy()
# ...
   rgb = img[y,x]
# ...
return PIL.Image.fromarray(img)

Note that the NumPy array is of size height x width x 3.

Note that np.asarray() can lead to a read-only array, here I added the .copy(), which does a deep copy of the data and assures a writeable array.


You use a lot of loops to copy individual values over into arrays. Instead, and using NumPy indexing, fetch blocks of pixels at once. The double loop over xs and ys that ends with RGBc = np.array(RGB) can be simply written as:

RGBc = img[pixSize*n : pixSize*(n+1), pixSize*ny : pixSize*(ny+1), :]

Similarly, the following loop over i (here you're re-using the loop index of the outer loop, which in this case doesn't hurt, but is not good practice, use a different loop variable) can be written as

rgbMid = tuple(np.median(RGBc, axis=(0, 1)))

...but you don't really need to convert this to a tuple if you keep using NumPy arrays everywhere, you can directly assign the array returned by np.median to all the pixels:

img[pixSize*n : pixSize*(n+1), pixSize*ny : pixSize*(ny+1), :] = rgbMid

Note that in the call to np.median I wrote axis=(0, 1). RGBc is now a 3D array, with color along the 3rd dimension. We need to compute the median over the first two (spatial) dimensions.


You divide each pixel value by 255 when you read it, you compute the median, then multiply the result by 255 before writing it back into the image. You could skip the division and multiplication, it accomplishes nothing.


Finally, your outer loop determines how many iterations to run, then it figures out the start coordinates for each loop iteration. I think it would be clearer to simply loop over the start coordinates for each block:

for ny in range(0, img.shape[0], pixSize):
   for nx in range(0, img.shape[1], pixSize):

Now we don't need to multiply n (which I've named nx for clarity) and ny by pixSize every time we use them: they are incremented by that amount automatically. I've used img.shape[1] as the image width and img.shape[0] as the height, assuming img is a NumPy array at this point.

Next, to avoid indexing out of bounds in case the image is not evenly divisible into pixSize square blocks, we can compute the end points for each block as follows:

for ny in range(0, img.shape[0], pixSize):
   ny_end = min(ny + pixSize, img.shape[0])
   for nx in range(0, img.shape[1], pixSize):
      nx_end = min(nx + pixSize, img.shape[1])

We compute ny_end outside the loop over nx, to avoid repeating the same calculation unnecessarily.

We put the loop over ny as the outer loop, and nx as the inner loop, because this way we access pixels in storage order. It is always faster to loop over data in storage order, because we make better use of the cache.


Putting it all together, I end up with the following function:

def pixelateImage(img, pixSize):
   img = np.asarray(img).copy()
   for ny in range(0, img.shape[0], pixSize):
      ny_end = min(ny + pixSize, img.shape[0])
      for nx in range(0, img.shape[1], pixSize):
         nx_end = min(nx + pixSize, img.shape[1])
         window = img[ny:ny_end, nx:nx_end, :]
         color = np.median(window, axis=(0, 1))
         img[ny:ny_end, nx:nx_end, :] = color
   return PIL.Image.fromarray(img)

It produces the same result as the original implementation, but is much shorter and faster by more than an order of magnitude.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you very much, this is exactly kind of an answer I was hoping to get. You explained everything clearly and showed me how much more I need to learn. \$\endgroup\$ Commented Oct 9, 2022 at 15:40
3
\$\begingroup\$

Static methods

This seems to be defined in a class, since you have the self parameter. However, that is not used, so you would probably be better off defining it as a @staticmethod, and dropping the self parameter.

    @staticmethod
    def pixelateImage(img, pixSize):

Variable names

Variable names like n and ny which are not descriptive are discouraged.

Also, according to PEP 8, local variables in functions should be lowercase. This means:

  • pixSize should be pix_size
  • RGB should be rgb
  • cModeRGB should be c_mode_rgb
  • rgbMid should be rgb_mid

Floor division

Finally, you have used int(... / ...) a bit. This is actually the same as floor division.

  • int(img.size[0]/pixSize)*int(img.size[1]/pixSize) can be (img.size[0] // pixSize) * (img.size[1] // pixSize)
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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