Lookup table (LUT, LookUp Table) is a powerful tool for image color conversion, used in many graphics and video editors.
2D LUT
2D LUT generation
def generate_identify_color_matrix(width, height, channel):
img = np.zeros((width, height, channels), dtype=np.uint8)
for by in range(8):
for bx in range(8):
for g in range(64):
for r in range(64):
x = r + bx * 64
y = g + by * 64
img[y][x][0] = int(r * 255.0 / 63.0 + 0.5)
img[y][x][1] = int(g * 255.0 / 63.0 + 0.5)
img[y][x][2] = int((bx + by * 8.0) * 255.0 / 63.0 + 0.5)
return img
identity_lut = generate_identify_color_matrix()
2D LUT for image conversion
from typing import Tuple
from functools import lru_cache
from math import floor
import numpy as np
import cv2
def lut_apply(image, lut):
img_list = image.tolist()
# dst_img = image.copy()
dst_img = np.zeros((image.shape[0], image.shape[1], image.shape[2]), dtype=np.uint8)
for iy in range(image.shape[0]):
for ix in range(image.shape[1]):
b, g, r = img_list[iy][ix] #bgr mode
x, y, bx, by = color_coordinate(r, g, b)
lut_y = y + by * 64
lut_x = x + bx * 64
dst_img[iy][ix][0] = lut[lut_y][lut_x][0]
dst_img[iy][ix][1] = lut[lut_y][lut_x][1]
dst_img[iy][ix][2] = lut[lut_y][lut_x][2]
return dst_img
@lru_cache(maxsize=512)
def color_coordinate(r, g, b) -> Tuple[int, int, int, int]:
x, y, bx, by = 0, 0, 0, 0
x = floor(r / 4.0)
y = floor(g / 4.0)
bx, by = blue_coordinate(floor(b / 4.0))
return x, y, bx, by
@lru_cache(maxsize=64)
def blue_coordinate(b: int) -> Tuple[int, int]:
assert b >= 0 and b <= 63, 'GOT {}'.format(b)
x, y = 0, 0
y = floor(floor(b) / 8.0)
x = int(floor(b) - y * 8.0)
return x, y
image = cv2.imread('./test.jpg')
lut = cv2.imread('./YV2.JPG')
result_img = lut_apply(image, lut)
cv2.imwrite('./test_YV2_cv.jpg', result_img)
Quick way:
class LUT_WHITEN:
def __init__(self, lut):
cube64rows = 8
cube64size = 64
cube256size = 256
cubescale = (int)(cube256size / cube64size) #4
reshapelut = np.zeros((cube256size, cube256size, cube256size, 3))
for i in range(cube64size):
tmp = math.floor(i / cube64rows)
cx = int((i - tmp * cube64rows) * cube64size)
cy = int(tmp * cube64size)
cube64 = lut[cy:cy+cube64size, cx:cx+cube64size]#cube64 in lut(512*512 (512=8*64))
_rows, _cols, _ = cube64.shape
if _rows == 0 or _cols == 0:
continue
cube256 = cv2.resize(cube64, (cube256size, cube256size))
i = i * cubescale
for k in range(cubescale):
reshapelut[i + k] = cube256
self.lut = reshapelut
# print('reshapelut shape:', reshapelut.shape)
def imageInLut(self, src):
arr = src.copy()
bs = arr[:, :, 0]
gs = arr[:, :, 1]
rs = arr[:, :, 2]
arr[:, :] = self.lut[bs, gs, rs]
return arr
3D LUT
Common formats of 3D LUT:
- Hald CLUT(.png)
- 3D LUT(.3dl)
- Cube LUT (.cube)
3D LUT generation (Hald CLUT)
-
Create identity image (3D LUT)
./bin/generate.py
def generate_hald(size): b, g, r = numpy.mgrid[ 0 : 255 : size**2*1j, 0 : 255 : size**2*1j, 0 : 255 : size**2*1j ].astype(numpy.uint8) rgb = numpy.stack((r, g, b), axis=-1) return Image.fromarray(rgb.reshape(size**3, size**3, 3))
In order to prevent distortion that may occur during the transformation, such as vignetting, scratches, gradation and JPEG artifacts. Generated 25 × 25 × 25 25 \times 25 \times 2525×25×The hadl image of 2 5 d is as follows (4 are shown):
-
Use image processing software to transform the hald image to obtain the filtered hald image of the same size.
-
Improved method, using Gaussian Blur filtered image for processing to reduce noise.
If the target filter is severely distorted at the local level or the image center has a significant gradient, some undesirable effects may appear. The filtered image in question is as follows:
./bin/convert.py raw/15.Hudson.jpg halds/ --smooth 1.5
-
Convert the filtered image to a real hald image (one of the 4 shown is cropped).
3D LUT (Hald CLUT) applied to images (Pillow library)
pillow-lut-tools
Pillow LUT tools
Pillow LUT tools contain tools for loading, operating and generating 3D lookup tables, and are designed specifically for the Pillow library.
from PIL import Image
from pillow_lut import load_hald_image
hefe = load_hald_image('./res/test_hald.5.png')
img = Image.open('./res/test.jpg')
img.filter(hefe).save('./res/test.hald.jpg')
Functions included in Pillow LUT:
-
load_cube_file loads 3D lookup table from .cube file format
-
load_hald_image loads a 3D lookup table from a Hald image (usually a .png or .tiff file).
-
identity_table returns a noop lookup table with linearly distributed values.
-
rgb_color_enhance generates a three-dimensional color lookup table based on the given value of the basic color settings.
-
sample_lut_linear uses linear interpolation to calculate new point values from a given three-dimensional lookup table.
-
sample_lut_cubic uses cubic interpolation to calculate new point values from a given three-dimensional lookup table.
-
resize_lut uses interpolation to resize the given lookup table to the new size.
-
transform_lut uses another table to transform the given lookup table and returns the result.
-
amplity_lut amplifies the given lookup table
Pillow LUT function usage example:
from PIL import Image
from pillow_lut import load_hald_image
hefe = load_hald_image('./res/hald.6.hefe.png')
im = Image.open('./res/pineapple.jpeg')
im.filter(hefe).save('./res/pineapple.hefe.jpeg')
from pillow_lut import rgb_color_enhance
lut = rgb_color_enhance(11, exposure=0.2, contrast=0.1, vibrance=0.5, gamma=1.3)
im = Image.open('./res/pineapple.jpeg')
im.filter(lut).save('./res/pineapple.enhance.jpeg')
from pillow_lut import load_hald_image, rgb_color_enhance
hefe = load_hald_image('./res/hald.6.hefe.png')
lut = rgb_color_enhance(hefe, exposure=0.2, contrast=0.1, vibrance=0.5, gamma=1.3)
im = Image.open('./res/pineapple.jpeg')
im.filter(lut).save('./res/pineapple.hefe.enhance.jpeg')
3D LUT (Hald CLUT) converted to 2D square CLUT
import numpy, cv2
hald = cv2.imread('1.Clarendon.png')
size = int(hald.shape[0] ** (1.0/3.0) + .5)
clut = numpy.concatenate([
numpy.concatenate(im.reshape((size, size, size**2, size**2, 3))[row], axis=1)
for row in range(size)
])
cv2.imwrite("clut.png", clut)
Conversion between 2D LUT and 3D LUT
The single-channel conversion diagram is easy to understand. The error-prone areas of 3D LUT to 2D LUT have been marked in red.
2D LUT to 3D LUT conversion
# 2D LUT(size**3, size**3, 3) -> 3D LUT(size**3, size**3, 3)
clut = cv2.imread('./identity_clut_8.png')
size = int(clut.shape[0] ** (1.0 / 3.0) + 0.5)
clut_result = np.zeros((size ** 2, size ** 2, size ** 2, 3))
for i in range(size ** 2):
tmp1 = math.floor(i / size)
cx = int((i - tmp1 * size) * size ** 2)
cy = int(tmp1 * size ** 2)
clut_result[i] = clut[cy: cy + size ** 2, cx : cx + size ** 2]
hald = clut_result.reshape((size ** 3, size ** 3, 3))
cv2.imwrite('./identity_hald_{}_test.png'.format(size), hald)
3D LUT to 2D LUT conversion
#3D LUT(size**3, size**3, 3) -> 2D LUT(size**3, size**3, 3)
hald = cv2.imread('./identity_hald_8.png')
size = int(hald.shape[0] ** (1.0/3.0) + .5)
#method1
clut = np.concatenate([
np.concatenate(hald.reshape((size, size, size**2, size**2, 3))[row], axis=1)
for row in range(size)
])
cv2.imwrite("2d_identity.png", clut)
#method2
hald_reshape = hald.reshape((size, size, size ** 2, size ** 2, 3))
print('hald_reshape shape:', hald_reshape.shape)
img_list = []
for i in range(size):
print('hald_reshape[i] shape:', hald_reshape[i].shape)
tmp = np.concatenate(hald_reshape[i], axis=1)
print('tmp shape:', tmp.shape)
img_list.append(tmp)
result = np.concatenate(img_list)
print('result shape:', result.shape)
cv2.imwrite("identity_clut_8_test.png", result)
Reference
color-filters-reconstruction
pillow-lut-tools
Pillow LUT tools
Create your own LUT
CLUT-from-images