diff --git a/Tests/helper.py b/Tests/helper.py index 207f497d2..b6ef6dc13 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -192,6 +192,16 @@ class PillowTestCase(unittest.TestCase): def assert_not_all_same(self, items, msg=None): self.assertFalse(items.count(items[0]) == len(items), msg) + def assert_tuple_approx_equal(self, actuals, targets, threshold, msg): + """Tests if actuals has values within threshold from targets""" + + value = True + for i, target in enumerate(targets): + value *= (target - threshold <= actuals[i] <= target + threshold) + + self.assertTrue(value, + msg + ': ' + repr(actuals) + ' != ' + repr(targets)) + def skipKnownBadTest(self, msg=None, platform=None, travis=None, interpreter=None): # Skip if platform/travis matches, and diff --git a/Tests/images/bw_gradient.png b/Tests/images/bw_gradient.png new file mode 100644 index 000000000..79c921486 Binary files /dev/null and b/Tests/images/bw_gradient.png differ diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 11cf3619d..9c4da2463 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,6 +1,7 @@ from helper import unittest, PillowTestCase, hopper from PIL import ImageOps +from PIL import Image class TestImageOps(PillowTestCase): @@ -94,6 +95,107 @@ class TestImageOps(PillowTestCase): newimg = ImageOps.scale(i, 0.5) self.assertEqual(newimg.size, (25, 25)) + def test_colorize_2color(self): + # Test the colorizing function with 2-color functionality + + # Open test image (256px by 10px, black to white) + im = Image.open("Tests/images/bw_gradient.png") + im = im.convert("L") + + # Create image with original 2-color functionality + im_test = ImageOps.colorize(im, 'red', 'green') + + # Test output image (2-color) + left = (0, 1) + middle = (127, 1) + right = (255, 1) + self.assert_tuple_approx_equal(im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg='black test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg='mid test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg='white test pixel incorrect') + + def test_colorize_2color_offset(self): + # Test the colorizing function with 2-color functionality and offset + + # Open test image (256px by 10px, black to white) + im = Image.open("Tests/images/bw_gradient.png") + im = im.convert("L") + + # Create image with original 2-color functionality with offsets + im_test = ImageOps.colorize(im, + black='red', + white='green', + blackpoint=50, + whitepoint=100) + + # Test output image (2-color) with offsets + left = (25, 1) + middle = (75, 1) + right = (125, 1) + self.assert_tuple_approx_equal(im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg='black test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg='mid test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg='white test pixel incorrect') + + def test_colorize_3color_offset(self): + # Test the colorizing function with 3-color functionality and offset + + # Open test image (256px by 10px, black to white) + im = Image.open("Tests/images/bw_gradient.png") + im = im.convert("L") + + # Create image with new three color functionality with offsets + im_test = ImageOps.colorize(im, + black='red', + white='green', + mid='blue', + blackpoint=50, + whitepoint=200, + midpoint=100) + + # Test output image (3-color) with offsets + left = (25, 1) + left_middle = (75, 1) + middle = (100, 1) + right_middle = (150, 1) + right = (225, 1) + self.assert_tuple_approx_equal(im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg='black test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(left_middle), + (127, 0, 127), + threshold=1, + msg='low-mid test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(middle), + (0, 0, 255), + threshold=1, + msg='mid incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(right_middle), + (0, 63, 127), + threshold=1, + msg='high-mid test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg='white test pixel incorrect') + if __name__ == '__main__': unittest.main() diff --git a/docs/releasenotes/5.3.0.rst b/docs/releasenotes/5.3.0.rst new file mode 100644 index 000000000..9869b6a7a --- /dev/null +++ b/docs/releasenotes/5.3.0.rst @@ -0,0 +1,39 @@ +5.3.0 +----- + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +These version constants have been deprecated. ``VERSION`` will be removed in +Pillow 6.0.0, and ``PILLOW_VERSION`` will be removed after that. + +* ``PIL.VERSION`` (old PIL version 1.1.7) +* ``PIL.PILLOW_VERSION`` +* ``PIL.Image.VERSION`` +* ``PIL.Image.PILLOW_VERSION`` + +Use ``PIL.__version__`` instead. + +API Additions +============= + +ImageOps.colorize +^^^^^^^^^^^^^^^^^ + +Previously ``ImageOps.colorize`` only supported two-color mapping with +``black`` and ``white`` arguments being mapped to 0 and 255 respectively. +Now it supports three-color mapping with the optional ``mid`` parameter, and +the positions for all three color arguments can each be optionally specified +(``blackpoint``, ``whitepoint`` and ``midpoint``). +For example, with all optional arguments:: + ImageOps.colorize(im, black=(32, 37, 79), white='white', mid=(59, 101, 175), + blackpoint=15, whitepoint=240, midpoint=100) + + + +Other Changes +============= + diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 16e5c1d85..fc8d686eb 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,6 +6,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 5.3.0 5.2.0 5.1.0 5.0.0 diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 25d491aff..9b470062a 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -136,28 +136,87 @@ def autocontrast(image, cutoff=0, ignore=None): return _lut(image, lut) -def colorize(image, black, white): +def colorize(image, black, white, mid=None, blackpoint=0, + whitepoint=255, midpoint=127): """ - Colorize grayscale image. The **black** and **white** - arguments should be RGB tuples; this function calculates a color - wedge mapping all black pixels in the source image to the first - color, and all white pixels to the second color. + Colorize grayscale image. + This function calculates a color wedge which maps all black pixels in + the source image to the first color and all white pixels to the + second color. If **mid** is specified, it uses three-color mapping. + The **black** and **white** arguments should be RGB tuples or color names; + optionally you can use three-color mapping by also specifying **mid**. + Mapping positions for any of the colors can be specified + (e.g. **blackpoint**), where these parameters are the integer + value corresponding to where the corresponding color should be mapped. + These parameters must have logical order, such that + **blackpoint** <= **midpoint** <= **whitepoint** (if **mid** is specified). :param image: The image to colorize. :param black: The color to use for black input pixels. :param white: The color to use for white input pixels. + :param mid: The color to use for midtone input pixels. + :param blackpoint: an int value [0, 255] for the black mapping. + :param whitepoint: an int value [0, 255] for the white mapping. + :param midpoint: an int value [0, 255] for the midtone mapping. :return: An image. """ + + # Initial asserts assert image.mode == "L" + if mid is None: + assert 0 <= blackpoint <= whitepoint <= 255 + else: + assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 + + # Define colors from arguments black = _color(black, "RGB") white = _color(white, "RGB") + if mid is not None: + mid = _color(mid, "RGB") + + # Empty lists for the mapping red = [] green = [] blue = [] - for i in range(256): - red.append(black[0]+i*(white[0]-black[0])//255) - green.append(black[1]+i*(white[1]-black[1])//255) - blue.append(black[2]+i*(white[2]-black[2])//255) + + # Create the low-end values + for i in range(0, blackpoint): + red.append(black[0]) + green.append(black[1]) + blue.append(black[2]) + + # Create the mapping (2-color) + if mid is None: + + range_map = range(0, whitepoint - blackpoint) + + for i in range_map: + red.append(black[0] + i * (white[0] - black[0]) // len(range_map)) + green.append(black[1] + i * (white[1] - black[1]) // len(range_map)) + blue.append(black[2] + i * (white[2] - black[2]) // len(range_map)) + + # Create the mapping (3-color) + else: + + range_map1 = range(0, midpoint - blackpoint) + range_map2 = range(0, whitepoint - midpoint) + + for i in range_map1: + red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1)) + green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1)) + blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1)) + for i in range_map2: + red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2)) + green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2)) + blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2)) + + # Create the high-end values + for i in range(0, 256 - whitepoint): + red.append(white[0]) + green.append(white[1]) + blue.append(white[2]) + + # Return converted image image = image.convert("RGB") return _lut(image, red + green + blue)