Home > Back-end >  What is the most efficient way to adjust hue of an image in Python?
What is the most efficient way to adjust hue of an image in Python?

Time:07-01

I am trying to turn images into infinite looping GIFs, basically you have an image and a number, you then create an array of that number elements, each element is the original image with hue rotated by index divided by number times 360°, and you save the array as a GIF.

Working solution:

import numpy as np
from PIL import Image

def rgb_to_hsv(rgb):
    rgb = rgb.astype('float')
    hsv = np.zeros_like(rgb)
    r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
    maxc = np.max(rgb[..., :3], axis=-1)
    minc = np.min(rgb[..., :3], axis=-1)
    hsv[..., 2] = maxc
    mask = maxc != minc
    hsv[mask, 1] = (maxc - minc)[mask] / maxc[mask]
    rc = np.zeros_like(r)
    gc = np.zeros_like(g)
    bc = np.zeros_like(b)
    rc[mask] = (maxc - r)[mask] / (maxc - minc)[mask]
    gc[mask] = (maxc - g)[mask] / (maxc - minc)[mask]
    bc[mask] = (maxc - b)[mask] / (maxc - minc)[mask]
    hsv[..., 0] = np.select(
        [r == maxc, g == maxc], [bc - gc, 2.0   rc - bc], default=4.0   gc - rc)
    hsv[..., 0] = (hsv[..., 0] / 6.0) % 1.0
    return hsv


def huegify(img, filepath, n=360):
    assert 0 < n <= 360
    height, width = img.shape[:2]
    hsv = rgb_to_hsv(img)
    h, s, v = hsv[:, :, 0], hsv[:, :, 1], hsv[:, :, 2]
    p = v * (1.0 - s)
    def adjust_hue(d):
        rgb = np.zeros([height, width, 3])
        h = ((h d/n)%1)*6
        i = h.astype('uint8')
        f = h - i
        q = v * (1.0 - s * f)
        t = v * (1.0 - s * (1.0 - f))
        i = i % 6
        conditions = [s == 0.0, i == 1, i == 2, i == 3, i == 4, i == 5]
        rgb[..., 0] = np.select(conditions, [v, q, p, p, t, v], default=v)
        rgb[..., 1] = np.select(conditions, [v, v, v, q, p, p], default=t)
        rgb[..., 2] = np.select(conditions, [v, p, t, v, v, q], default=p)
        return rgb.astype('uint8')
    images = [Image.fromarray(adjust_hue(i)) for i in range(n)]
    images[0].save(filepath, format='GIF', save_all=True, append_images=images[1:], quality=100, loop=0, duration=42)

Example input:

enter image description here

Example output

GIF file

Method taken from here

The above is the fastest method I have found, but it is still not ideal, it takes around 528 milliseconds to complete one shift (or 170 milliseconds to create an RGB image from adjusted HSV values):

In [38]: %%timeit
    ...: hsv = rgb_to_hsv(arr)
    ...: h, s, v = hsv[:, :, 0], hsv[:, :, 1], hsv[:, :, 2]
    ...: Image.fromarray(hsv_to_rgb((h 1/360)%1, s, v))
528 ms ± 29.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [39]: hsv = rgb_to_hsv(arr)
    ...: h, s, v = hsv[:, :, 0], hsv[:, :, 1], hsv[:, :, 2]

In [40]: %timeit Image.fromarray(hsv_to_rgb((h 1/360)%1, s, v))
170 ms ± 2.81 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Is there any way this can be faster? All other methods I have found are using for loops.


Edit

I am able to give it a little speed up, but it is not very much.

CodePudding user response:

I switched to cv2 and now it's much faster. I also replaced GIF with MP4, because GIFs are of low quality.

But I didn't use cv2.VideoWriter, because: 1, I can't control the bitrate, and 2, it doesn't use an FFMPEG version that supports CUDA, instead I found pre-compiled FFMPEG binaries with CUDA support for Windows here.

The code:

import cv2
import os

FFMPEG = 'D:/ffmpeg/ffmpeg.exe'

def hueloopvid(imagefile, outfile, n=256, loops=1, fps=24):
    assert 0 < n <= 256
    rgb = cv2.imread(imagefile, cv2.IMREAD_COLOR)
    height, width = rgb.shape[:2]
    hsv = cv2.cvtColor(rgb, cv2.COLOR_BGR2HSV_FULL)
    h = hsv[:, :, 0]
    chsv = hsv.copy()
    file_name = imagefile.split('/')[-1][::-1].split('.', 1)[1][::-1]
    tmp_folder = os.environ['tmp']
    for i in range(n):
        chsv[..., 0] = (h   round(i/n*256)) % 256
        cv2.imwrite('{}/{}_{}.png'.format(tmp_folder, file_name, i), cv2.cvtColor(chsv, cv2.COLOR_HSV2BGR_FULL))
    
    command = '{} -y -stream_loop {} -framerate {} -hwaccel cuda -hwaccel_output_format cuda -i {}/{}_%d.png -c:v h264_nvenc -b:v 5M -vf scale={}:{} {}'
    os.system(command.format(FFMPEG, loops-1, fps, tmp_folder, file_name, width, height, outfile))
    for i in range(n):
        os.remove('{}/{}_{}.png'.format(tmp_folder, file_name, i))

if __name__ == '__main__':
    hueloopvid("D:/images/Matplotlib/Misc/new_art_32.png", 'D:/hueloopvid.mp4', loops=6, fps=24)

It is tremendously faster than before:

In [12]: imagefile = "D:/images/Matplotlib/Misc/new_art_32.png"

In [13]: %%timeit
    ...: rgb = cv2.imread(imagefile, cv2.IMREAD_COLOR)
    ...: height, width = rgb.shape[:2]
    ...: hsv = cv2.cvtColor(rgb, cv2.COLOR_BGR2HSV_FULL)
    ...: h = hsv[:, :, 0]
    ...: chsv = hsv.copy()
    ...: images = []
libpng warning: iCCP: known incorrect sRGB profile
...
23.4 ms ± 2.48 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [14]: rgb = cv2.imread(imagefile, cv2.IMREAD_COLOR)
    ...: height, width = rgb.shape[:2]
    ...: hsv = cv2.cvtColor(rgb, cv2.COLOR_BGR2HSV_FULL)
    ...: h = hsv[:, :, 0]
    ...: chsv = hsv.copy()
    ...: images = []
libpng warning: iCCP: known incorrect sRGB profile

In [15]: %%timeit
    ...: images = []
    ...: for i in range(256):
    ...:     chsv[..., 0] = (h   i) % 256
    ...:     images.append(cv2.cvtColor(chsv, cv2.COLOR_HSV2BGR_FULL))
3.81 s ± 94.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [16]: %%timeit
    ...: chsv[..., 0] = (h   1) % 256
    ...: cv2.cvtColor(chsv, cv2.COLOR_HSV2BGR_FULL)
15.5 ms ± 689 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  • Related