From 853208c65f98de9d20b9bdc3d910f3664af0278f Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 25 Mar 2018 15:49:42 +0300 Subject: [PATCH 01/26] color 3D LUT, just start --- setup.py | 2 +- src/libImaging/ColorLUT.c | 106 ++++++++++++++++++++++++++++++++++++++ src/libImaging/Imaging.h | 2 + 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/libImaging/ColorLUT.c diff --git a/setup.py b/setup.py index 4a0ad86c5..e75a19bdf 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/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c new file mode 100644 index 000000000..d7aa56121 --- /dev/null +++ b/src/libImaging/ColorLUT.c @@ -0,0 +1,106 @@ +#include "Imaging.h" +#include + + +#define ROUND_UP(f) ((int) ((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) + + +/* 8 bits for result. Table can overflow [0, 1.0] range, + so we need extra bits for overflow and negative values. */ +#define PRECISION_BITS (16 - 8 - 2) + + +static inline void +interpolate3(INT16 out[3], const INT16 a[3], const INT16 b[3], float shift) +{ + out[0] = a[0] * (1-shift) + b[0] * shift; + out[1] = a[1] * (1-shift) + b[1] * shift; + out[2] = a[2] * (1-shift) + b[2] * shift; +} + +static inline void +interpolate4(INT16 out[3], const INT16 a[3], const INT16 b[3], float shift) +{ + out[0] = a[0] * (1-shift) + b[0] * shift; + out[1] = a[1] * (1-shift) + b[1] * shift; + out[2] = a[2] * (1-shift) + b[2] * shift; + out[3] = a[3] * (1-shift) + b[3] * shift; +} + +static inline int +table3D_index3(int index1D, int index2D, int index3D, + int size1D, int size1D_2D) +{ + return (index1D + index2D * size1D + index3D * size1D_2D) * 3; +} + +static inline int +table3D_index4(int index1D, int index2D, int index3D, + int size1D, int size1D_2D) +{ + return (index1D + index2D * size1D + index3D * size1D_2D) * 4; +} + +/* + Transforms colors of imIn using provided 3D look-up 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 look-up 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 — flatten 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) +{ + int size1D_2D = size1D * size2D; + float scale1D = (size1D - 1) / 255.0; + float scale2D = (size2D - 1) / 255.0; + float scale3D = (size3D - 1) / 255.0; + int x, y; + + 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(); + } + + for (y = 0; y < imOut->ysize; y++) { + UINT8 *rowIn = (UINT8 *)imIn->image[y]; + UINT8 *rowOut = (UINT8 *)imIn->image[y]; + for (x = 0; x < imOut->xsize; x++) { + float scaled1D = rowIn[x*4 + 0] * scale1D; + float scaled2D = rowIn[x*4 + 1] * scale2D; + float scaled3D = rowIn[x*4 + 2] * scale3D; + int index1D = (int) scaled1D; + int index2D = (int) scaled2D; + int index3D = (int) scaled3D; + float shift1D = scaled1D - index1D; + float shift2D = scaled2D - index2D; + float shift3D = scaled3D - index3D; + + } + } + + return imOut; +} diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index aa59fe18c..ece082a18 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); From d2d546d4ae4d700b3ccf9a247d4adf2c9678b60a Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Mar 2018 11:26:51 +0300 Subject: [PATCH 02/26] Python to C bridge --- src/_imaging.c | 122 +++++++++++++++++++++++++++++++++++++- src/libImaging/ColorLUT.c | 30 ++++++---- 2 files changed, 139 insertions(+), 13 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 11f5f6ea4..d1262f5b8 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) { @@ -720,7 +834,10 @@ _convert(ImagingObject* self, PyObject* args) } } - return PyImagingNew(ImagingConvert(self->image, mode, paletteimage ? paletteimage->image->palette : NULL, dither)); + return PyImagingNew(ImagingConvert( + self->image, mode, + paletteimage ? paletteimage->image->palette : NULL, + dither)); } static PyObject* @@ -2982,6 +3099,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 index d7aa56121..6288625a1 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -6,25 +6,26 @@ /* 8 bits for result. Table can overflow [0, 1.0] range, - so we need extra bits for overflow and negative values. */ + 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) static inline void interpolate3(INT16 out[3], const INT16 a[3], const INT16 b[3], float shift) { - out[0] = a[0] * (1-shift) + b[0] * shift; - out[1] = a[1] * (1-shift) + b[1] * shift; - out[2] = a[2] * (1-shift) + b[2] * shift; + out[0] = a[0] * (1-shift) + b[0] * shift + 0.5; + out[1] = a[1] * (1-shift) + b[1] * shift + 0.5; + out[2] = a[2] * (1-shift) + b[2] * shift + 0.5; } static inline void interpolate4(INT16 out[3], const INT16 a[3], const INT16 b[3], float shift) { - out[0] = a[0] * (1-shift) + b[0] * shift; - out[1] = a[1] * (1-shift) + b[1] * shift; - out[2] = a[2] * (1-shift) + b[2] * shift; - out[3] = a[3] * (1-shift) + b[3] * shift; + out[0] = a[0] * (1-shift) + b[0] * shift + 0.5; + out[1] = a[1] * (1-shift) + b[1] * shift + 0.5; + out[2] = a[2] * (1-shift) + b[2] * shift + 0.5; + out[3] = a[3] * (1-shift) + b[3] * shift + 0.5; } static inline int @@ -61,10 +62,17 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, int size1D, int size2D, int size3D, INT16* table) { + /* The fractions are a way to avoid overflow. + For every pixel, we interpolate 8 elements from the table: + current and +1 for every dimension and they 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 noticable difference. */ + float scale1D = (size1D - 1) / (255.0002); + float scale2D = (size2D - 1) / (255.0002); + float scale3D = (size3D - 1) / (255.0002); int size1D_2D = size1D * size2D; - float scale1D = (size1D - 1) / 255.0; - float scale2D = (size2D - 1) / 255.0; - float scale3D = (size3D - 1) / 255.0; int x, y; if (table_channels < 3 || table_channels > 4) { From 696ae12b3776dde4b272800b999fca4f7ff77ee5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Mar 2018 14:14:52 +0300 Subject: [PATCH 03/26] 3D to 3D implementation --- src/_imaging.c | 1 + src/libImaging/ColorLUT.c | 150 +++++++++++++++++++++++++++++++------- 2 files changed, 126 insertions(+), 25 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index d1262f5b8..f023a6328 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -740,6 +740,7 @@ _prepare_lut_table(PyObject* table, Py_ssize_t table_size) } else { prepared[i] = table_data[i] * (255 << PRECISION_BITS) + 0.5; } + // printf("%f, %d ", table_data[i], prepared[i]); } #undef PRECISION_BITS diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index 6288625a1..dab9eb627 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -2,30 +2,104 @@ #include -#define ROUND_UP(f) ((int) ((f) >= 0.0 ? (f) + 0.5F : (f) - 0.5F)) - - /* 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)) + + +UINT8 _lookups2[1024] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, + 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, + 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, + 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, + 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, + 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, + 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, + 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 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 *lookups2 = &_lookups2[512]; + + +static inline UINT8 clip8(int in) +{ + return lookups2[(in + PRECISION_ROUNDING) >> PRECISION_BITS]; +} static inline void -interpolate3(INT16 out[3], const INT16 a[3], const INT16 b[3], float shift) +interpolate3(INT16 out[3], const INT16 a[3], const INT16 b[3], INT16 shift) { - out[0] = a[0] * (1-shift) + b[0] * shift + 0.5; - out[1] = a[1] * (1-shift) + b[1] * shift + 0.5; - out[2] = a[2] * (1-shift) + b[2] * shift + 0.5; + out[0] = (a[0] * (0x1000-shift) + b[0] * shift + (1<<11)) >> 12; + out[1] = (a[1] * (0x1000-shift) + b[1] * shift + (1<<11)) >> 12; + out[2] = (a[2] * (0x1000-shift) + b[2] * shift + (1<<11)) >> 12; } static inline void -interpolate4(INT16 out[3], const INT16 a[3], const INT16 b[3], float shift) +interpolate4(INT16 out[4], const INT16 a[4], const INT16 b[4], INT16 shift) { - out[0] = a[0] * (1-shift) + b[0] * shift + 0.5; - out[1] = a[1] * (1-shift) + b[1] * shift + 0.5; - out[2] = a[2] * (1-shift) + b[2] * shift + 0.5; - out[3] = a[3] * (1-shift) + b[3] * shift + 0.5; + out[0] = (a[0] * (0x1000-shift) + b[0] * shift + (1<<11)) >> 12; + out[1] = (a[1] * (0x1000-shift) + b[1] * shift + (1<<11)) >> 12; + out[2] = (a[2] * (0x1000-shift) + b[2] * shift + (1<<11)) >> 12; + out[3] = (a[3] * (0x1000-shift) + b[3] * shift + (1<<11)) >> 12; } static inline int @@ -94,19 +168,45 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, } for (y = 0; y < imOut->ysize; y++) { - UINT8 *rowIn = (UINT8 *)imIn->image[y]; - UINT8 *rowOut = (UINT8 *)imIn->image[y]; - for (x = 0; x < imOut->xsize; x++) { - float scaled1D = rowIn[x*4 + 0] * scale1D; - float scaled2D = rowIn[x*4 + 1] * scale2D; - float scaled3D = rowIn[x*4 + 2] * scale3D; - int index1D = (int) scaled1D; - int index2D = (int) scaled2D; - int index3D = (int) scaled3D; - float shift1D = scaled1D - index1D; - float shift2D = scaled2D - index2D; - float shift3D = scaled3D - index3D; - + UINT8* rowIn = (UINT8 *)imIn->image[y]; + UINT8* rowOut = (UINT8 *)imOut->image[y]; + if (table_channels == 3) { + for (x = 0; x < imOut->xsize; x++) { + float scaled1D = rowIn[x*4 + 0] * scale1D; + float scaled2D = rowIn[x*4 + 1] * scale2D; + float scaled3D = rowIn[x*4 + 2] * scale3D; + int index1D = (int) scaled1D; + int index2D = (int) scaled2D; + int index3D = (int) scaled3D; + INT16 shift1D = (scaled1D - index1D) * 0x1000 + 0.5; + INT16 shift2D = (scaled2D - index2D) * 0x1000 + 0.5; + INT16 shift3D = (scaled3D - index3D) * 0x1000 + 0.5; + int idx = table3D_index3(index1D, index2D, index3D, size1D, size1D_2D); + INT16 result[3], left[3], right[3]; + INT16 leftleft[3], leftright[3], rightleft[3], rightright[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*4 + 0] = clip8(result[0]); + rowOut[x*4 + 1] = clip8(result[1]); + rowOut[x*4 + 2] = clip8(result[2]); + rowOut[x*4 + 3] = rowIn[x*4 + 3]; + + // printf("%d: %f -> %d, %f; idx: %d; (%d, %d, %d)\n", + // rowIn[x*4 + 0], scaled1D, index1D, shift1D, + // idx, nearest[0], nearest[1], nearest[2]); + } } } From 23827d52506a6e1c0e898a53d1c6018e241e0e99 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Mar 2018 14:18:17 +0300 Subject: [PATCH 04/26] 3D to 4D implementation --- src/libImaging/ColorLUT.c | 50 ++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index dab9eb627..a1f562d90 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -170,21 +170,21 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, for (y = 0; y < imOut->ysize; y++) { UINT8* rowIn = (UINT8 *)imIn->image[y]; UINT8* rowOut = (UINT8 *)imOut->image[y]; - if (table_channels == 3) { - for (x = 0; x < imOut->xsize; x++) { - float scaled1D = rowIn[x*4 + 0] * scale1D; - float scaled2D = rowIn[x*4 + 1] * scale2D; - float scaled3D = rowIn[x*4 + 2] * scale3D; - int index1D = (int) scaled1D; - int index2D = (int) scaled2D; - int index3D = (int) scaled3D; - INT16 shift1D = (scaled1D - index1D) * 0x1000 + 0.5; - INT16 shift2D = (scaled2D - index2D) * 0x1000 + 0.5; - INT16 shift3D = (scaled3D - index3D) * 0x1000 + 0.5; - int idx = table3D_index3(index1D, index2D, index3D, size1D, size1D_2D); - INT16 result[3], left[3], right[3]; - INT16 leftleft[3], leftright[3], rightleft[3], rightright[3]; + for (x = 0; x < imOut->xsize; x++) { + float scaled1D = rowIn[x*4 + 0] * scale1D; + float scaled2D = rowIn[x*4 + 1] * scale2D; + float scaled3D = rowIn[x*4 + 2] * scale3D; + int index1D = (int) scaled1D; + int index2D = (int) scaled2D; + int index3D = (int) scaled3D; + INT16 shift1D = (scaled1D - index1D) * 0x1000 + 0.5; + INT16 shift2D = (scaled2D - index2D) * 0x1000 + 0.5; + INT16 shift3D = (scaled3D - index3D) * 0x1000 + 0.5; + int idx = table3D_index3(index1D, index2D, index3D, 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); @@ -202,10 +202,26 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, rowOut[x*4 + 1] = clip8(result[1]); rowOut[x*4 + 2] = clip8(result[2]); rowOut[x*4 + 3] = rowIn[x*4 + 3]; + } - // printf("%d: %f -> %d, %f; idx: %d; (%d, %d, %d)\n", - // rowIn[x*4 + 0], scaled1D, index1D, shift1D, - // idx, nearest[0], nearest[1], nearest[2]); + 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*4 + 0] = clip8(result[0]); + rowOut[x*4 + 1] = clip8(result[1]); + rowOut[x*4 + 2] = clip8(result[2]); + rowOut[x*4 + 3] = clip8(result[3]); } } } From 3a5f0201f53df27bce53793268ff1fc5d8a7cce8 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Mar 2018 17:23:14 +0300 Subject: [PATCH 05/26] pure FPI implementation --- src/libImaging/ColorLUT.c | 65 ++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index a1f562d90..4a5c6bced 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -8,6 +8,14 @@ #define PRECISION_BITS (16 - 8 - 2) #define PRECISION_ROUNDING (1<<(PRECISION_BITS-1)) +/* 8 — scales are multiplyed on byte. + 6 — max index in the table (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) +#define SHIFT_ROUNDING (1<<(SHIFT_BITS-1)) + UINT8 _lookups2[1024] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -88,18 +96,18 @@ static inline UINT8 clip8(int in) static inline void interpolate3(INT16 out[3], const INT16 a[3], const INT16 b[3], INT16 shift) { - out[0] = (a[0] * (0x1000-shift) + b[0] * shift + (1<<11)) >> 12; - out[1] = (a[1] * (0x1000-shift) + b[1] * shift + (1<<11)) >> 12; - out[2] = (a[2] * (0x1000-shift) + b[2] * shift + (1<<11)) >> 12; + 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] * (0x1000-shift) + b[0] * shift + (1<<11)) >> 12; - out[1] = (a[1] * (0x1000-shift) + b[1] * shift + (1<<11)) >> 12; - out[2] = (a[2] * (0x1000-shift) + b[2] * shift + (1<<11)) >> 12; - out[3] = (a[3] * (0x1000-shift) + b[3] * shift + (1<<11)) >> 12; + 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 @@ -143,9 +151,9 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, +1 cells will be outside of the table. With this compensation we never hit the upper cells but this also doesn't introduce any noticable difference. */ - float scale1D = (size1D - 1) / (255.0002); - float scale2D = (size2D - 1) / (255.0002); - float scale3D = (size3D - 1) / (255.0002); + UINT32 scale1D = (size1D - 1) / 255.0 * (1<ysize; y++) { UINT8* rowIn = (UINT8 *)imIn->image[y]; - UINT8* rowOut = (UINT8 *)imOut->image[y]; + UINT32* rowOut = (UINT32 *)imOut->image[y]; for (x = 0; x < imOut->xsize; x++) { - float scaled1D = rowIn[x*4 + 0] * scale1D; - float scaled2D = rowIn[x*4 + 1] * scale2D; - float scaled3D = rowIn[x*4 + 2] * scale3D; - int index1D = (int) scaled1D; - int index2D = (int) scaled2D; - int index3D = (int) scaled3D; - INT16 shift1D = (scaled1D - index1D) * 0x1000 + 0.5; - INT16 shift2D = (scaled2D - index2D) * 0x1000 + 0.5; - INT16 shift3D = (scaled3D - index3D) * 0x1000 + 0.5; - int idx = table3D_index3(index1D, index2D, index3D, size1D, size1D_2D); + 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 = table3D_index3( + 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]; @@ -198,10 +207,9 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, interpolate3(result, left, right, shift3D); - rowOut[x*4 + 0] = clip8(result[0]); - rowOut[x*4 + 1] = clip8(result[1]); - rowOut[x*4 + 2] = clip8(result[2]); - rowOut[x*4 + 3] = rowIn[x*4 + 3]; + rowOut[x] = MAKE_UINT32( + clip8(result[0]), clip8(result[1]), + clip8(result[2]), rowIn[x*4 + 3]); } if (table_channels == 4) { @@ -218,10 +226,9 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, interpolate4(result, left, right, shift3D); - rowOut[x*4 + 0] = clip8(result[0]); - rowOut[x*4 + 1] = clip8(result[1]); - rowOut[x*4 + 2] = clip8(result[2]); - rowOut[x*4 + 3] = clip8(result[3]); + rowOut[x] = MAKE_UINT32( + clip8(result[0]), clip8(result[1]), + clip8(result[2]), clip8(result[3])); } } } From 845f4dbfe152a53f9562bc912869a1b892f95cfb Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Mar 2018 17:34:56 +0300 Subject: [PATCH 06/26] update comment --- src/libImaging/ColorLUT.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index 4a5c6bced..2af348586 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -144,13 +144,16 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, int size1D, int size2D, int size3D, INT16* table) { - /* The fractions are a way to avoid overflow. - For every pixel, we interpolate 8 elements from the table: - current and +1 for every dimension and they 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 noticable difference. */ + /* 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 they 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 noticable difference. */ UINT32 scale1D = (size1D - 1) / 255.0 * (1< Date: Mon, 26 Mar 2018 17:41:25 +0300 Subject: [PATCH 07/26] one function table_index3D --- src/libImaging/ColorLUT.c | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index 2af348586..65fcd9d7c 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -111,18 +111,12 @@ interpolate4(INT16 out[4], const INT16 a[4], const INT16 b[4], INT16 shift) } static inline int -table3D_index3(int index1D, int index2D, int index3D, +table_index3D(int index1D, int index2D, int index3D, int size1D, int size1D_2D) { - return (index1D + index2D * size1D + index3D * size1D_2D) * 3; + return index1D + index2D * size1D + index3D * size1D_2D; } -static inline int -table3D_index4(int index1D, int index2D, int index3D, - int size1D, int size1D_2D) -{ - return (index1D + index2D * size1D + index3D * size1D_2D) * 4; -} /* Transforms colors of imIn using provided 3D look-up table @@ -145,7 +139,7 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, INT16* table) { /* This float to int conversion doesn't have rounding - error compensation (+ 0.5) for two reasons: + 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: @@ -188,11 +182,9 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, 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 = table3D_index3( - index1D >> SCALE_BITS, - index2D >> SCALE_BITS, - index3D >> SCALE_BITS, - size1D, size1D_2D); + int idx = 3 * 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]; From 78d16d30c4ec1b929d63de40990e4720430f1713 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Mar 2018 18:02:08 +0300 Subject: [PATCH 08/26] share clip8_lookups table between Resample and ColorLUT --- src/libImaging/ColorLUT.c | 73 +-------------------------------------- src/libImaging/Imaging.h | 2 ++ src/libImaging/Resample.c | 39 ++++++++++++++++++--- 3 files changed, 38 insertions(+), 76 deletions(-) diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index 65fcd9d7c..38371dbf2 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -17,82 +17,11 @@ #define SHIFT_ROUNDING (1<<(SHIFT_BITS-1)) -UINT8 _lookups2[1024] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, - 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, - 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, - 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, - 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, - 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, - 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, - 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, - 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, - 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, - 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, - 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, - 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, - 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, - 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 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 *lookups2 = &_lookups2[512]; - - static inline UINT8 clip8(int in) { - return lookups2[(in + PRECISION_ROUNDING) >> PRECISION_BITS]; + 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) { diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index ece082a18..8c3ad65c3 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -536,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..29a6cce1d 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -83,7 +83,31 @@ static struct filter LANCZOS = { lanczos_filter, 3.0 }; #define PRECISION_BITS (32 - 8 - 2) -UINT8 _lookups[512] = { +UINT8 _clip8_lookups[1024] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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 +139,22 @@ 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 }; -UINT8 *lookups = &_lookups[128]; - +UINT8 *clip8_lookups = &_clip8_lookups[512]; static inline UINT8 clip8(int in) { - return lookups[in >> PRECISION_BITS]; + return clip8_lookups[in >> PRECISION_BITS]; } From b30b2a280fc8d09f43b8e773ac81c9204e5cf3bb Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Mar 2018 19:30:00 +0300 Subject: [PATCH 09/26] Tests. First part --- Tests/test_color_lut.py | 129 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 Tests/test_color_lut.py diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py new file mode 100644 index 000000000..18b3f630f --- /dev/null +++ b/Tests/test_color_lut.py @@ -0,0 +1,129 @@ +from __future__ import division +from helper import unittest, PillowTestCase + +import time +from PIL import Image + + +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, + b / float(size3D-1) if size3D != 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_arguments(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(6, 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))) + + def test_correct_arguments(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)) + + +if __name__ == '__main__': + unittest.main() From 5f0b7ee73e17c8866a29cfb804cd1cc6f4853b0c Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Mar 2018 22:29:50 +0300 Subject: [PATCH 10/26] More tests --- Tests/test_color_lut.py | 68 +++++++++++++++++++++++++++++++++++++++-- src/_imaging.c | 1 - 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 18b3f630f..3eb2bdcef 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -19,7 +19,6 @@ class TestColorLut3DCoreAPI(PillowTestCase): 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, - b / float(size3D-1) if size3D != 1 else 0, ][:channels] for b in range(size3D) for g in range(size2D) @@ -42,7 +41,7 @@ class TestColorLut3DCoreAPI(PillowTestCase): with self.assertRaisesRegexp(ValueError, "table_channels"): im.im.color_lut_3d('RGB', Image.LINEAR, - *self.generate_unit_table(6, 3)) + *self.generate_unit_table(5, 3)) with self.assertRaisesRegexp(ValueError, "table_channels"): im.im.color_lut_3d('RGB', Image.LINEAR, @@ -60,6 +59,14 @@ class TestColorLut3DCoreAPI(PillowTestCase): 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_arguments(self): im = Image.new('RGB', (10, 10), 0) @@ -124,6 +131,63 @@ class TestColorLut3DCoreAPI(PillowTestCase): 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_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)) + if __name__ == '__main__': unittest.main() diff --git a/src/_imaging.c b/src/_imaging.c index f023a6328..d1262f5b8 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -740,7 +740,6 @@ _prepare_lut_table(PyObject* table, Py_ssize_t table_size) } else { prepared[i] = table_data[i] * (255 << PRECISION_BITS) + 0.5; } - // printf("%f, %d ", table_data[i], prepared[i]); } #undef PRECISION_BITS From 5227c30561174c727458d6ca8bf6a26c3eb2e9da Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 26 Mar 2018 22:33:22 +0300 Subject: [PATCH 11/26] typos --- Tests/test_color_lut.py | 1 - src/libImaging/ColorLUT.c | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 3eb2bdcef..2da59b115 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,7 +1,6 @@ from __future__ import division from helper import unittest, PillowTestCase -import time from PIL import Image diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index 38371dbf2..f9dd91cb8 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -8,7 +8,7 @@ #define PRECISION_BITS (16 - 8 - 2) #define PRECISION_ROUNDING (1<<(PRECISION_BITS-1)) -/* 8 — scales are multiplyed on byte. +/* 8 — scales are multiplied on byte. 6 — max index in the table (size is 65, but index 64 is not reachable) */ #define SCALE_BITS (32 - 8 - 6) #define SCALE_MASK ((1 << SCALE_BITS) - 1) @@ -72,11 +72,11 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, 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 they combinations. + 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 noticable difference. */ + but this also doesn't introduce any noticeable difference. */ UINT32 scale1D = (size1D - 1) / 255.0 * (1< Date: Mon, 26 Mar 2018 23:17:17 +0300 Subject: [PATCH 12/26] more tests and fixed bug for interpolate4 --- Tests/test_color_lut.py | 21 +++++++++++++++++++++ src/libImaging/ColorLUT.c | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 2da59b115..65b5c5127 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -146,6 +146,27 @@ class TestColorLut3DCoreAPI(PillowTestCase): 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), diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index f9dd91cb8..3f930c3ee 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -111,7 +111,7 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, 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 = 3 * table_index3D( + 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]; From 71f643e1ead202596c186ea46e2925a1f8f1615c Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 27 Mar 2018 03:45:00 +0300 Subject: [PATCH 13/26] doesn't affect accuracy, but a bit faster --- src/_imaging.c | 5 +---- src/libImaging/ColorLUT.c | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index d1262f5b8..544e54d87 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -834,10 +834,7 @@ _convert(ImagingObject* self, PyObject* args) } } - return PyImagingNew(ImagingConvert( - self->image, mode, - paletteimage ? paletteimage->image->palette : NULL, - dither)); + return PyImagingNew(ImagingConvert(self->image, mode, paletteimage ? paletteimage->image->palette : NULL, dither)); } static PyObject* diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index 3f930c3ee..efcc49ca6 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -9,12 +9,12 @@ #define PRECISION_ROUNDING (1<<(PRECISION_BITS-1)) /* 8 — scales are multiplied on byte. - 6 — max index in the table (size is 65, but index 64 is not reachable) */ + 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 SCALE_MASK ((1<> SHIFT_BITS; - out[1] = (a[1] * ((1<> SHIFT_BITS; - out[2] = (a[2] * ((1<> SHIFT_BITS; + 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; + 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 From 461a0904058c49c45f820b9c6a1f92bdea147f47 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 29 Mar 2018 14:39:28 +0300 Subject: [PATCH 14/26] Python interface --- src/PIL/ImageFilter.py | 59 ++++++++++++++++++++++++++++++++++++++- src/libImaging/ColorLUT.c | 6 ++-- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 735a00831..eba5453d4 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -130,7 +130,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 +296,61 @@ 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. + """ + def __init__(self, size, table, channels=3, target_mode=None): + try: + _, _, _ = size + except ValueError: + raise ValueError("Size should be an integer either " + "tuple of three integers.") + except TypeError: + size = (size, size, size) + self.size = size + self.channels = channels + self.mode = target_mode + + table = list(table) + # Convert to a flat list + if isinstance(table[0], (list, tuple)): + table = [ + pixel + for pixel in table + if len(pixel) == channels + for color in pixel + ] + + if len(table) != channels * size[0] * size[1] * size[2]: + raise ValueError( + "The table should have channels * size**3 float items " + "either size**3 items of channels-sized tuples with floats.") + self.table = table + + 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/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index efcc49ca6..36b87f108 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -48,15 +48,15 @@ table_index3D(int index1D, int index2D, int index3D, /* - Transforms colors of imIn using provided 3D look-up table + 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 look-up table, 3 or 4. + 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 — flatten 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 From 506995d8161b7efc0aac02c09bc313869b6d5371 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 29 Mar 2018 16:37:35 +0300 Subject: [PATCH 15/26] Tests for python API --- Tests/test_color_lut.py | 47 ++++++++++++++++++++++++++++++++++++++--- src/PIL/ImageFilter.py | 26 ++++++++++++++++------- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 65b5c5127..5d15c4cb2 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,7 +1,7 @@ from __future__ import division from helper import unittest, PillowTestCase -from PIL import Image +from PIL import Image, ImageFilter class TestColorLut3DCoreAPI(PillowTestCase): @@ -27,7 +27,7 @@ class TestColorLut3DCoreAPI(PillowTestCase): channels, size1D, size2D, size3D, [item for sublist in table for item in sublist]) - def test_wrong_arguments(self): + def test_wrong_args(self): im = Image.new('RGB', (10, 10), 0) with self.assertRaisesRegexp(ValueError, "filter"): @@ -66,7 +66,7 @@ class TestColorLut3DCoreAPI(PillowTestCase): im.im.color_lut_3d('RGB', Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 9) - def test_correct_arguments(self): + def test_correct_args(self): im = Image.new('RGB', (10, 10), 0) im.im.color_lut_3d('RGB', Image.LINEAR, @@ -209,5 +209,46 @@ class TestColorLut3DCoreAPI(PillowTestCase): self.assertEqual(transformed[205, 205], (255, 255, 0)) +class TestColorLut3DFilter(PillowTestCase): + def test_wrong_args(self): + with self.assertRaisesRegexp(ValueError, "should be an integer"): + ImageFilter.Color3DLUT("small", [1]) + + with self.assertRaisesRegexp(ValueError, "should be 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): + flt = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) + self.assertEqual(tuple(flt.size), (2, 2, 2)) + self.assertEqual(flt.name, "Color 3D LUT") + + flt = 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(flt.size), (2, 2, 2)) + self.assertEqual(flt.table, list(range(24))) + + flt = ImageFilter.Color3DLUT((2, 2, 2), [(0, 1, 2, 3)] * 8, + channels=4) + + if __name__ == '__main__': unittest.main() diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index eba5453d4..1c9561576 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: @@ -320,6 +321,8 @@ class Color3DLUT(MultibandFilter): 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): try: _, _, _ = size @@ -328,24 +331,31 @@ class Color3DLUT(MultibandFilter): "tuple of three integers.") except TypeError: size = (size, size, size) + size = map(int, size) + for size1D in size: + if not 2 <= size1D <= 65: + raise ValueError("Size should be in [2, 65] range.") + self.size = size self.channels = channels self.mode = target_mode table = list(table) # Convert to a flat list - if isinstance(table[0], (list, tuple)): - table = [ - pixel - for pixel in table - if len(pixel) == channels - for color in pixel - ] + 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 channels * size**3 float items " - "either size**3 items of channels-sized tuples with floats.") + "either size**3 items of channels-sized tuples with floats. " + "Table length: {}".format(len(table))) self.table = table def filter(self, image): From 622749530bfbd3ffb081fbe75965d74a7c87ffa1 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 29 Mar 2018 17:26:21 +0300 Subject: [PATCH 16/26] Color3DLUT.generate --- Tests/test_color_lut.py | 23 ++++++++++++++++++ src/PIL/ImageFilter.py | 53 +++++++++++++++++++++++++++++++---------- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 5d15c4cb2..30b8de279 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -249,6 +249,29 @@ class TestColorLut3DFilter(PillowTestCase): flt = ImageFilter.Color3DLUT((2, 2, 2), [(0, 1, 2, 3)] * 8, channels=4) + def test_generate(self): + flt = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) + self.assertEqual(tuple(flt.size), (5, 5, 5)) + self.assertEqual(flt.name, "Color 3D LUT") + self.assertEqual(flt.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]) + + flt = ImageFilter.Color3DLUT.generate(5, channels=4, + callback=lambda r, g, b: (b, r, g, (r+g+b) / 2)) + self.assertEqual(tuple(flt.size), (5, 5, 5)) + self.assertEqual(flt.name, "Color 3D LUT") + self.assertEqual(flt.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/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 1c9561576..e616b4641 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -324,19 +324,7 @@ class Color3DLUT(MultibandFilter): name = "Color 3D LUT" def __init__(self, size, table, channels=3, target_mode=None): - try: - _, _, _ = size - except ValueError: - raise ValueError("Size should be an integer either " - "tuple of three integers.") - except TypeError: - size = (size, size, size) - size = map(int, size) - for size1D in size: - if not 2 <= size1D <= 65: - raise ValueError("Size should be in [2, 65] range.") - - self.size = size + self.size = size = self._check_size(size) self.channels = channels self.mode = target_mode @@ -358,6 +346,45 @@ class Color3DLUT(MultibandFilter): "Table length: {}".format(len(table))) self.table = table + @staticmethod + def _check_size(size): + try: + _, _, _ = size + except ValueError: + raise ValueError("Size should be an integer either " + "tuple of three integers.") + except TypeError: + size = (size, size, size) + size = map(int, 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 From 7f0bbf52e3d506d823739c1b16c5a6bfc4c30255 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 29 Mar 2018 17:37:00 +0300 Subject: [PATCH 17/26] Python3 fix --- src/PIL/ImageFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index e616b4641..e16b47bdd 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -355,7 +355,7 @@ class Color3DLUT(MultibandFilter): "tuple of three integers.") except TypeError: size = (size, size, size) - size = map(int, 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.") From d2a5d1e44de391365046ca99b0331bc92e4de25d Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 29 Mar 2018 23:56:51 +0300 Subject: [PATCH 18/26] Add tests for some cases and fix bugs --- Tests/test_color_lut.py | 22 ++++++++++++++++++++++ src/libImaging/Resample.c | 23 ++++++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 30b8de279..3bfb05f02 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -198,7 +198,24 @@ class TestColorLut3DCoreAPI(PillowTestCase): -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)) @@ -257,6 +274,11 @@ class TestColorLut3DFilter(PillowTestCase): 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(flt)) + flt = ImageFilter.Color3DLUT.generate(5, channels=4, callback=lambda r, g, b: (b, r, g, (r+g+b) / 2)) self.assertEqual(tuple(flt.size), (5, 5, 5)) diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 29a6cce1d..6fae2081d 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -83,7 +83,16 @@ static struct filter LANCZOS = { lanczos_filter, 3.0 }; #define PRECISION_BITS (32 - 8 - 2) -UINT8 _clip8_lookups[1024] = { +/* 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, @@ -147,10 +156,18 @@ UINT8 _clip8_lookups[1024] = { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 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 *clip8_lookups = &_clip8_lookups[512]; +UINT8 *clip8_lookups = &_clip8_lookups[640]; static inline UINT8 clip8(int in) { From aa929dda98cb0243face04049aa75000efc291d3 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 30 Mar 2018 02:02:37 +0300 Subject: [PATCH 19/26] from_cube_file + test --- Tests/test_color_lut.py | 73 ++++++++++++++++++++++++++++++++--------- src/PIL/ImageFilter.py | 57 +++++++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 17 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 3bfb05f02..6d1cb2373 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -253,37 +253,37 @@ class TestColorLut3DFilter(PillowTestCase): ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8) def test_convert_table(self): - flt = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) - self.assertEqual(tuple(flt.size), (2, 2, 2)) - self.assertEqual(flt.name, "Color 3D LUT") + lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) + self.assertEqual(tuple(lut.size), (2, 2, 2)) + self.assertEqual(lut.name, "Color 3D LUT") - flt = ImageFilter.Color3DLUT((2, 2, 2), [ + 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(flt.size), (2, 2, 2)) - self.assertEqual(flt.table, list(range(24))) + self.assertEqual(tuple(lut.size), (2, 2, 2)) + self.assertEqual(lut.table, list(range(24))) - flt = ImageFilter.Color3DLUT((2, 2, 2), [(0, 1, 2, 3)] * 8, + lut = ImageFilter.Color3DLUT((2, 2, 2), [(0, 1, 2, 3)] * 8, channels=4) def test_generate(self): - flt = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) - self.assertEqual(tuple(flt.size), (5, 5, 5)) - self.assertEqual(flt.name, "Color 3D LUT") - self.assertEqual(flt.table[:24], [ + 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(flt)) + self.assertEqual(im, im.filter(lut)) - flt = ImageFilter.Color3DLUT.generate(5, channels=4, + lut = ImageFilter.Color3DLUT.generate(5, channels=4, callback=lambda r, g, b: (b, r, g, (r+g+b) / 2)) - self.assertEqual(tuple(flt.size), (5, 5, 5)) - self.assertEqual(flt.name, "Color 3D LUT") - self.assertEqual(flt.table[:24], [ + 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]) @@ -294,6 +294,47 @@ class TestColorLut3DFilter(PillowTestCase): ImageFilter.Color3DLUT.generate(5, channels=4, callback=lambda r, g, b: (r, g, b)) + def test_from_cube_file_minimal(self): + lut = ImageFilter.Color3DLUT.from_cube_file([ + "LUT_3D_SIZE 2", + "", + "0 0 0.031", + "0.96 0 0.031", + "0 1 0.031", + "0.96 1 0.031", + "0 0 0.931", + "0.96 0 0.931", + "0 1 0.931", + "0.96 1 0.931", + ]) + self.assertEqual(tuple(lut.size), (2, 2, 2)) + self.assertEqual(lut.name, "Color 3D LUT") + self.assertEqual(lut.table[:12], [ + 0, 0, 0.031, 0.96, 0, 0.031, 0, 1, 0.031, 0.96, 1, 0.031]) + + def test_from_cube_file_parser(self): + lut = ImageFilter.Color3DLUT.from_cube_file([ + " # Comment", + 'TITLE "LUT name from file"', + "LUT_3D_SIZE 2 3 4", + " # Comment", + "CHANNELS 4", + "", + ] + [ + " # Comment", + "0 0 0.031 1", + "0.96 0 0.031 1", + "", + "0 1 0.031 1", + "0.96 1 0.031 1", + ] * 6, target_mode='HSV') + self.assertEqual(tuple(lut.size), (2, 3, 4)) + self.assertEqual(lut.channels, 4) + self.assertEqual(lut.name, "LUT name from file") + self.assertEqual(lut.mode, 'HSV') + self.assertEqual(lut.table[:12], [ + 0, 0, 0.031, 1, 0.96, 0, 0.031, 1, 0, 1, 0.031, 1]) + if __name__ == '__main__': unittest.main() diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index e16b47bdd..916c24cae 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -17,6 +17,8 @@ import functools +from ._util import isPath + class Filter(object): pass @@ -343,7 +345,8 @@ class Color3DLUT(MultibandFilter): raise ValueError( "The table should have channels * size**3 float items " "either size**3 items of channels-sized tuples with floats. " - "Table length: {}".format(len(table))) + "Table size: {}x{}x{}. Table length: {}".format( + size[0], size[1], size[2], len(table))) self.table = table @staticmethod @@ -385,6 +388,58 @@ class Color3DLUT(MultibandFilter): return cls((size1D, size2D, size3D), table, channels, target_mode) + @classmethod + def from_cube_file(cls, lines, target_mode=None): + name, size = None, None + channels = 3 + file = None + + if isPath(lines): + file = lines = open(lines, 'rt') + + try: + iterator = iter(lines) + + for i, line in enumerate(iterator, 1): + line = line.strip() + if not line: + break + if line.startswith('TITLE "'): + name = line.split('"')[1] + continue + if line.startswith('LUT_3D_SIZE '): + size = [int(x) for x in line.split()[1:]] + if len(size) == 1: + size = size[0] + continue + if line.startswith('CHANNELS '): + channels = int(line.split()[1]) + + if size is None: + raise ValueError('No size found in the file') + + table = [] + for i, line in enumerate(iterator, i + 1): + line = line.strip() + if not line or line.startswith('#'): + continue + try: + pixel = [float(x) for x in line.split()] + except ValueError: + raise ValueError("Not a number on line {}".format(i)) + if len(pixel) != channels: + raise ValueError( + "Wrong number of colors on line {}".format(i)) + table.append(tuple(pixel)) + finally: + if file is not None: + file.close() + + instance = cls(size, table, channels, target_mode) + if name is not None: + instance.name = name + return instance + def filter(self, image): from . import Image From e304a0d501315b95c71ea16c532216f391db240f Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 30 Mar 2018 09:57:54 +0300 Subject: [PATCH 20/26] add tests, fix error messages --- Tests/test_color_lut.py | 57 ++++++++++++++++++++++++++++++++++++++--- src/PIL/ImageFilter.py | 8 +++--- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 6d1cb2373..96050c6fb 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,4 +1,5 @@ from __future__ import division +from tempfile import NamedTemporaryFile from helper import unittest, PillowTestCase from PIL import Image, ImageFilter @@ -228,10 +229,10 @@ class TestColorLut3DCoreAPI(PillowTestCase): class TestColorLut3DFilter(PillowTestCase): def test_wrong_args(self): - with self.assertRaisesRegexp(ValueError, "should be an integer"): + with self.assertRaisesRegexp(ValueError, "should be either an integer"): ImageFilter.Color3DLUT("small", [1]) - with self.assertRaisesRegexp(ValueError, "should be an integer"): + with self.assertRaisesRegexp(ValueError, "should be either an integer"): ImageFilter.Color3DLUT((11, 11), [1]) with self.assertRaisesRegexp(ValueError, r"in \[2, 65\] range"): @@ -316,7 +317,8 @@ class TestColorLut3DFilter(PillowTestCase): lut = ImageFilter.Color3DLUT.from_cube_file([ " # Comment", 'TITLE "LUT name from file"', - "LUT_3D_SIZE 2 3 4", + " LUT_3D_SIZE 2 3 4", + " SKIP THIS", " # Comment", "CHANNELS 4", "", @@ -335,6 +337,55 @@ class TestColorLut3DFilter(PillowTestCase): self.assertEqual(lut.table[:12], [ 0, 0, 0.031, 1, 0.96, 0, 0.031, 1, 0, 1, 0.031, 1]) + def test_from_cube_file_errors(self): + with self.assertRaisesRegexp(ValueError, "No size found"): + lut = ImageFilter.Color3DLUT.from_cube_file([ + 'TITLE "LUT name from file"', + "", + ] + [ + "0 0 0.031", + "0.96 0 0.031", + ] * 3) + + with self.assertRaisesRegexp(ValueError, "number of colors on line 4"): + lut = ImageFilter.Color3DLUT.from_cube_file([ + 'LUT_3D_SIZE 2', + "", + ] + [ + "0 0 0.031", + "0.96 0 0.031 1", + ] * 3) + + with self.assertRaisesRegexp(ValueError, "Not a number on line 3"): + lut = ImageFilter.Color3DLUT.from_cube_file([ + 'LUT_3D_SIZE 2', + "", + ] + [ + "0 green 0.031", + "0.96 0 0.031", + ] * 3) + + def test_from_cube_file_filename(self): + with NamedTemporaryFile() as f: + f.write( + "LUT_3D_SIZE 2\n" + "\n" + "0 0 0.031\n" + "0.96 0 0.031\n" + "0 1 0.031\n" + "0.96 1 0.031\n" + "0 0 0.931\n" + "0.96 0 0.931\n" + "0 1 0.931\n" + "0.96 1 0.931\n" + ) + f.flush() + lut = ImageFilter.Color3DLUT.from_cube_file(f.name) + self.assertEqual(tuple(lut.size), (2, 2, 2)) + self.assertEqual(lut.name, "Color 3D LUT") + self.assertEqual(lut.table[:12], [ + 0, 0, 0.031, 0.96, 0, 0.031, 0, 1, 0.031, 0.96, 1, 0.031]) + if __name__ == '__main__': unittest.main() diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 916c24cae..4fb3db381 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -343,8 +343,8 @@ class Color3DLUT(MultibandFilter): if len(table) != channels * size[0] * size[1] * size[2]: raise ValueError( - "The table should have channels * size**3 float items " - "either size**3 items of channels-sized tuples with floats. " + "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 @@ -354,8 +354,8 @@ class Color3DLUT(MultibandFilter): try: _, _, _ = size except ValueError: - raise ValueError("Size should be an integer either " - "tuple of three integers.") + 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] From 83a5f6e5b530da16d6d7bd4cb2692ec5cefbd91c Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 30 Mar 2018 10:09:22 +0300 Subject: [PATCH 21/26] change file mode --- Tests/test_color_lut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 96050c6fb..7b9faaa75 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -366,7 +366,7 @@ class TestColorLut3DFilter(PillowTestCase): ] * 3) def test_from_cube_file_filename(self): - with NamedTemporaryFile() as f: + with NamedTemporaryFile('w+t') as f: f.write( "LUT_3D_SIZE 2\n" "\n" From c1b956e3c84d5b8378f7529e5004a3a8517f9228 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 30 Mar 2018 10:21:01 +0300 Subject: [PATCH 22/26] More tests fixes for windows --- Tests/test_color_lut.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 7b9faaa75..fffd3341b 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,8 +1,10 @@ from __future__ import division + +import os from tempfile import NamedTemporaryFile -from helper import unittest, PillowTestCase from PIL import Image, ImageFilter +from helper import unittest, PillowTestCase class TestColorLut3DCoreAPI(PillowTestCase): @@ -366,7 +368,7 @@ class TestColorLut3DFilter(PillowTestCase): ] * 3) def test_from_cube_file_filename(self): - with NamedTemporaryFile('w+t') as f: + with NamedTemporaryFile('w+t', delete=False) as f: f.write( "LUT_3D_SIZE 2\n" "\n" @@ -379,12 +381,15 @@ class TestColorLut3DFilter(PillowTestCase): "0 1 0.931\n" "0.96 1 0.931\n" ) - f.flush() + + try: lut = ImageFilter.Color3DLUT.from_cube_file(f.name) self.assertEqual(tuple(lut.size), (2, 2, 2)) self.assertEqual(lut.name, "Color 3D LUT") self.assertEqual(lut.table[:12], [ 0, 0, 0.031, 0.96, 0, 0.031, 0, 1, 0.031, 0.96, 1, 0.031]) + finally: + os.unlink(f.name) if __name__ == '__main__': From 805dc447076dc580816d730d360de08917d86a9e Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 30 Mar 2018 11:29:59 +0300 Subject: [PATCH 23/26] improve color cube parser --- Tests/test_color_lut.py | 18 +++++++++++------- src/PIL/ImageFilter.py | 15 ++++++++++++--- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index fffd3341b..4aedd5b43 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -300,7 +300,6 @@ class TestColorLut3DFilter(PillowTestCase): def test_from_cube_file_minimal(self): lut = ImageFilter.Color3DLUT.from_cube_file([ "LUT_3D_SIZE 2", - "", "0 0 0.031", "0.96 0 0.031", "0 1 0.031", @@ -321,6 +320,7 @@ class TestColorLut3DFilter(PillowTestCase): 'TITLE "LUT name from file"', " LUT_3D_SIZE 2 3 4", " SKIP THIS", + "", " # Comment", "CHANNELS 4", "", @@ -343,25 +343,30 @@ class TestColorLut3DFilter(PillowTestCase): with self.assertRaisesRegexp(ValueError, "No size found"): lut = ImageFilter.Color3DLUT.from_cube_file([ 'TITLE "LUT name from file"', - "", ] + [ "0 0 0.031", "0.96 0 0.031", ] * 3) - with self.assertRaisesRegexp(ValueError, "number of colors on line 4"): + with self.assertRaisesRegexp(ValueError, "number of colors on line 3"): lut = ImageFilter.Color3DLUT.from_cube_file([ 'LUT_3D_SIZE 2', - "", ] + [ "0 0 0.031", "0.96 0 0.031 1", ] * 3) - with self.assertRaisesRegexp(ValueError, "Not a number on line 3"): + with self.assertRaisesRegexp(ValueError, "1D LUT cube files"): + lut = ImageFilter.Color3DLUT.from_cube_file([ + 'LUT_1D_SIZE 2', + ] + [ + "0 0 0.031", + "0.96 0 0.031 1", + ]) + + with self.assertRaisesRegexp(ValueError, "Not a number on line 2"): lut = ImageFilter.Color3DLUT.from_cube_file([ 'LUT_3D_SIZE 2', - "", ] + [ "0 green 0.031", "0.96 0 0.031", @@ -371,7 +376,6 @@ class TestColorLut3DFilter(PillowTestCase): with NamedTemporaryFile('w+t', delete=False) as f: f.write( "LUT_3D_SIZE 2\n" - "\n" "0 0 0.031\n" "0.96 0 0.031\n" "0 1 0.031\n" diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 4fb3db381..8e193798d 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -16,6 +16,7 @@ # import functools +from itertools import chain from ._util import isPath @@ -402,8 +403,6 @@ class Color3DLUT(MultibandFilter): for i, line in enumerate(iterator, 1): line = line.strip() - if not line: - break if line.startswith('TITLE "'): name = line.split('"')[1] continue @@ -414,12 +413,22 @@ class Color3DLUT(MultibandFilter): continue if line.startswith('CHANNELS '): channels = int(line.split()[1]) + if line.startswith('LUT_1D_SIZE '): + raise ValueError("1D LUT cube files aren't supported.") + + try: + float(line.partition(' ')[0]) + except ValueError: + pass + else: + # Data starts + break if size is None: raise ValueError('No size found in the file') table = [] - for i, line in enumerate(iterator, i + 1): + for i, line in enumerate(chain([line], iterator), i): line = line.strip() if not line or line.startswith('#'): continue From 76d467245d0c66376543b5f130eab09b37c5aea4 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 31 Mar 2018 09:52:05 +0300 Subject: [PATCH 24/26] Release GIL --- src/libImaging/ColorLUT.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c index 36b87f108..9988ad1b1 100644 --- a/src/libImaging/ColorLUT.c +++ b/src/libImaging/ColorLUT.c @@ -82,6 +82,7 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, UINT32 scale3D = (size3D - 1) / 255.0 * (1< 4) { PyErr_SetString(PyExc_ValueError, "table_channels could be 3 or 4"); @@ -101,6 +102,7 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, 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]; @@ -156,6 +158,7 @@ ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, } } } + ImagingSectionLeave(&cookie); return imOut; } From 912980c52f35e1ab4453aa29b83a4ec1c3e3d861 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 31 Mar 2018 19:55:43 +0300 Subject: [PATCH 25/26] =?UTF-8?q?Remove=20Color3DLUT.from=5Fcube=5Ffile=20?= =?UTF-8?q?from=20=D1=81ore=20library?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/test_color_lut.py | 98 ----------------------------------------- src/PIL/ImageFilter.py | 60 ------------------------- 2 files changed, 158 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 4aedd5b43..b6d6e441f 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -297,104 +297,6 @@ class TestColorLut3DFilter(PillowTestCase): ImageFilter.Color3DLUT.generate(5, channels=4, callback=lambda r, g, b: (r, g, b)) - def test_from_cube_file_minimal(self): - lut = ImageFilter.Color3DLUT.from_cube_file([ - "LUT_3D_SIZE 2", - "0 0 0.031", - "0.96 0 0.031", - "0 1 0.031", - "0.96 1 0.031", - "0 0 0.931", - "0.96 0 0.931", - "0 1 0.931", - "0.96 1 0.931", - ]) - self.assertEqual(tuple(lut.size), (2, 2, 2)) - self.assertEqual(lut.name, "Color 3D LUT") - self.assertEqual(lut.table[:12], [ - 0, 0, 0.031, 0.96, 0, 0.031, 0, 1, 0.031, 0.96, 1, 0.031]) - - def test_from_cube_file_parser(self): - lut = ImageFilter.Color3DLUT.from_cube_file([ - " # Comment", - 'TITLE "LUT name from file"', - " LUT_3D_SIZE 2 3 4", - " SKIP THIS", - "", - " # Comment", - "CHANNELS 4", - "", - ] + [ - " # Comment", - "0 0 0.031 1", - "0.96 0 0.031 1", - "", - "0 1 0.031 1", - "0.96 1 0.031 1", - ] * 6, target_mode='HSV') - self.assertEqual(tuple(lut.size), (2, 3, 4)) - self.assertEqual(lut.channels, 4) - self.assertEqual(lut.name, "LUT name from file") - self.assertEqual(lut.mode, 'HSV') - self.assertEqual(lut.table[:12], [ - 0, 0, 0.031, 1, 0.96, 0, 0.031, 1, 0, 1, 0.031, 1]) - - def test_from_cube_file_errors(self): - with self.assertRaisesRegexp(ValueError, "No size found"): - lut = ImageFilter.Color3DLUT.from_cube_file([ - 'TITLE "LUT name from file"', - ] + [ - "0 0 0.031", - "0.96 0 0.031", - ] * 3) - - with self.assertRaisesRegexp(ValueError, "number of colors on line 3"): - lut = ImageFilter.Color3DLUT.from_cube_file([ - 'LUT_3D_SIZE 2', - ] + [ - "0 0 0.031", - "0.96 0 0.031 1", - ] * 3) - - with self.assertRaisesRegexp(ValueError, "1D LUT cube files"): - lut = ImageFilter.Color3DLUT.from_cube_file([ - 'LUT_1D_SIZE 2', - ] + [ - "0 0 0.031", - "0.96 0 0.031 1", - ]) - - with self.assertRaisesRegexp(ValueError, "Not a number on line 2"): - lut = ImageFilter.Color3DLUT.from_cube_file([ - 'LUT_3D_SIZE 2', - ] + [ - "0 green 0.031", - "0.96 0 0.031", - ] * 3) - - def test_from_cube_file_filename(self): - with NamedTemporaryFile('w+t', delete=False) as f: - f.write( - "LUT_3D_SIZE 2\n" - "0 0 0.031\n" - "0.96 0 0.031\n" - "0 1 0.031\n" - "0.96 1 0.031\n" - "0 0 0.931\n" - "0.96 0 0.931\n" - "0 1 0.931\n" - "0.96 1 0.931\n" - ) - - try: - lut = ImageFilter.Color3DLUT.from_cube_file(f.name) - self.assertEqual(tuple(lut.size), (2, 2, 2)) - self.assertEqual(lut.name, "Color 3D LUT") - self.assertEqual(lut.table[:12], [ - 0, 0, 0.031, 0.96, 0, 0.031, 0, 1, 0.031, 0.96, 1, 0.031]) - finally: - os.unlink(f.name) - if __name__ == '__main__': unittest.main() diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 8e193798d..63e0d46e8 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -389,66 +389,6 @@ class Color3DLUT(MultibandFilter): return cls((size1D, size2D, size3D), table, channels, target_mode) - @classmethod - def from_cube_file(cls, lines, target_mode=None): - name, size = None, None - channels = 3 - file = None - - if isPath(lines): - file = lines = open(lines, 'rt') - - try: - iterator = iter(lines) - - for i, line in enumerate(iterator, 1): - line = line.strip() - if line.startswith('TITLE "'): - name = line.split('"')[1] - continue - if line.startswith('LUT_3D_SIZE '): - size = [int(x) for x in line.split()[1:]] - if len(size) == 1: - size = size[0] - continue - if line.startswith('CHANNELS '): - channels = int(line.split()[1]) - if line.startswith('LUT_1D_SIZE '): - raise ValueError("1D LUT cube files aren't supported.") - - try: - float(line.partition(' ')[0]) - except ValueError: - pass - else: - # Data starts - break - - if size is None: - raise ValueError('No size found in the file') - - table = [] - for i, line in enumerate(chain([line], iterator), i): - line = line.strip() - if not line or line.startswith('#'): - continue - try: - pixel = [float(x) for x in line.split()] - except ValueError: - raise ValueError("Not a number on line {}".format(i)) - if len(pixel) != channels: - raise ValueError( - "Wrong number of colors on line {}".format(i)) - table.append(tuple(pixel)) - finally: - if file is not None: - file.close() - - instance = cls(size, table, channels, target_mode) - if name is not None: - instance.name = name - return instance - def filter(self, image): from . import Image From 42310381325e1cbcaa1920f32a8534a31bebc818 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 1 Apr 2018 19:52:39 +0300 Subject: [PATCH 26/26] Remove unused imports --- Tests/test_color_lut.py | 3 --- src/PIL/ImageFilter.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index b6d6e441f..f9d35c83c 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,8 +1,5 @@ from __future__ import division -import os -from tempfile import NamedTemporaryFile - from PIL import Image, ImageFilter from helper import unittest, PillowTestCase diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 63e0d46e8..93cd7ad47 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -16,9 +16,6 @@ # import functools -from itertools import chain - -from ._util import isPath class Filter(object):