0

I have started working with images and currently I am trying to rescale and grayscale an image (Size 6000x4000 -> 600x400) to better work with it. For this I am using Numpy and PIL.Images.

import PIL.Image as Img
import numpy as np

img = Img.open('rendering/testpic.jpg', 'r')

r, g, b = img.split()
channels = np.array([np.array(r), np.array(g), np.array(b)])

small_channels = []
for channel in channels:
    x_len = len(channel)//10
    y_len = len(channel[0])//10
    for chunk_x in range(x_len):
        for chunk_y in range(y_len):
            pix_sum = 0
            for x_in_chunk in range(10):
                for y_in_chunk in range(10):
                    pix_sum += channel[chunk_x*10+x_in_chunk,chunk_y*10+y_in_chunk]
            channel[chunk_x,chunk_y] = pix_sum // 100
    small_channels.append(channel[:x_len,:y_len])

channels = np.array(small_channels)

grayscale = np.round((channels[0]*0.3+ channels[1]*0.6+ channels[2]*0.1)).astype('uint8')
pixels = np.stack([grayscale, grayscale, grayscale], axis = 2)
new_img = Img.fromarray(pixels)
new_img.show()

So what I am doing is chunking the channels into chunks size 10, then mapping the average of the chunk into the topleft corner. In the end I cut off the rest of the picture.

In total this takes around 100 to 130 seconds for me. Is there a faster way to do this? Where am I being inefficient? I'm new so I'm probably doing wrong a lot of stuff. How does Photoshop for example scale pictures up and down so fast?

Max
  • 15
  • 6

2 Answers2

1

Instead of looping over every pixel in your image we can use numpy array slicing and some methods to speed things up. I have removed the inner loops and used slicing and the .sum() method of numpy arrays:

import PIL.Image as Img
import numpy as np

img = Img.open('rendering/testpic.jpg', 'r')

r, g, b = img.split()
channels = np.array([np.array(r), np.array(g), np.array(b)])

small_channels = []
for channel in channels:
    x_len = len(channel)//10
    y_len = len(channel[0])//10
    for chunk_x in range(x_len):
        for chunk_y in range(y_len):
            # slice all pixels within 10*10 box and sum them
            pix_sum = channel[chunk_x*10:10*(chunk_x+1),chunk_y*10:10*(chunk_y+1)].sum()
            channel[chunk_x, chunk_y] = pix_sum // 100
    small_channels.append(channel[:x_len,:y_len])

channels = np.array(small_channels)

grayscale = np.round((channels[0]*0.3+ channels[1]*0.6+ channels[2]*0.1)).astype('uint8')
pixels = np.stack([grayscale, grayscale, grayscale], axis = 2)
new_img = Img.fromarray(pixels)
new_img.show()

This algorithm is 3-4 times faster by my testing. I hope this helps. Definitely have a look at numpy arrays- they are very useful especially for images and the computation is quicker in a lot of cases.

Paddy Harrison
  • 1,808
  • 1
  • 8
  • 21
  • Do you have any suggestions to improve my code (like from the ground up, concept-wise) for rescaling the image? – Max May 08 '20 at 16:34
  • 1
    There are a few options from well known packages, for example it looks like skimage.transform.downscale_local_mean looks to be exactly what you want: https://scikit-image.org/docs/dev/api/skimage.transform.html#downscale-local-mean – Paddy Harrison May 08 '20 at 19:46
  • 1
    Another example here too: https://scikit-image.org/docs/dev/auto_examples/transform/plot_rescale.html – Paddy Harrison May 08 '20 at 19:46
  • 1
    There are other options from scipy.ndimage and openCV as @sK500 has suggested. I would give them a look to see if their results are what you are after. Otherwise I would make as much use of numpy slicing as possible to speed up your code. I hope that’s given you some ideas! – Paddy Harrison May 08 '20 at 19:46
0

I wouldn't use loops in this case, cv2.resize() will do the job.

Here is a time comparison between the three approaches:

import PIL.Image as Img
import numpy as np
from time import perf_counter
import cv2


def timer(method):
    def timed(*args, **kwargs):
        t1 = perf_counter()
        result = method(*args, **kwargs)
        t2 = perf_counter() 
        print(f'{method.__name__} time: {t2 - t1} seconds')
        return result
    return timed


@timer
def resize_1(image_path, shrink):
    img = Img.open(image_path, 'r')
    r, g, b = img.split()
    channels = np.array([np.array(r), np.array(g), np.array(b)])
    small_channels = []
    for channel in channels:
        x_len = len(channel)//shrink
        y_len = len(channel[0])//shrink
        for chunk_x in range(x_len):
            for chunk_y in range(y_len):
                pix_sum = 0
                for x_in_chunk in range(shrink):
                    for y_in_chunk in range(shrink):
                        pix_sum += channel[chunk_x*shrink+x_in_chunk,chunk_y*shrink+y_in_chunk]
                channel[chunk_x,chunk_y] = pix_sum // 100
        small_channels.append(channel[:x_len,:y_len])
    channels = np.array(small_channels)
    grayscale = np.round((channels[0]*0.3+ channels[1]*0.6+ channels[2]*0.1)).astype('uint8')
    pixels = np.stack([grayscale, grayscale, grayscale], axis = 2)
    return Img.fromarray(pixels)


@timer
def resize_2(image_path, shrink):
    img = Img.open(image_path, 'r')
    r, g, b = img.split()
    channels = np.array([np.array(r), np.array(g), np.array(b)])
    small_channels = []
    for channel in channels:
        x_len = len(channel)//shrink
        y_len = len(channel[0])//shrink
        for chunk_x in range(x_len):
            for chunk_y in range(y_len):
                # slice all pixels within 10*10 box and sum them
                pix_sum = channel[chunk_x*shrink:shrink*(chunk_x+1),
                          chunk_y*shrink:shrink*(chunk_y+1)].sum()
                channel[chunk_x, chunk_y] = pix_sum // 100
        small_channels.append(channel[:x_len,:y_len])
    channels = np.array(small_channels)
    grayscale = np.round((channels[0]*0.3+ channels[1]*0.6+ channels[2]*0.1)).astype('uint8')
    pixels = np.stack([grayscale, grayscale, grayscale], axis = 2)
    return Img.fromarray(pixels)


@timer
def resize_3(image_path, shrink):
    image = cv2.imread(image_path)
    size = image.shape[:-1]
    new_size = tuple(int(item / shrink) for item in size)[::-1]
    resized = cv2.resize(image, tuple(new_size))
    gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
    return gray


if __name__ == '__main__':
    img = 'sample_image.png'
    shrink_by = 10
    image1, image2, image3 = [item(img, shrink_by) for item in [resize_1, resize_2, resize_3]]
    image1.show()
    image2.show()
    cv2.imshow('resize_3', image3)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

Out:

resize_1 time: 1.980221013 seconds
resize_2 time: 0.3170622839999999 seconds
resize_3 time: 0.01659756599999973 seconds