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?

4
  • what is the expected outcome? Commented May 7, 2020 at 19:36
  • 1
    you should consider OpenCV, which is reported to be considerably faster than PIL. Commented May 7, 2020 at 19:38
  • @sk500 well as I stated, it should rescale the image to a 10th of its original size. Commented May 8, 2020 at 16:37
  • For grayscaling with numpy you might want to check my answer here: stackoverflow.com/a/65919555/6342392, this approach takes 0.026 seconds to run Commented Jan 27, 2021 at 12:51

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

4 Comments

Do you have any suggestions to improve my code (like from the ground up, concept-wise) for rescaling the image?
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: scikit-image.org/docs/dev/api/…
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!
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

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.