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.