mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-01-26 17:24:31 +03:00
Merge pull request #3056 from uploadcare/color-LUT
3D Color lookup tables
This commit is contained in:
commit
e24fad40ad
299
Tests/test_color_lut.py
Normal file
299
Tests/test_color_lut.py
Normal file
|
@ -0,0 +1,299 @@
|
|||
from __future__ import division
|
||||
|
||||
from PIL import Image, ImageFilter
|
||||
from helper import unittest, PillowTestCase
|
||||
|
||||
|
||||
class TestColorLut3DCoreAPI(PillowTestCase):
|
||||
def generate_unit_table(self, channels, size):
|
||||
if isinstance(size, tuple):
|
||||
size1D, size2D, size3D = size
|
||||
else:
|
||||
size1D, size2D, size3D = (size, size, size)
|
||||
|
||||
table = [
|
||||
[
|
||||
r / float(size1D-1) if size1D != 1 else 0,
|
||||
g / float(size2D-1) if size2D != 1 else 0,
|
||||
b / float(size3D-1) if size3D != 1 else 0,
|
||||
r / float(size1D-1) if size1D != 1 else 0,
|
||||
g / float(size2D-1) if size2D != 1 else 0,
|
||||
][:channels]
|
||||
for b in range(size3D)
|
||||
for g in range(size2D)
|
||||
for r in range(size1D)
|
||||
]
|
||||
return (
|
||||
channels, size1D, size2D, size3D,
|
||||
[item for sublist in table for item in sublist])
|
||||
|
||||
def test_wrong_args(self):
|
||||
im = Image.new('RGB', (10, 10), 0)
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "filter"):
|
||||
im.im.color_lut_3d('RGB', Image.CUBIC,
|
||||
*self.generate_unit_table(3, 3))
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "image mode"):
|
||||
im.im.color_lut_3d('wrong', Image.LINEAR,
|
||||
*self.generate_unit_table(3, 3))
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "table_channels"):
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(5, 3))
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "table_channels"):
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(1, 3))
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "table_channels"):
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(2, 3))
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "Table size"):
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(3, (1, 3, 3)))
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "Table size"):
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(3, (66, 3, 3)))
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, r"size1D \* size2D \* size3D"):
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
3, 2, 2, 2, [0, 0, 0] * 7)
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, r"size1D \* size2D \* size3D"):
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
3, 2, 2, 2, [0, 0, 0] * 9)
|
||||
|
||||
def test_correct_args(self):
|
||||
im = Image.new('RGB', (10, 10), 0)
|
||||
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(3, 3))
|
||||
|
||||
im.im.color_lut_3d('CMYK', Image.LINEAR,
|
||||
*self.generate_unit_table(4, 3))
|
||||
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(3, (2, 3, 3)))
|
||||
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(3, (65, 3, 3)))
|
||||
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(3, (3, 65, 3)))
|
||||
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(3, (3, 3, 65)))
|
||||
|
||||
def test_wrong_mode(self):
|
||||
with self.assertRaisesRegexp(ValueError, "wrong mode"):
|
||||
im = Image.new('L', (10, 10), 0)
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(3, 3))
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "wrong mode"):
|
||||
im = Image.new('RGB', (10, 10), 0)
|
||||
im.im.color_lut_3d('L', Image.LINEAR,
|
||||
*self.generate_unit_table(3, 3))
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "wrong mode"):
|
||||
im = Image.new('L', (10, 10), 0)
|
||||
im.im.color_lut_3d('L', Image.LINEAR,
|
||||
*self.generate_unit_table(3, 3))
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "wrong mode"):
|
||||
im = Image.new('RGB', (10, 10), 0)
|
||||
im.im.color_lut_3d('RGBA', Image.LINEAR,
|
||||
*self.generate_unit_table(3, 3))
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "wrong mode"):
|
||||
im = Image.new('RGB', (10, 10), 0)
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(4, 3))
|
||||
|
||||
def test_correct_mode(self):
|
||||
im = Image.new('RGBA', (10, 10), 0)
|
||||
im.im.color_lut_3d('RGBA', Image.LINEAR,
|
||||
*self.generate_unit_table(3, 3))
|
||||
|
||||
im = Image.new('RGBA', (10, 10), 0)
|
||||
im.im.color_lut_3d('RGBA', Image.LINEAR,
|
||||
*self.generate_unit_table(4, 3))
|
||||
|
||||
im = Image.new('RGB', (10, 10), 0)
|
||||
im.im.color_lut_3d('HSV', Image.LINEAR,
|
||||
*self.generate_unit_table(3, 3))
|
||||
|
||||
im = Image.new('RGB', (10, 10), 0)
|
||||
im.im.color_lut_3d('RGBA', Image.LINEAR,
|
||||
*self.generate_unit_table(4, 3))
|
||||
|
||||
def test_units(self):
|
||||
g = Image.linear_gradient('L')
|
||||
im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90),
|
||||
g.transpose(Image.ROTATE_180)])
|
||||
|
||||
# Fast test with small cubes
|
||||
for size in [2, 3, 5, 7, 11, 16, 17]:
|
||||
self.assert_image_equal(im, im._new(
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(3, size))))
|
||||
|
||||
# Not so fast
|
||||
self.assert_image_equal(im, im._new(
|
||||
im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
*self.generate_unit_table(3, (2, 2, 65)))))
|
||||
|
||||
def test_units_4channels(self):
|
||||
g = Image.linear_gradient('L')
|
||||
im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90),
|
||||
g.transpose(Image.ROTATE_180)])
|
||||
|
||||
# Red channel copied to alpha
|
||||
self.assert_image_equal(
|
||||
Image.merge('RGBA', (im.split()*2)[:4]),
|
||||
im._new(im.im.color_lut_3d('RGBA', Image.LINEAR,
|
||||
*self.generate_unit_table(4, 17))))
|
||||
|
||||
def test_copy_alpha_channel(self):
|
||||
g = Image.linear_gradient('L')
|
||||
im = Image.merge('RGBA', [g, g.transpose(Image.ROTATE_90),
|
||||
g.transpose(Image.ROTATE_180),
|
||||
g.transpose(Image.ROTATE_270)])
|
||||
|
||||
self.assert_image_equal(im, im._new(
|
||||
im.im.color_lut_3d('RGBA', Image.LINEAR,
|
||||
*self.generate_unit_table(3, 17))))
|
||||
|
||||
def test_channels_order(self):
|
||||
g = Image.linear_gradient('L')
|
||||
im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90),
|
||||
g.transpose(Image.ROTATE_180)])
|
||||
|
||||
# Reverse channels by splitting and using table
|
||||
self.assert_image_equal(
|
||||
Image.merge('RGB', im.split()[::-1]),
|
||||
im._new(im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
3, 2, 2, 2, [
|
||||
0, 0, 0, 0, 0, 1,
|
||||
0, 1, 0, 0, 1, 1,
|
||||
|
||||
1, 0, 0, 1, 0, 1,
|
||||
1, 1, 0, 1, 1, 1,
|
||||
])))
|
||||
|
||||
def test_overflow(self):
|
||||
g = Image.linear_gradient('L')
|
||||
im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90),
|
||||
g.transpose(Image.ROTATE_180)])
|
||||
|
||||
transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
3, 2, 2, 2,
|
||||
[
|
||||
-1, -1, -1, 2, -1, -1,
|
||||
-1, 2, -1, 2, 2, -1,
|
||||
|
||||
-1, -1, 2, 2, -1, 2,
|
||||
-1, 2, 2, 2, 2, 2,
|
||||
])).load()
|
||||
self.assertEqual(transformed[0, 0], (0, 0, 255))
|
||||
self.assertEqual(transformed[50, 50], (0, 0, 255))
|
||||
self.assertEqual(transformed[255, 0], (0, 255, 255))
|
||||
self.assertEqual(transformed[205, 50], (0, 255, 255))
|
||||
self.assertEqual(transformed[0, 255], (255, 0, 0))
|
||||
self.assertEqual(transformed[50, 205], (255, 0, 0))
|
||||
self.assertEqual(transformed[255, 255], (255, 255, 0))
|
||||
self.assertEqual(transformed[205, 205], (255, 255, 0))
|
||||
|
||||
transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR,
|
||||
3, 2, 2, 2,
|
||||
[
|
||||
-3, -3, -3, 5, -3, -3,
|
||||
-3, 5, -3, 5, 5, -3,
|
||||
|
||||
-3, -3, 5, 5, -3, 5,
|
||||
-3, 5, 5, 5, 5, 5,
|
||||
])).load()
|
||||
self.assertEqual(transformed[0, 0], (0, 0, 255))
|
||||
self.assertEqual(transformed[50, 50], (0, 0, 255))
|
||||
self.assertEqual(transformed[255, 0], (0, 255, 255))
|
||||
self.assertEqual(transformed[205, 50], (0, 255, 255))
|
||||
self.assertEqual(transformed[0, 255], (255, 0, 0))
|
||||
self.assertEqual(transformed[50, 205], (255, 0, 0))
|
||||
self.assertEqual(transformed[255, 255], (255, 255, 0))
|
||||
self.assertEqual(transformed[205, 205], (255, 255, 0))
|
||||
|
||||
|
||||
class TestColorLut3DFilter(PillowTestCase):
|
||||
def test_wrong_args(self):
|
||||
with self.assertRaisesRegexp(ValueError, "should be either an integer"):
|
||||
ImageFilter.Color3DLUT("small", [1])
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "should be either an integer"):
|
||||
ImageFilter.Color3DLUT((11, 11), [1])
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, r"in \[2, 65\] range"):
|
||||
ImageFilter.Color3DLUT((11, 11, 1), [1])
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, r"in \[2, 65\] range"):
|
||||
ImageFilter.Color3DLUT((11, 11, 66), [1])
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "table should have .+ items"):
|
||||
ImageFilter.Color3DLUT((3, 3, 3), [1, 1, 1])
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "table should have .+ items"):
|
||||
ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 2)
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "should have a length of 4"):
|
||||
ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 27, channels=4)
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "should have a length of 3"):
|
||||
ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8)
|
||||
|
||||
def test_convert_table(self):
|
||||
lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8)
|
||||
self.assertEqual(tuple(lut.size), (2, 2, 2))
|
||||
self.assertEqual(lut.name, "Color 3D LUT")
|
||||
|
||||
lut = ImageFilter.Color3DLUT((2, 2, 2), [
|
||||
(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11),
|
||||
(12, 13, 14), (15, 16, 17), (18, 19, 20), (21, 22, 23)])
|
||||
self.assertEqual(tuple(lut.size), (2, 2, 2))
|
||||
self.assertEqual(lut.table, list(range(24)))
|
||||
|
||||
lut = ImageFilter.Color3DLUT((2, 2, 2), [(0, 1, 2, 3)] * 8,
|
||||
channels=4)
|
||||
|
||||
def test_generate(self):
|
||||
lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b))
|
||||
self.assertEqual(tuple(lut.size), (5, 5, 5))
|
||||
self.assertEqual(lut.name, "Color 3D LUT")
|
||||
self.assertEqual(lut.table[:24], [
|
||||
0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.5, 0.0, 0.0, 0.75, 0.0, 0.0,
|
||||
1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0])
|
||||
|
||||
g = Image.linear_gradient('L')
|
||||
im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90),
|
||||
g.transpose(Image.ROTATE_180)])
|
||||
self.assertEqual(im, im.filter(lut))
|
||||
|
||||
lut = ImageFilter.Color3DLUT.generate(5, channels=4,
|
||||
callback=lambda r, g, b: (b, r, g, (r+g+b) / 2))
|
||||
self.assertEqual(tuple(lut.size), (5, 5, 5))
|
||||
self.assertEqual(lut.name, "Color 3D LUT")
|
||||
self.assertEqual(lut.table[:24], [
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.125, 0.0, 0.5, 0.0, 0.25,
|
||||
0.0, 0.75, 0.0, 0.375, 0.0, 1.0, 0.0, 0.5, 0.0, 0.0, 0.25, 0.125])
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "should have a length of 3"):
|
||||
ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b, r))
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, "should have a length of 4"):
|
||||
ImageFilter.Color3DLUT.generate(5, channels=4,
|
||||
callback=lambda r, g, b: (r, g, b))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
2
setup.py
2
setup.py
|
@ -39,7 +39,7 @@ _IMAGING = ("decode", "encode", "map", "display", "outline", "path")
|
|||
|
||||
_LIB_IMAGING = (
|
||||
"Access", "AlphaComposite", "Resample", "Bands", "BcnDecode", "BitDecode",
|
||||
"Blend", "Chops", "Convert", "ConvertYCbCr", "Copy", "Crop",
|
||||
"Blend", "Chops", "ColorLUT", "Convert", "ConvertYCbCr", "Copy", "Crop",
|
||||
"Dib", "Draw", "Effects", "EpsEncode", "File", "Fill", "Filter",
|
||||
"FliDecode", "Geometry", "GetBBox", "GifDecode", "GifEncode", "HexDecode",
|
||||
"Histo", "JpegDecode", "JpegEncode", "Matrix", "ModeFilter",
|
||||
|
|
|
@ -43,6 +43,7 @@ class Kernel(MultibandFilter):
|
|||
:param offset: Offset. If given, this value is added to the result,
|
||||
after it has been divided by the scale factor.
|
||||
"""
|
||||
name = "Kernel"
|
||||
|
||||
def __init__(self, size, kernel, scale=None, offset=0):
|
||||
if scale is None:
|
||||
|
@ -130,7 +131,6 @@ class MaxFilter(RankFilter):
|
|||
|
||||
class ModeFilter(Filter):
|
||||
"""
|
||||
|
||||
Create a mode filter. Picks the most frequent pixel value in a box with the
|
||||
given size. Pixel values that occur only once or twice are ignored; if no
|
||||
pixel value occurs more than twice, the original pixel value is preserved.
|
||||
|
@ -297,3 +297,98 @@ class SMOOTH_MORE(BuiltinFilter):
|
|||
1, 5, 5, 5, 1,
|
||||
1, 1, 1, 1, 1
|
||||
)
|
||||
|
||||
|
||||
class Color3DLUT(MultibandFilter):
|
||||
"""Three-dimensional color lookup table.
|
||||
|
||||
Transforms 3-channel pixels using the values of the channels as coordinates
|
||||
in the 3D lookup table and interpolating the nearest elements.
|
||||
|
||||
This method allows you to apply almost any color transformation
|
||||
in constant time by using pre-calculated decimated tables.
|
||||
|
||||
:param size: Size of the table. One int or tuple of (int, int, int).
|
||||
Minimal size in any dimension is 2, maximum is 65.
|
||||
:param table: Flat lookup table. A list of ``channels * size**3``
|
||||
float elements or a list of ``size**3`` channels-sized
|
||||
tuples with floats. Channels are changed first,
|
||||
then first dimension, then second, then third.
|
||||
Value 0.0 corresponds lowest value of output, 1.0 highest.
|
||||
:param channels: Number of channels in the table. Could be 3 or 4.
|
||||
Default is 3.
|
||||
:param target_mode: A mode for the result image. Should have not less
|
||||
than ``channels`` channels. Default is ``None``,
|
||||
which means that mode wouldn't be changed.
|
||||
"""
|
||||
name = "Color 3D LUT"
|
||||
|
||||
def __init__(self, size, table, channels=3, target_mode=None):
|
||||
self.size = size = self._check_size(size)
|
||||
self.channels = channels
|
||||
self.mode = target_mode
|
||||
|
||||
table = list(table)
|
||||
# Convert to a flat list
|
||||
if table and isinstance(table[0], (list, tuple)):
|
||||
table, raw_table = [], table
|
||||
for pixel in raw_table:
|
||||
if len(pixel) != channels:
|
||||
raise ValueError("The elements of the table should have "
|
||||
"a length of {}.".format(channels))
|
||||
for color in pixel:
|
||||
table.append(color)
|
||||
|
||||
if len(table) != channels * size[0] * size[1] * size[2]:
|
||||
raise ValueError(
|
||||
"The table should have either channels * size**3 float items "
|
||||
"or size**3 items of channels-sized tuples with floats. "
|
||||
"Table size: {}x{}x{}. Table length: {}".format(
|
||||
size[0], size[1], size[2], len(table)))
|
||||
self.table = table
|
||||
|
||||
@staticmethod
|
||||
def _check_size(size):
|
||||
try:
|
||||
_, _, _ = size
|
||||
except ValueError:
|
||||
raise ValueError("Size should be either an integer or "
|
||||
"a tuple of three integers.")
|
||||
except TypeError:
|
||||
size = (size, size, size)
|
||||
size = [int(x) for x in size]
|
||||
for size1D in size:
|
||||
if not 2 <= size1D <= 65:
|
||||
raise ValueError("Size should be in [2, 65] range.")
|
||||
return size
|
||||
|
||||
@classmethod
|
||||
def generate(cls, size, callback, channels=3, target_mode=None):
|
||||
"""Generates new LUT using provided callback.
|
||||
|
||||
:param size: Size of the table. Passed to the constructor.
|
||||
:param callback: Function with three parameters which correspond
|
||||
three color channels. Will be called ``size**3``
|
||||
times with values from 0.0 to 1.0 and should return
|
||||
a tuple with ``channels`` elements.
|
||||
:param channels: Passed to the constructor.
|
||||
:param target_mode: Passed to the constructor.
|
||||
"""
|
||||
size1D, size2D, size3D = cls._check_size(size)
|
||||
table = []
|
||||
for b in range(size3D):
|
||||
for g in range(size2D):
|
||||
for r in range(size1D):
|
||||
table.append(callback(
|
||||
r / float(size1D-1),
|
||||
g / float(size2D-1),
|
||||
b / float(size3D-1)))
|
||||
|
||||
return cls((size1D, size2D, size3D), table, channels, target_mode)
|
||||
|
||||
def filter(self, image):
|
||||
from . import Image
|
||||
|
||||
return image.color_lut_3d(
|
||||
self.mode or image.mode, Image.LINEAR, self.channels,
|
||||
self.size[0], self.size[1], self.size[2], self.table)
|
||||
|
|
117
src/_imaging.c
117
src/_imaging.c
|
@ -696,9 +696,123 @@ _blend(ImagingObject* self, PyObject* args)
|
|||
}
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/* METHODS */
|
||||
/* METHODS */
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
||||
|
||||
static INT16*
|
||||
_prepare_lut_table(PyObject* table, Py_ssize_t table_size)
|
||||
{
|
||||
int i;
|
||||
FLOAT32* table_data;
|
||||
INT16* prepared;
|
||||
|
||||
/* NOTE: This value should be the same as in ColorLUT.c */
|
||||
#define PRECISION_BITS (16 - 8 - 2)
|
||||
|
||||
table_data = (FLOAT32*) getlist(table, &table_size,
|
||||
"The table should have table_channels * "
|
||||
"size1D * size2D * size3D float items.", TYPE_FLOAT32);
|
||||
if ( ! table_data) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* malloc check ok, max is 2 * 4 * 65**3 = 2197000 */
|
||||
prepared = (INT16*) malloc(sizeof(INT16) * table_size);
|
||||
if ( ! prepared) {
|
||||
free(table_data);
|
||||
return (INT16*) PyErr_NoMemory();
|
||||
}
|
||||
|
||||
for (i = 0; i < table_size; i++) {
|
||||
/* Max value for INT16 */
|
||||
if (table_data[i] >= (0x7fff - 0.5) / (255 << PRECISION_BITS)) {
|
||||
prepared[i] = 0x7fff;
|
||||
continue;
|
||||
}
|
||||
/* Min value for INT16 */
|
||||
if (table_data[i] <= (-0x8000 + 0.5) / (255 << PRECISION_BITS)) {
|
||||
prepared[i] = -0x8000;
|
||||
continue;
|
||||
}
|
||||
if (table_data[i] < 0) {
|
||||
prepared[i] = table_data[i] * (255 << PRECISION_BITS) - 0.5;
|
||||
} else {
|
||||
prepared[i] = table_data[i] * (255 << PRECISION_BITS) + 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
#undef PRECISION_BITS
|
||||
free(table_data);
|
||||
return prepared;
|
||||
}
|
||||
|
||||
|
||||
static PyObject*
|
||||
_color_lut_3d(ImagingObject* self, PyObject* args)
|
||||
{
|
||||
char* mode;
|
||||
int filter;
|
||||
int table_channels;
|
||||
int size1D, size2D, size3D;
|
||||
PyObject* table;
|
||||
|
||||
INT16* prepared_table;
|
||||
Imaging imOut;
|
||||
|
||||
if ( ! PyArg_ParseTuple(args, "siiiiiO:color_lut_3d", &mode, &filter,
|
||||
&table_channels, &size1D, &size2D, &size3D,
|
||||
&table)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* actually, it is trilinear */
|
||||
if (filter != IMAGING_TRANSFORM_BILINEAR) {
|
||||
PyErr_SetString(PyExc_ValueError,
|
||||
"Only LINEAR filter is supported.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (1 > table_channels || table_channels > 4) {
|
||||
PyErr_SetString(PyExc_ValueError,
|
||||
"table_channels should be from 1 to 4");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (2 > size1D || size1D > 65 ||
|
||||
2 > size2D || size2D > 65 ||
|
||||
2 > size3D || size3D > 65
|
||||
) {
|
||||
PyErr_SetString(PyExc_ValueError,
|
||||
"Table size in any dimension should be from 2 to 65");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
prepared_table = _prepare_lut_table(
|
||||
table, table_channels * size1D * size2D * size3D);
|
||||
if ( ! prepared_table) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
imOut = ImagingNewDirty(mode, self->image->xsize, self->image->ysize);
|
||||
if ( ! imOut) {
|
||||
free(prepared_table);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if ( ! ImagingColorLUT3D_linear(imOut, self->image,
|
||||
table_channels, size1D, size2D, size3D,
|
||||
prepared_table)) {
|
||||
free(prepared_table);
|
||||
ImagingDelete(imOut);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
free(prepared_table);
|
||||
|
||||
return PyImagingNew(imOut);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
_convert(ImagingObject* self, PyObject* args)
|
||||
{
|
||||
|
@ -2982,6 +3096,7 @@ static struct PyMethodDef methods[] = {
|
|||
{"pixel_access", (PyCFunction)pixel_access_new, 1},
|
||||
|
||||
/* Standard processing methods (Image) */
|
||||
{"color_lut_3d", (PyCFunction)_color_lut_3d, 1},
|
||||
{"convert", (PyCFunction)_convert, 1},
|
||||
{"convert2", (PyCFunction)_convert2, 1},
|
||||
{"convert_matrix", (PyCFunction)_convert_matrix, 1},
|
||||
|
|
164
src/libImaging/ColorLUT.c
Normal file
164
src/libImaging/ColorLUT.c
Normal file
|
@ -0,0 +1,164 @@
|
|||
#include "Imaging.h"
|
||||
#include <math.h>
|
||||
|
||||
|
||||
/* 8 bits for result. Table can overflow [0, 1.0] range,
|
||||
so we need extra bits for overflow and negative values.
|
||||
NOTE: This value should be the same as in _imaging/_prepare_lut_table() */
|
||||
#define PRECISION_BITS (16 - 8 - 2)
|
||||
#define PRECISION_ROUNDING (1<<(PRECISION_BITS-1))
|
||||
|
||||
/* 8 — scales are multiplied on byte.
|
||||
6 — max index in the table
|
||||
(max size is 65, but index 64 is not reachable) */
|
||||
#define SCALE_BITS (32 - 8 - 6)
|
||||
#define SCALE_MASK ((1<<SCALE_BITS) - 1)
|
||||
|
||||
#define SHIFT_BITS (16 - 1)
|
||||
|
||||
|
||||
static inline UINT8 clip8(int in)
|
||||
{
|
||||
return clip8_lookups[(in + PRECISION_ROUNDING) >> PRECISION_BITS];
|
||||
}
|
||||
|
||||
static inline void
|
||||
interpolate3(INT16 out[3], const INT16 a[3], const INT16 b[3], INT16 shift)
|
||||
{
|
||||
out[0] = (a[0] * ((1<<SHIFT_BITS)-shift) + b[0] * shift) >> SHIFT_BITS;
|
||||
out[1] = (a[1] * ((1<<SHIFT_BITS)-shift) + b[1] * shift) >> SHIFT_BITS;
|
||||
out[2] = (a[2] * ((1<<SHIFT_BITS)-shift) + b[2] * shift) >> SHIFT_BITS;
|
||||
}
|
||||
|
||||
static inline void
|
||||
interpolate4(INT16 out[4], const INT16 a[4], const INT16 b[4], INT16 shift)
|
||||
{
|
||||
out[0] = (a[0] * ((1<<SHIFT_BITS)-shift) + b[0] * shift) >> SHIFT_BITS;
|
||||
out[1] = (a[1] * ((1<<SHIFT_BITS)-shift) + b[1] * shift) >> SHIFT_BITS;
|
||||
out[2] = (a[2] * ((1<<SHIFT_BITS)-shift) + b[2] * shift) >> SHIFT_BITS;
|
||||
out[3] = (a[3] * ((1<<SHIFT_BITS)-shift) + b[3] * shift) >> SHIFT_BITS;
|
||||
}
|
||||
|
||||
static inline int
|
||||
table_index3D(int index1D, int index2D, int index3D,
|
||||
int size1D, int size1D_2D)
|
||||
{
|
||||
return index1D + index2D * size1D + index3D * size1D_2D;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Transforms colors of imIn using provided 3D lookup table
|
||||
and puts the result in imOut. Returns imOut on sucess or 0 on error.
|
||||
|
||||
imOut, imIn — images, should be the same size and may be the same image.
|
||||
Should have 3 or 4 channels.
|
||||
table_channels — number of channels in the lookup table, 3 or 4.
|
||||
Should be less or equal than number of channels in imOut image;
|
||||
size1D, size_2D and size3D — dimensions of provided table;
|
||||
table — flat table,
|
||||
array with table_channels × size1D × size2D × size3D elements,
|
||||
where channels are changed first, then 1D, then 2D, then 3D.
|
||||
Each element is signed 16-bit int where 0 is lowest output value
|
||||
and 255 << PRECISION_BITS (16320) is highest value.
|
||||
*/
|
||||
Imaging
|
||||
ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels,
|
||||
int size1D, int size2D, int size3D,
|
||||
INT16* table)
|
||||
{
|
||||
/* This float to int conversion doesn't have rounding
|
||||
error compensation (+0.5) for two reasons:
|
||||
1. As we don't hit the highest value,
|
||||
we can use one extra bit for precision.
|
||||
2. For every pixel, we interpolate 8 elements from the table:
|
||||
current and +1 for every dimension and their combinations.
|
||||
If we hit the upper cells from the table,
|
||||
+1 cells will be outside of the table.
|
||||
With this compensation we never hit the upper cells
|
||||
but this also doesn't introduce any noticeable difference. */
|
||||
UINT32 scale1D = (size1D - 1) / 255.0 * (1<<SCALE_BITS);
|
||||
UINT32 scale2D = (size2D - 1) / 255.0 * (1<<SCALE_BITS);
|
||||
UINT32 scale3D = (size3D - 1) / 255.0 * (1<<SCALE_BITS);
|
||||
int size1D_2D = size1D * size2D;
|
||||
int x, y;
|
||||
ImagingSectionCookie cookie;
|
||||
|
||||
if (table_channels < 3 || table_channels > 4) {
|
||||
PyErr_SetString(PyExc_ValueError, "table_channels could be 3 or 4");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (imIn->type != IMAGING_TYPE_UINT8 ||
|
||||
imOut->type != IMAGING_TYPE_UINT8 ||
|
||||
imIn->bands < 3 ||
|
||||
imOut->bands < table_channels
|
||||
) {
|
||||
return (Imaging) ImagingError_ModeError();
|
||||
}
|
||||
|
||||
/* In case we have one extra band in imOut and don't have in imIn.*/
|
||||
if (imOut->bands > table_channels && imOut->bands > imIn->bands) {
|
||||
return (Imaging) ImagingError_ModeError();
|
||||
}
|
||||
|
||||
ImagingSectionEnter(&cookie);
|
||||
for (y = 0; y < imOut->ysize; y++) {
|
||||
UINT8* rowIn = (UINT8 *)imIn->image[y];
|
||||
UINT32* rowOut = (UINT32 *)imOut->image[y];
|
||||
for (x = 0; x < imOut->xsize; x++) {
|
||||
UINT32 index1D = rowIn[x*4 + 0] * scale1D;
|
||||
UINT32 index2D = rowIn[x*4 + 1] * scale2D;
|
||||
UINT32 index3D = rowIn[x*4 + 2] * scale3D;
|
||||
INT16 shift1D = (SCALE_MASK & index1D) >> (SCALE_BITS - SHIFT_BITS);
|
||||
INT16 shift2D = (SCALE_MASK & index2D) >> (SCALE_BITS - SHIFT_BITS);
|
||||
INT16 shift3D = (SCALE_MASK & index3D) >> (SCALE_BITS - SHIFT_BITS);
|
||||
int idx = table_channels * table_index3D(
|
||||
index1D >> SCALE_BITS, index2D >> SCALE_BITS,
|
||||
index3D >> SCALE_BITS, size1D, size1D_2D);
|
||||
INT16 result[4], left[4], right[4];
|
||||
INT16 leftleft[4], leftright[4], rightleft[4], rightright[4];
|
||||
|
||||
if (table_channels == 3) {
|
||||
interpolate3(leftleft, &table[idx + 0], &table[idx + 3], shift1D);
|
||||
interpolate3(leftright, &table[idx + size1D*3],
|
||||
&table[idx + size1D*3 + 3], shift1D);
|
||||
interpolate3(left, leftleft, leftright, shift2D);
|
||||
|
||||
interpolate3(rightleft, &table[idx + size1D_2D*3],
|
||||
&table[idx + size1D_2D*3 + 3], shift1D);
|
||||
interpolate3(rightright, &table[idx + size1D_2D*3 + size1D*3],
|
||||
&table[idx + size1D_2D*3 + size1D*3 + 3], shift1D);
|
||||
interpolate3(right, rightleft, rightright, shift2D);
|
||||
|
||||
interpolate3(result, left, right, shift3D);
|
||||
|
||||
rowOut[x] = MAKE_UINT32(
|
||||
clip8(result[0]), clip8(result[1]),
|
||||
clip8(result[2]), rowIn[x*4 + 3]);
|
||||
}
|
||||
|
||||
if (table_channels == 4) {
|
||||
interpolate4(leftleft, &table[idx + 0], &table[idx + 4], shift1D);
|
||||
interpolate4(leftright, &table[idx + size1D*4],
|
||||
&table[idx + size1D*4 + 4], shift1D);
|
||||
interpolate4(left, leftleft, leftright, shift2D);
|
||||
|
||||
interpolate4(rightleft, &table[idx + size1D_2D*4],
|
||||
&table[idx + size1D_2D*4 + 4], shift1D);
|
||||
interpolate4(rightright, &table[idx + size1D_2D*4 + size1D*4],
|
||||
&table[idx + size1D_2D*4 + size1D*4 + 4], shift1D);
|
||||
interpolate4(right, rightleft, rightright, shift2D);
|
||||
|
||||
interpolate4(result, left, right, shift3D);
|
||||
|
||||
rowOut[x] = MAKE_UINT32(
|
||||
clip8(result[0]), clip8(result[1]),
|
||||
clip8(result[2]), clip8(result[3]));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImagingSectionLeave(&cookie);
|
||||
|
||||
return imOut;
|
||||
}
|
|
@ -319,6 +319,8 @@ extern Imaging ImagingTransform(
|
|||
extern Imaging ImagingUnsharpMask(
|
||||
Imaging imOut, Imaging im, float radius, int percent, int threshold);
|
||||
extern Imaging ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n);
|
||||
extern Imaging ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn,
|
||||
int table_channels, int size1D, int size2D, int size3D, INT16* table);
|
||||
|
||||
extern Imaging ImagingCopy2(Imaging imOut, Imaging imIn);
|
||||
extern Imaging ImagingConvert2(Imaging imOut, Imaging imIn);
|
||||
|
@ -534,6 +536,8 @@ extern Py_ssize_t _imaging_tell_pyFd(PyObject *fd);
|
|||
|
||||
|
||||
#include "ImagingUtils.h"
|
||||
extern UINT8 *clip8_lookups;
|
||||
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
|
|
|
@ -83,7 +83,40 @@ static struct filter LANCZOS = { lanczos_filter, 3.0 };
|
|||
#define PRECISION_BITS (32 - 8 - 2)
|
||||
|
||||
|
||||
UINT8 _lookups[512] = {
|
||||
/* Handles values form -640 to 639. */
|
||||
UINT8 _clip8_lookups[1280] = {
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
|
@ -115,15 +148,30 @@ UINT8 _lookups[512] = {
|
|||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
};
|
||||
|
||||
UINT8 *lookups = &_lookups[128];
|
||||
|
||||
UINT8 *clip8_lookups = &_clip8_lookups[640];
|
||||
|
||||
static inline UINT8 clip8(int in)
|
||||
{
|
||||
return lookups[in >> PRECISION_BITS];
|
||||
return clip8_lookups[in >> PRECISION_BITS];
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user