First of all, I'm relatively new to Python and its libraries. I'm trying to loop through all the pixels of an image using Pillow to divide an image into a fraction of the original dimensions, by take the closest RGB color for the division matrix from all the colors of the original image. With the following function, this takes more than three hours for an average image with a large number of colors:
def divide_image_by_rgb(image_file, division=4):
input_img = Image.open(image_file)
width, height = input_img.size
pixels_count = width * height
if division < 2:
raise Exception("Invalid division!")
white = (255, 255, 255)
width_out = width // division
height_out = height // division
output_img = Image.new('RGB', (width_out, height_out), white)
all_colors = list(get_colors_count(input_img).keys())
for x in range(width_out):
for y in range(height_out):
pixels = []
[r, g, b] = [0, 0, 0]
for d1 in range(division):
for d2 in range(division):
pos_x = x * division + d1
pos_y = y * division + d2
if pos_x > width or pos_y > height:
continue
pixel = input_img.getpixel((pos_x, pos_y))
pixels.append(pixel)
r += pixel[0] ** 2
g += pixel[1] ** 2
b += pixel[2] ** 2
div = division ** 2
mean_color = (
round(sqrt(r // div)),
round(sqrt(g // div)),
round(sqrt(b // div))
)
mean_color = closest(all_colors, mean_color)
output_img.putpixel((x, y), mean_color)
# log_reduce_progress(changed, pixels_count, height, width, idx, x, y)
output_file = f"resized.[{division}].{os.path.basename(image_file)}"
output_img.save(output_file)
return output_file
def closest(colors, color):
colors = np.array(colors)
color = np.array(color)
distances = np.sqrt(np.sum((colors - color) ** 2, axis=1))
index_of_smallest = np.where(distances == np.amin(distances))
smallest_distance = tuple(colors[index_of_smallest][0])
return smallest_distance
Goal: I want to replace explicit loops using numpy
Input image: The input image is something like this:
PNG, Size: (6.32MP) Dimensions: 2228x2836, Depth: 24bit, Colors count: 632576
Result:
PNG, Size: (0.39MP) Dimensions: 557x709, Depth: 24bit, Colors count: 110896
Why: I want reduce image dimensions and colors to be smoothed with the least number of colors, the hue, uniformity and composition of the image should not be lost. I use libimagequant, quantize ADAPTIVE, reduce colors from given RGB palette csv, etc. before, but this method gives me the closest color combination to the desired result, I use result image as RGB palette to reduce original Image colors with respect to main colors.
Try: I tried to do this with numpy functions which failed and with every change, my errors and confusion increase:
def resize_image_manual_ex(image_file, division=4):
input_img = Image.open(image_file)
width, height = input_img.size
pixels_count = width * height
white = (255, 255, 255)
width_out = width // division
height_out = height // division
output_img = Image.new('RGB', (width_out, height_out), white)
# Convert the input image to a NumPy array
input_array = np.array(input_img)
# Use NumPy's reshaping to create sub-images
sub_images = input_array.reshape(height_out, division, width_out, division, 3)
# Calculate the mean color of each sub-image
mean_colors = np.mean(sub_images, axis=(1, 3))
# Find the closest color for each mean color
all_colors = list(get_colors_count(input_img).keys())
closest_colors = [closest(all_colors, color) for color in mean_colors]
for x in range(width_out):
for y in range(height_out):
output_img.putpixel((x, y), closest_colors[y, x])
output_file = f"resized.[{division}].{os.path.basename(image_file)}"
output_img.save(output_file)
return output_file
Last error:
sub_images = input_array.reshape(height_out, division, width_out, division, 3)
ValueError: cannot reshape array of size 56925 into shape (57,2,82,2,3)
Is there any way to replace for loops with numpy (or any else) to reduce the running time? Thank you very much.
This should do what you want, I think. I'm not to clear on PIL
commands, so check those.
import numpy as np
from PIL import Image
from skimage.util import view_as_blocks
from scipy.spatial import KDTree
import os
def divide_image_by_rgb(image_file, division=4):
"""this is all PIL stuff, ask another question if this doesn't work"""
input_img = Image.open(image_file)
if division < 2:
raise Exception("Invalid division!")
np_image = np.array(input_img)
mean_img = mean_divide_np_image(np_image, division)
output_img = Image.fromarray(mean_img)
output_file = f"resized.[{division}].{os.path.basename(image_file)}"
output_img.save(output_file)
return output_file
def mean_divide_np_image(input_img, division):
width, height, _ = input_img.shape
width_out = width // division
height_out = height // division
np_image = np.array(input_img)[:width_out * division, :height_out * division]
all_colors = np.unique(np_image.reshape(-1, 3), axis = 0)
img_blocks = view_as_blocks(np_image, (width_out, height_out, 3))
mean_img = np.mean(img_blocks**2, axis = (0, 1, 2))
mean_img = np.sqrt(closest(all_colors**2, mean_img)).astype(input_img.dtype)
return mean_img
def closest(colors, img):
color_tree = KDTree(colors)
dist, i = color_tree.query(img)
closest_color = colors[i]
return closest_color
Big things I did:
view
from view_as_blocks
to save memory. Maintained the root mean squared functionality needed to average RGB values, also carrying the squares through the closest
search.closest
to a KDTree
functionality. This is much faster for large numbers of colors