Skip to content

Commit 2b09e7f

Browse files
authored
Merge pull request #3099 from uploadcare/lut-numpy
NumPy support for LUTs
2 parents d52fd2b + 33c0b5d commit 2b09e7f

6 files changed

Lines changed: 204 additions & 43 deletions

File tree

.travis/install.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pip install -U pytest
1515
pip install -U pytest-cov
1616
pip install pyroma
1717
pip install test-image-results
18+
pip install numpy
1819

1920
# docs only on Python 2.7
2021
if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then pip install -r requirements.txt ; fi

Tests/test_color_lut.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
from PIL import Image, ImageFilter
66
from helper import unittest, PillowTestCase
77

8+
try:
9+
import numpy
10+
except ImportError:
11+
numpy = None
12+
813

914
class TestColorLut3DCoreAPI(PillowTestCase):
1015
def generate_identity_table(self, channels, size):
@@ -279,6 +284,80 @@ def test_convert_table(self):
279284
lut = ImageFilter.Color3DLUT((2, 2, 2), [(0, 1, 2, 3)] * 8,
280285
channels=4)
281286

287+
@unittest.skipIf(numpy is None, "Numpy is not installed")
288+
def test_numpy_sources(self):
289+
table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16)
290+
with self.assertRaisesRegex(ValueError, "should have either channels"):
291+
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
292+
293+
table = numpy.ones((7, 6, 5, 3), dtype=numpy.float16)
294+
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
295+
self.assertIsInstance(lut.table, numpy.ndarray)
296+
self.assertEqual(lut.table.dtype, table.dtype)
297+
self.assertEqual(lut.table.shape, (table.size,))
298+
299+
table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16)
300+
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
301+
self.assertEqual(lut.table.shape, (table.size,))
302+
303+
table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16)
304+
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
305+
self.assertEqual(lut.table.shape, (table.size,))
306+
307+
# Check application
308+
Image.new('RGB', (10, 10), 0).filter(lut)
309+
310+
# Check copy
311+
table[0] = 33
312+
self.assertEqual(lut.table[0], 1)
313+
314+
# Check not copy
315+
table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16)
316+
lut = ImageFilter.Color3DLUT((5, 6, 7), table, _copy_table=False)
317+
table[0] = 33
318+
self.assertEqual(lut.table[0], 33)
319+
320+
@unittest.skipIf(numpy is None, "Numpy is not installed")
321+
def test_numpy_formats(self):
322+
g = Image.linear_gradient('L')
323+
im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90),
324+
g.transpose(Image.ROTATE_180)])
325+
326+
lut = ImageFilter.Color3DLUT.generate((7, 9, 11),
327+
lambda r, g, b: (r, g, b))
328+
lut.table = numpy.array(lut.table, dtype=numpy.float32)[:-1]
329+
with self.assertRaisesRegex(ValueError, "should have table_channels"):
330+
im.filter(lut)
331+
332+
lut = ImageFilter.Color3DLUT.generate((7, 9, 11),
333+
lambda r, g, b: (r, g, b))
334+
lut.table = (numpy.array(lut.table, dtype=numpy.float32)
335+
.reshape((7 * 9 * 11), 3))
336+
with self.assertRaisesRegex(ValueError, "should have table_channels"):
337+
im.filter(lut)
338+
339+
lut = ImageFilter.Color3DLUT.generate((7, 9, 11),
340+
lambda r, g, b: (r, g, b))
341+
lut.table = numpy.array(lut.table, dtype=numpy.float16)
342+
self.assert_image_equal(im, im.filter(lut))
343+
344+
lut = ImageFilter.Color3DLUT.generate((7, 9, 11),
345+
lambda r, g, b: (r, g, b))
346+
lut.table = numpy.array(lut.table, dtype=numpy.float32)
347+
self.assert_image_equal(im, im.filter(lut))
348+
349+
lut = ImageFilter.Color3DLUT.generate((7, 9, 11),
350+
lambda r, g, b: (r, g, b))
351+
lut.table = numpy.array(lut.table, dtype=numpy.float64)
352+
self.assert_image_equal(im, im.filter(lut))
353+
354+
lut = ImageFilter.Color3DLUT.generate((7, 9, 11),
355+
lambda r, g, b: (r, g, b))
356+
lut.table = numpy.array(lut.table, dtype=numpy.int32)
357+
im.filter(lut)
358+
lut.table = numpy.array(lut.table, dtype=numpy.int8)
359+
im.filter(lut)
360+
282361
def test_repr(self):
283362
lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8)
284363
self.assertEqual(repr(lut),

Tests/test_numpy.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,16 @@
44
from PIL import Image
55

66
try:
7-
import site
87
import numpy
9-
assert site # silence warning
10-
assert numpy # silence warning
118
except ImportError:
12-
# Skip via setUp()
13-
pass
9+
numpy = None
10+
1411

1512
TEST_IMAGE_SIZE = (10, 10)
1613

1714

15+
@unittest.skipIf(numpy is None, "Numpy is not installed")
1816
class TestNumpy(PillowTestCase):
19-
20-
def setUp(self):
21-
try:
22-
import site
23-
import numpy
24-
assert site # silence warning
25-
assert numpy # silence warning
26-
except ImportError:
27-
self.skipTest("ImportError")
28-
2917
def test_numpy_to_image(self):
3018

3119
def to_image(dtype, bands=1, boolean=0):

src/PIL/ImageFilter.py

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919

2020
import functools
2121

22+
try:
23+
import numpy
24+
except ImportError: # pragma: no cover
25+
numpy = None
26+
2227

2328
class Filter(object):
2429
pass
@@ -310,6 +315,8 @@ class Color3DLUT(MultibandFilter):
310315
This method allows you to apply almost any color transformation
311316
in constant time by using pre-calculated decimated tables.
312317
318+
.. versionadded:: 5.2.0
319+
313320
:param size: Size of the table. One int or tuple of (int, int, int).
314321
Minimal size in any dimension is 2, maximum is 65.
315322
:param table: Flat lookup table. A list of ``channels * size**3``
@@ -334,25 +341,40 @@ def __init__(self, size, table, channels=3, target_mode=None, **kwargs):
334341

335342
# Hidden flag `_copy_table=False` could be used to avoid extra copying
336343
# of the table if the table is specially made for the constructor.
337-
if kwargs.get('_copy_table', True):
338-
table = list(table)
339-
340-
# Convert to a flat list
341-
if table and isinstance(table[0], (list, tuple)):
342-
table, raw_table = [], table
343-
for pixel in raw_table:
344-
if len(pixel) != channels:
345-
raise ValueError("The elements of the table should have "
346-
"a length of {}.".format(channels))
347-
for color in pixel:
348-
table.append(color)
349-
350-
if len(table) != channels * size[0] * size[1] * size[2]:
344+
copy_table = kwargs.get('_copy_table', True)
345+
items = size[0] * size[1] * size[2]
346+
wrong_size = False
347+
348+
if numpy and isinstance(table, numpy.ndarray):
349+
if copy_table:
350+
table = table.copy()
351+
352+
if table.shape in [(items * channels,), (items, channels),
353+
(size[2], size[1], size[0], channels)]:
354+
table = table.reshape(items * channels)
355+
else:
356+
wrong_size = True
357+
358+
else:
359+
if copy_table:
360+
table = list(table)
361+
362+
# Convert to a flat list
363+
if table and isinstance(table[0], (list, tuple)):
364+
table, raw_table = [], table
365+
for pixel in raw_table:
366+
if len(pixel) != channels:
367+
raise ValueError(
368+
"The elements of the table should "
369+
"have a length of {}.".format(channels))
370+
table.extend(pixel)
371+
372+
if wrong_size or len(table) != items * channels:
351373
raise ValueError(
352374
"The table should have either channels * size**3 float items "
353375
"or size**3 items of channels-sized tuples with floats. "
354-
"Table size: {}x{}x{}. Table length: {}".format(
355-
size[0], size[1], size[2], len(table)))
376+
"Table should be: {}x{}x{}x{}. Actual length: {}".format(
377+
channels, size[0], size[1], size[2], len(table)))
356378
self.table = table
357379

358380
@staticmethod

src/_imaging.c

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ getbands(const char* mode)
354354

355355
#define TYPE_UINT8 (0x100|sizeof(UINT8))
356356
#define TYPE_INT32 (0x200|sizeof(INT32))
357+
#define TYPE_FLOAT16 (0x500|sizeof(FLOAT16))
357358
#define TYPE_FLOAT32 (0x300|sizeof(FLOAT32))
358359
#define TYPE_DOUBLE (0x400|sizeof(double))
359360

@@ -437,6 +438,30 @@ getlist(PyObject* arg, Py_ssize_t* length, const char* wrong_length, int type)
437438
return list;
438439
}
439440

441+
FLOAT32
442+
float16tofloat32(const FLOAT16 in) {
443+
UINT32 t1;
444+
UINT32 t2;
445+
UINT32 t3;
446+
FLOAT32 out[1] = {0};
447+
448+
t1 = in & 0x7fff; // Non-sign bits
449+
t2 = in & 0x8000; // Sign bit
450+
t3 = in & 0x7c00; // Exponent
451+
452+
t1 <<= 13; // Align mantissa on MSB
453+
t2 <<= 16; // Shift sign bit into position
454+
455+
t1 += 0x38000000; // Adjust bias
456+
457+
t1 = (t3 == 0 ? 0 : t1); // Denormals-as-zero
458+
459+
t1 |= t2; // Re-insert sign bit
460+
461+
memcpy(out, &t1, 4);
462+
return out[0];
463+
}
464+
440465
static inline PyObject*
441466
getpixel(Imaging im, ImagingAccess access, int x, int y)
442467
{
@@ -700,22 +725,54 @@ _blend(ImagingObject* self, PyObject* args)
700725
/* METHODS */
701726
/* -------------------------------------------------------------------- */
702727

703-
704728
static INT16*
705729
_prepare_lut_table(PyObject* table, Py_ssize_t table_size)
706730
{
707731
int i;
708-
FLOAT32* table_data;
732+
Py_buffer buffer_info;
733+
INT32 data_type = TYPE_FLOAT32;
734+
float item = 0;
735+
void* table_data = NULL;
736+
int free_table_data = 0;
709737
INT16* prepared;
710738

711739
/* NOTE: This value should be the same as in ColorLUT.c */
712740
#define PRECISION_BITS (16 - 8 - 2)
713741

714-
table_data = (FLOAT32*) getlist(table, &table_size,
715-
"The table should have table_channels * "
716-
"size1D * size2D * size3D float items.", TYPE_FLOAT32);
742+
const char* wrong_size = ("The table should have table_channels * "
743+
"size1D * size2D * size3D float items.");
744+
745+
if (PyObject_CheckBuffer(table)) {
746+
if ( ! PyObject_GetBuffer(table, &buffer_info,
747+
PyBUF_CONTIG_RO | PyBUF_FORMAT)) {
748+
if (buffer_info.ndim == 1 && buffer_info.shape[0] == table_size) {
749+
if (strlen(buffer_info.format) == 1) {
750+
switch (buffer_info.format[0]) {
751+
case 'e':
752+
data_type = TYPE_FLOAT16;
753+
table_data = buffer_info.buf;
754+
break;
755+
case 'f':
756+
data_type = TYPE_FLOAT32;
757+
table_data = buffer_info.buf;
758+
break;
759+
case 'd':
760+
data_type = TYPE_DOUBLE;
761+
table_data = buffer_info.buf;
762+
break;
763+
}
764+
}
765+
}
766+
PyBuffer_Release(&buffer_info);
767+
}
768+
}
769+
717770
if ( ! table_data) {
718-
return NULL;
771+
free_table_data = 1;
772+
table_data = getlist(table, &table_size, wrong_size, TYPE_FLOAT32);
773+
if ( ! table_data) {
774+
return NULL;
775+
}
719776
}
720777

721778
/* malloc check ok, max is 2 * 4 * 65**3 = 2197000 */
@@ -726,25 +783,38 @@ _prepare_lut_table(PyObject* table, Py_ssize_t table_size)
726783
}
727784

728785
for (i = 0; i < table_size; i++) {
786+
switch (data_type) {
787+
case TYPE_FLOAT16:
788+
item = float16tofloat32(((FLOAT16*) table_data)[i]);
789+
break;
790+
case TYPE_FLOAT32:
791+
item = ((FLOAT32*) table_data)[i];
792+
break;
793+
case TYPE_DOUBLE:
794+
item = ((double*) table_data)[i];
795+
break;
796+
}
729797
/* Max value for INT16 */
730-
if (table_data[i] >= (0x7fff - 0.5) / (255 << PRECISION_BITS)) {
798+
if (item >= (0x7fff - 0.5) / (255 << PRECISION_BITS)) {
731799
prepared[i] = 0x7fff;
732800
continue;
733801
}
734802
/* Min value for INT16 */
735-
if (table_data[i] <= (-0x8000 + 0.5) / (255 << PRECISION_BITS)) {
803+
if (item <= (-0x8000 + 0.5) / (255 << PRECISION_BITS)) {
736804
prepared[i] = -0x8000;
737805
continue;
738806
}
739-
if (table_data[i] < 0) {
740-
prepared[i] = table_data[i] * (255 << PRECISION_BITS) - 0.5;
807+
if (item < 0) {
808+
prepared[i] = item * (255 << PRECISION_BITS) - 0.5;
741809
} else {
742-
prepared[i] = table_data[i] * (255 << PRECISION_BITS) + 0.5;
810+
prepared[i] = item * (255 << PRECISION_BITS) + 0.5;
743811
}
744812
}
745813

746814
#undef PRECISION_BITS
747-
free(table_data);
815+
if (free_table_data) {
816+
free(table_data);
817+
}
748818
return prepared;
749819
}
750820

src/libImaging/ImPlatform.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
#endif
7272

7373
/* assume IEEE; tweak if necessary (patches are welcome) */
74+
#define FLOAT16 UINT16
7475
#define FLOAT32 float
7576
#define FLOAT64 double
7677

0 commit comments

Comments
 (0)