diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py new file mode 100644 index 000000000..f9d35c83c --- /dev/null +++ b/Tests/test_color_lut.py @@ -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() diff --git a/setup.py b/setup.py index b51de41d0..f4dbbb62d 100755 --- a/setup.py +++ b/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", diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 735a00831..93cd7ad47 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -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) diff --git a/src/_imaging.c b/src/_imaging.c index 11f5f6ea4..544e54d87 100644 --- a/src/_imaging.c +++ b/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}, diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c new file mode 100644 index 000000000..9988ad1b1 --- /dev/null +++ b/src/libImaging/ColorLUT.c @@ -0,0 +1,164 @@ +#include "Imaging.h" +#include + + +/* 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<> 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; + out[1] = (a[1] * ((1<> SHIFT_BITS; + out[2] = (a[2] * ((1<> 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; + out[1] = (a[1] * ((1<> SHIFT_BITS; + out[2] = (a[2] * ((1<> SHIFT_BITS; + out[3] = (a[3] * ((1<> 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< 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; +} diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index aa59fe18c..8c3ad65c3 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -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) } diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 7cefdb2af..6fae2081d 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -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]; }