From 63d8637bb8cbd90ce0372a221b2acdae505b053c Mon Sep 17 00:00:00 2001 From: tsennott Date: Fri, 6 Jul 2018 18:18:06 -0700 Subject: [PATCH 1/8] adding three-color feature to ImageOps.colorize --- src/PIL/ImageOps.py | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 25d491aff..33196d5ed 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -136,28 +136,50 @@ def autocontrast(image, cutoff=0, ignore=None): return _lut(image, lut) -def colorize(image, black, white): +def colorize(image, black, white, mid=None, midpoint=128): """ - 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 mapping 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; optionally you can use + three color mapping by also specifying **mid**, and + optionally, **midpoint** (which is the integer value + in [0, 255] corresponding to the midpoint color, + default 128). :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 midpoint: the int value [0, 255] for the mid color. :return: An image. """ assert image.mode == "L" black = _color(black, "RGB") + if mid is not None: + mid = _color(mid, "RGB") white = _color(white, "RGB") 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) + if mid is None: + 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) + else: + for i in range(0, midpoint): + red.append(black[0] + i * (mid[0] - black[0]) // 255) + green.append(black[1] + i * (mid[1] - black[1]) // 255) + blue.append(black[2] + i * (mid[2] - black[2]) // 255) + for i in range(0, 256 - midpoint): + red.append(mid[0] + i * (white[0] - mid[0]) // 255) + green.append(mid[1] + i * (white[1] - mid[1]) // 255) + blue.append(mid[2] + i * (white[2] - mid[2]) // 255) + image = image.convert("RGB") return _lut(image, red + green + blue) From adf570a77ea802d86a59199a165aef6c053b852b Mon Sep 17 00:00:00 2001 From: tsennott Date: Fri, 6 Jul 2018 18:42:16 -0700 Subject: [PATCH 2/8] adding tests, updated docstring and comments --- Tests/test_imageops.py | 16 ++++++++++++++++ src/PIL/ImageOps.py | 8 ++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 11cf3619d..63ec0d2e4 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -94,6 +94,22 @@ class TestImageOps(PillowTestCase): newimg = ImageOps.scale(i, 0.5) self.assertEqual(newimg.size, (25, 25)) + def test_colorize(self): + # Test the colorizing function + + # Grab test image + i = hopper("L").resize((15, 16)) + + # Test original 2-color functionality + ImageOps.colorize(i, 'green', 'red') + + # Test new three color functionality (cyanotype colors) + ImageOps.colorize(i, + (32, 37, 79), + (255, 255, 255), + (35, 52, 121), + 40) + if __name__ == '__main__': unittest.main() diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 33196d5ed..29dec124d 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -147,8 +147,8 @@ def colorize(image, black, white, mid=None, midpoint=128): arguments should be RGB tuples; optionally you can use three color mapping by also specifying **mid**, and optionally, **midpoint** (which is the integer value - in [0, 255] corresponding to the midpoint color, - default 128). + in [1, 254] corresponding to where the midpoint color + should be mapped (0 being black and 255 being white). :param image: The image to colorize. :param black: The color to use for black input pixels. @@ -158,10 +158,14 @@ def colorize(image, black, white, mid=None, midpoint=128): :return: An image. """ assert image.mode == "L" + + # Define colors from arguments black = _color(black, "RGB") if mid is not None: mid = _color(mid, "RGB") white = _color(white, "RGB") + + # Create the mapping red = [] green = [] blue = [] From 3c6fd275c8965d85ee89dfff608420e638feec21 Mon Sep 17 00:00:00 2001 From: tsennott Date: Fri, 6 Jul 2018 19:09:57 -0700 Subject: [PATCH 3/8] added assert for midpoint range --- src/PIL/ImageOps.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 29dec124d..133f9a8b8 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -154,10 +154,11 @@ def colorize(image, black, white, mid=None, midpoint=128): :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 midpoint: the int value [0, 255] for the mid color. + :param midpoint: the int value in [1, 254] for the mid color. :return: An image. """ assert image.mode == "L" + assert 1 <= midpoint <= 254 # Define colors from arguments black = _color(black, "RGB") From b19c460568675268145ce9f8ea2cfb42c357a5f0 Mon Sep 17 00:00:00 2001 From: tsennott Date: Fri, 6 Jul 2018 19:49:07 -0700 Subject: [PATCH 4/8] fixed mapping function, now smooth --- src/PIL/ImageOps.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 133f9a8b8..e5ce71060 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -136,7 +136,7 @@ def autocontrast(image, cutoff=0, ignore=None): return _lut(image, lut) -def colorize(image, black, white, mid=None, midpoint=128): +def colorize(image, black, white, mid=None, midpoint=127): """ Colorize grayscale image. This function calculates a color wedge mapping all @@ -176,14 +176,16 @@ def colorize(image, black, white, mid=None, midpoint=128): green.append(black[1] + i * (white[1] - black[1]) // 255) blue.append(black[2] + i * (white[2] - black[2]) // 255) else: - for i in range(0, midpoint): - red.append(black[0] + i * (mid[0] - black[0]) // 255) - green.append(black[1] + i * (mid[1] - black[1]) // 255) - blue.append(black[2] + i * (mid[2] - black[2]) // 255) - for i in range(0, 256 - midpoint): - red.append(mid[0] + i * (white[0] - mid[0]) // 255) - green.append(mid[1] + i * (white[1] - mid[1]) // 255) - blue.append(mid[2] + i * (white[2] - mid[2]) // 255) + range1 = range(0, midpoint) + range2 = range(0, 256 - midpoint) + for i in range1: + red.append(black[0] + i * (mid[0] - black[0]) // len(range1)) + green.append(black[1] + i * (mid[1] - black[1]) // len(range1)) + blue.append(black[2] + i * (mid[2] - black[2]) // len(range1)) + for i in range2: + red.append(mid[0] + i * (white[0] - mid[0]) // len(range2)) + green.append(mid[1] + i * (white[1] - mid[1]) // len(range2)) + blue.append(mid[2] + i * (white[2] - mid[2]) // len(range2)) image = image.convert("RGB") return _lut(image, red + green + blue) From 837d8683332a14cdd5234cfebe026235417d1a6a Mon Sep 17 00:00:00 2001 From: tsennott Date: Sat, 7 Jul 2018 02:40:25 -0700 Subject: [PATCH 5/8] updated test to assert equality with reference images --- Tests/images/bw_gradient.png | Bin 0 -> 1926 bytes Tests/images/bw_gradient_2color.png | Bin 0 -> 179 bytes Tests/images/bw_gradient_3color.png | Bin 0 -> 246 bytes Tests/test_imageops.py | 24 +++++++++++++++--------- 4 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 Tests/images/bw_gradient.png create mode 100644 Tests/images/bw_gradient_2color.png create mode 100644 Tests/images/bw_gradient_3color.png diff --git a/Tests/images/bw_gradient.png b/Tests/images/bw_gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..0b7275dc19cb188d0b1c65334593fe5adc948476 GIT binary patch literal 1926 zcmb_dU5MO798WEN9aa&2Qjm~?Qqf6fl592^ZauEqv%AvVo!g~7-#R-pyBlwlNt4{| z-aUx=B2^#zAmWqagCHXKpwb76S_Sb%DiqN-K@cp0FQS4t*^gN3T|Ew$-RxxM_y79+ ze=~D_ZRNy~xd-MHMLE)J)LV*j=zck#yWM?EcF?C^trGtp)L>)zgIK!iwHuj6_M*$cAdw(~$%13VKvEA*vd%Vn7E#grSQ~q)tDYjFz%4Zq*;3#*%L}t)J%!HjLqL zs1Ge&q&)+9o@YSQFijv4Alr^}G6HcnKl4!M8BN0^4@ImN9!W=R<~2=bIw>JaO1E(~ zrAZQIj7VZ29Tp{xfsB^8WHTL%gEMOI0grf`XA+A_tYj$&O8BmBqo_n@`SO;`Xj`_O#Tvn4{KjkDBX&+J#zP;MhK zIJlp(P1qL$kp_ZEmMu;Fd%o%WYpLjl1G$j3P8?U8%f5p=#{s5}3bl__hZ|ugTf05o zKd1P)>1YtDW1(R}i@i|OXdwbFDVR)$yHL143MTf$K9^2!qKgSE6Wx4BQohuabnj1m z#$FXk@OR65VY&xrdxBM$6uE3d851jv$VA>~f zkIN(0&< zi|41OKRzult$wq<)b2lb?Fzd4l6sf_)zM!U^Q&`>cTaxv=Y!g7Z@UjI)!XmA_{kp^ zzx%BF+e=@5z4~o%-{mXU-hbxRH$HfB<%J8Yt;;{G>fggh-nwx6rI&wEj(&0Ym1Fmu ieSP8C^Vh%pcAY} zZ+$AZ@2z(&tK_?Kool@2KC}~K_+Z|`uE6<_ l(Zi{NVUmDcg9;iq{@%pDQw3)GKLC1!!PC{xWt~$(698i!XOaK_ literal 0 HcmV?d00001 diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 63ec0d2e4..c63e75574 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): @@ -97,18 +98,23 @@ class TestImageOps(PillowTestCase): def test_colorize(self): # Test the colorizing function - # Grab test image - i = hopper("L").resize((15, 16)) + # Grab test image (10px black, 256px gradient, 10px white) + im = Image.open("Tests/images/bw_gradient.png") + im = im.convert("L") # Test original 2-color functionality - ImageOps.colorize(i, 'green', 'red') + out_2color = ImageOps.colorize(im, 'red', 'green') - # Test new three color functionality (cyanotype colors) - ImageOps.colorize(i, - (32, 37, 79), - (255, 255, 255), - (35, 52, 121), - 40) + # Test new three color functionality, with midpoint offset + out_3color = ImageOps.colorize(im, 'red', 'green', 'yellow', 100) + + # Assert 2-color + ref_2color = Image.open("Tests/images/bw_gradient_2color.png") + self.assert_image_equal(out_2color, ref_2color) + + # Assert 3-color + ref_3color = Image.open("Tests/images/bw_gradient_3color.png") + self.assert_image_equal(out_3color, ref_3color) if __name__ == '__main__': From 4a6ec5ca72ed832578aeff7b80a5254dc6214801 Mon Sep 17 00:00:00 2001 From: tsennott Date: Sat, 7 Jul 2018 18:19:26 -0700 Subject: [PATCH 6/8] updated colorize to allow optional black/white positions; enhanced tests --- Tests/images/bw_gradient.png | Bin 1926 -> 102 bytes Tests/images/bw_gradient_2color.png | Bin 179 -> 0 bytes Tests/images/bw_gradient_3color.png | Bin 246 -> 0 bytes Tests/test_imageops.py | 84 +++++++++++++++++++--- src/PIL/ImageOps.py | 106 ++++++++++++++++++++-------- 5 files changed, 148 insertions(+), 42 deletions(-) delete mode 100644 Tests/images/bw_gradient_2color.png delete mode 100644 Tests/images/bw_gradient_3color.png diff --git a/Tests/images/bw_gradient.png b/Tests/images/bw_gradient.png index 0b7275dc19cb188d0b1c65334593fe5adc948476..79c921486f8dc91f7c6f6bab36ba8ba302ed9b6f 100644 GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5xHv#$-PijWKuXus#WAE}&f5!)f(#5i%^S-8 px%#v93$e=hZjU`?*}%5|8QZ?gVHD1NS6l`%z|+;wWt~$(69Dsx9MAv& literal 1926 zcmb_dU5MO798WEN9aa&2Qjm~?Qqf6fl592^ZauEqv%AvVo!g~7-#R-pyBlwlNt4{| z-aUx=B2^#zAmWqagCHXKpwb76S_Sb%DiqN-K@cp0FQS4t*^gN3T|Ew$-RxxM_y79+ ze=~D_ZRNy~xd-MHMLE)J)LV*j=zck#yWM?EcF?C^trGtp)L>)zgIK!iwHuj6_M*$cAdw(~$%13VKvEA*vd%Vn7E#grSQ~q)tDYjFz%4Zq*;3#*%L}t)J%!HjLqL zs1Ge&q&)+9o@YSQFijv4Alr^}G6HcnKl4!M8BN0^4@ImN9!W=R<~2=bIw>JaO1E(~ zrAZQIj7VZ29Tp{xfsB^8WHTL%gEMOI0grf`XA+A_tYj$&O8BmBqo_n@`SO;`Xj`_O#Tvn4{KjkDBX&+J#zP;MhK zIJlp(P1qL$kp_ZEmMu;Fd%o%WYpLjl1G$j3P8?U8%f5p=#{s5}3bl__hZ|ugTf05o zKd1P)>1YtDW1(R}i@i|OXdwbFDVR)$yHL143MTf$K9^2!qKgSE6Wx4BQohuabnj1m z#$FXk@OR65VY&xrdxBM$6uE3d851jv$VA>~f zkIN(0&< zi|41OKRzult$wq<)b2lb?Fzd4l6sf_)zM!U^Q&`>cTaxv=Y!g7Z@UjI)!XmA_{kp^ zzx%BF+e=@5z4~o%-{mXU-hbxRH$HfB<%J8Yt;;{G>fggh-nwx6rI&wEj(&0Ym1Fmu ieSP8C^Vh%pcAY} zZ+$AZ@2z(&tK_?Kool@2KC}~K_+Z|`uE6<_ l(Zi{NVUmDcg9;iq{@%pDQw3)GKLC1!!PC{xWt~$(698i!XOaK_ diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index c63e75574..d1be04046 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -98,23 +98,85 @@ class TestImageOps(PillowTestCase): def test_colorize(self): # Test the colorizing function - # Grab test image (10px black, 256px gradient, 10px white) + # Open test image (256px by 10px, black to white) im = Image.open("Tests/images/bw_gradient.png") im = im.convert("L") - # Test original 2-color functionality - out_2color = ImageOps.colorize(im, 'red', 'green') + # Create image with original 2-color functionality + im_2c = ImageOps.colorize(im, 'red', 'green') - # Test new three color functionality, with midpoint offset - out_3color = ImageOps.colorize(im, 'red', 'green', 'yellow', 100) + # Create image with original 2-color functionality with offsets + im_2c_offset = ImageOps.colorize(im, + black='red', + white='green', + blackpoint=50, + whitepoint=200) - # Assert 2-color - ref_2color = Image.open("Tests/images/bw_gradient_2color.png") - self.assert_image_equal(out_2color, ref_2color) + # Create image with new three color functionality with offsets + im_3c_offset = ImageOps.colorize(im, + black='red', + white='green', + mid='blue', + blackpoint=50, + whitepoint=200, + midpoint=100) - # Assert 3-color - ref_3color = Image.open("Tests/images/bw_gradient_3color.png") - self.assert_image_equal(out_3color, ref_3color) + # Define function for approximate equality of tuples + def tuple_approx_equal(actual, target, thresh): + value = True + for i, target in enumerate(target): + value *= (target - thresh <= actual[i] <= target + thresh) + return value + + # Test output image (2-color) + left = (0, 1) + middle = (127, 1) + right = (255, 1) + self.assertTrue(tuple_approx_equal(im_2c.getpixel(left), + (255, 0, 0), thresh=1), + '2-color image black incorrect') + self.assertTrue(tuple_approx_equal(im_2c.getpixel(middle), + (127, 63, 0), thresh=1), + '2-color image mid incorrect') + self.assertTrue(tuple_approx_equal(im_2c.getpixel(right), + (0, 127, 0), thresh=1), + '2-color image white incorrect') + + # Test output image (2-color) with offsets + left = (25, 1) + middle = (125, 1) + right = (225, 1) + self.assertTrue(tuple_approx_equal(im_2c_offset.getpixel(left), + (255, 0, 0), thresh=1), + '2-color image (with offset) black incorrect') + self.assertTrue(tuple_approx_equal(im_2c_offset.getpixel(middle), + (127, 63, 0), thresh=1), + '2-color image (with offset) mid incorrect') + self.assertTrue(tuple_approx_equal(im_2c_offset.getpixel(right), + (0, 127, 0), thresh=1), + '2-color image (with offset) white incorrect') + + # 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.assertTrue(tuple_approx_equal(im_3c_offset.getpixel(left), + (255, 0, 0), thresh=1), + '3-color image (with offset) black incorrect') + self.assertTrue(tuple_approx_equal(im_3c_offset.getpixel(left_middle), + (127, 0, 127), thresh=1), + '3-color image (with offset) low-mid incorrect') + self.assertTrue(tuple_approx_equal(im_3c_offset.getpixel(middle), + (0, 0, 255), thresh=1), + '3-color image (with offset) mid incorrect') + self.assertTrue(tuple_approx_equal(im_3c_offset.getpixel(right_middle), + (0, 63, 127), thresh=1), + '3-color image (with offset) high-mid incorrect') + self.assertTrue(tuple_approx_equal(im_3c_offset.getpixel(right), + (0, 127, 0), thresh=1), + '3-color image (with offset) white incorrect') if __name__ == '__main__': diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index e5ce71060..4e6baf8c9 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -136,57 +136,101 @@ def autocontrast(image, cutoff=0, ignore=None): return _lut(image, lut) -def colorize(image, black, white, mid=None, midpoint=127): +def colorize(image, black, white, mid=None, blackpoint=0, + whitepoint=255, midpoint=127): """ Colorize grayscale image. - 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. If - mid is specified, it uses three color mapping. - The **black** and **white** - arguments should be RGB tuples; optionally you can use - three color mapping by also specifying **mid**, and - optionally, **midpoint** (which is the integer value - in [1, 254] corresponding to where the midpoint color - should be mapped (0 being black and 255 being white). + 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 in [0, 255] corresponding to where the corresponding color + should be mapped. :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 midpoint: the int value in [1, 254] for the mid color. + :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" - assert 1 <= midpoint <= 254 + assert 0 <= whitepoint <= 255 + assert 0 <= blackpoint <= 255 + assert 0 <= midpoint <= 255 + assert blackpoint <= whitepoint + if mid is not None: + assert blackpoint <= midpoint + assert whitepoint >= midpoint # Define colors from arguments black = _color(black, "RGB") + white = _color(white, "RGB") if mid is not None: mid = _color(mid, "RGB") - white = _color(white, "RGB") - # Create the mapping + # Empty lists for the mapping red = [] green = [] blue = [] - if mid is None: - 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) - else: - range1 = range(0, midpoint) - range2 = range(0, 256 - midpoint) - for i in range1: - red.append(black[0] + i * (mid[0] - black[0]) // len(range1)) - green.append(black[1] + i * (mid[1] - black[1]) // len(range1)) - blue.append(black[2] + i * (mid[2] - black[2]) // len(range1)) - for i in range2: - red.append(mid[0] + i * (white[0] - mid[0]) // len(range2)) - green.append(mid[1] + i * (white[1] - mid[1]) // len(range2)) - blue.append(mid[2] + i * (white[2] - mid[2]) // len(range2)) + # Create the mapping (2-color) + if mid is None: + + # Define ranges + range_low = range(0, blackpoint) + range_map = range(0, whitepoint - blackpoint) + range_high = range(0, 256 - whitepoint) + + # Map + for i in range_low: + red.append(black[0]) + green.append(black[1]) + blue.append(black[2]) + 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)) + for i in range_high: + red.append(white[0]) + green.append(white[1]) + blue.append(white[2]) + + # Create the mapping (3-color) + else: + + # Define ranges + range_low = range(0, blackpoint) + range_map1 = range(0, midpoint - blackpoint) + range_map2 = range(0, whitepoint - midpoint) + range_high = range(0, 256 - whitepoint) + + # Map + for i in range_low: + red.append(black[0]) + green.append(black[1]) + blue.append(black[2]) + 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)) + for i in range_high: + 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) From 1eed17c70e4a37039e30c78dfb472af7bc47c667 Mon Sep 17 00:00:00 2001 From: tsennott Date: Sun, 8 Jul 2018 20:09:39 -0700 Subject: [PATCH 7/8] tightened up colorize(); split tests; moved tuple comparison fcn to helper.py --- Tests/helper.py | 9 ++++ Tests/test_imageops.py | 109 ++++++++++++++++++++++------------------- src/PIL/ImageOps.py | 47 ++++++------------ 3 files changed, 83 insertions(+), 82 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 207f497d2..834589723 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -307,6 +307,15 @@ def hopper(mode=None, cache={}): return im.copy() +def tuple_approx_equal(actual, target, threshold): + """Tests if tuple actual has values within threshold from tuple target""" + + value = True + for i, target in enumerate(target): + value *= (target - threshold <= actual[i] <= target + threshold) + return value + + def command_succeeds(cmd): """ Runs the command, which must be a list of strings. Returns True if the diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index d1be04046..07e4dd343 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from helper import unittest, PillowTestCase, hopper, tuple_approx_equal from PIL import ImageOps from PIL import Image @@ -95,87 +95,94 @@ class TestImageOps(PillowTestCase): newimg = ImageOps.scale(i, 0.5) self.assertEqual(newimg.size, (25, 25)) - def test_colorize(self): - # Test the colorizing function + 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_2c = ImageOps.colorize(im, 'red', 'green') - - # Create image with original 2-color functionality with offsets - im_2c_offset = ImageOps.colorize(im, - black='red', - white='green', - blackpoint=50, - whitepoint=200) - - # Create image with new three color functionality with offsets - im_3c_offset = ImageOps.colorize(im, - black='red', - white='green', - mid='blue', - blackpoint=50, - whitepoint=200, - midpoint=100) - - # Define function for approximate equality of tuples - def tuple_approx_equal(actual, target, thresh): - value = True - for i, target in enumerate(target): - value *= (target - thresh <= actual[i] <= target + thresh) - return value + im_test = ImageOps.colorize(im, 'red', 'green') # Test output image (2-color) left = (0, 1) middle = (127, 1) right = (255, 1) - self.assertTrue(tuple_approx_equal(im_2c.getpixel(left), - (255, 0, 0), thresh=1), + self.assertTrue(tuple_approx_equal(im_test.getpixel(left), + (255, 0, 0), threshold=1), '2-color image black incorrect') - self.assertTrue(tuple_approx_equal(im_2c.getpixel(middle), - (127, 63, 0), thresh=1), + self.assertTrue(tuple_approx_equal(im_test.getpixel(middle), + (127, 63, 0), threshold=1), '2-color image mid incorrect') - self.assertTrue(tuple_approx_equal(im_2c.getpixel(right), - (0, 127, 0), thresh=1), + self.assertTrue(tuple_approx_equal(im_test.getpixel(right), + (0, 127, 0), threshold=1), '2-color image white 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 = (125, 1) - right = (225, 1) - self.assertTrue(tuple_approx_equal(im_2c_offset.getpixel(left), - (255, 0, 0), thresh=1), + middle = (75, 1) + right = (125, 1) + self.assertTrue(tuple_approx_equal(im_test.getpixel(left), + (255, 0, 0), threshold=1), '2-color image (with offset) black incorrect') - self.assertTrue(tuple_approx_equal(im_2c_offset.getpixel(middle), - (127, 63, 0), thresh=1), + self.assertTrue(tuple_approx_equal(im_test.getpixel(middle), + (127, 63, 0), threshold=1), '2-color image (with offset) mid incorrect') - self.assertTrue(tuple_approx_equal(im_2c_offset.getpixel(right), - (0, 127, 0), thresh=1), + self.assertTrue(tuple_approx_equal(im_test.getpixel(right), + (0, 127, 0), threshold=1), '2-color image (with offset) white 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.assertTrue(tuple_approx_equal(im_3c_offset.getpixel(left), - (255, 0, 0), thresh=1), + self.assertTrue(tuple_approx_equal(im_test.getpixel(left), + (255, 0, 0), threshold=1), '3-color image (with offset) black incorrect') - self.assertTrue(tuple_approx_equal(im_3c_offset.getpixel(left_middle), - (127, 0, 127), thresh=1), + self.assertTrue(tuple_approx_equal(im_test.getpixel(left_middle), + (127, 0, 127), threshold=1), '3-color image (with offset) low-mid incorrect') - self.assertTrue(tuple_approx_equal(im_3c_offset.getpixel(middle), - (0, 0, 255), thresh=1), + self.assertTrue(tuple_approx_equal(im_test.getpixel(middle), + (0, 0, 255), threshold=1), '3-color image (with offset) mid incorrect') - self.assertTrue(tuple_approx_equal(im_3c_offset.getpixel(right_middle), - (0, 63, 127), thresh=1), + self.assertTrue(tuple_approx_equal(im_test.getpixel(right_middle), + (0, 63, 127), threshold=1), '3-color image (with offset) high-mid incorrect') - self.assertTrue(tuple_approx_equal(im_3c_offset.getpixel(right), - (0, 127, 0), thresh=1), + self.assertTrue(tuple_approx_equal(im_test.getpixel(right), + (0, 127, 0), threshold=1), '3-color image (with offset) white incorrect') diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 4e6baf8c9..0ba7ce069 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -162,13 +162,10 @@ def colorize(image, black, white, mid=None, blackpoint=0, # Initial asserts assert image.mode == "L" - assert 0 <= whitepoint <= 255 - assert 0 <= blackpoint <= 255 - assert 0 <= midpoint <= 255 - assert blackpoint <= whitepoint - if mid is not None: - assert blackpoint <= midpoint - assert whitepoint >= midpoint + if mid is None: + assert 0 <= blackpoint <= whitepoint <= 255 + else: + assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 # Define colors from arguments black = _color(black, "RGB") @@ -181,42 +178,28 @@ def colorize(image, black, white, mid=None, blackpoint=0, green = [] blue = [] + # 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: - # Define ranges - range_low = range(0, blackpoint) range_map = range(0, whitepoint - blackpoint) - range_high = range(0, 256 - whitepoint) - # Map - for i in range_low: - red.append(black[0]) - green.append(black[1]) - blue.append(black[2]) 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)) - for i in range_high: - red.append(white[0]) - green.append(white[1]) - blue.append(white[2]) # Create the mapping (3-color) else: - # Define ranges - range_low = range(0, blackpoint) range_map1 = range(0, midpoint - blackpoint) range_map2 = range(0, whitepoint - midpoint) - range_high = range(0, 256 - whitepoint) - # Map - for i in range_low: - red.append(black[0]) - green.append(black[1]) - blue.append(black[2]) 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)) @@ -225,10 +208,12 @@ def colorize(image, black, white, mid=None, blackpoint=0, 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)) - for i in range_high: - red.append(white[0]) - green.append(white[1]) - blue.append(white[2]) + + # 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") From 50d6611587424394be1fdd93255b32f96ea7e34f Mon Sep 17 00:00:00 2001 From: tsennott Date: Mon, 9 Jul 2018 07:04:48 -0700 Subject: [PATCH 8/8] moved tuple test to assert method in PillowTestCase; added docs --- Tests/helper.py | 19 ++++----- Tests/test_imageops.py | 79 +++++++++++++++++++++---------------- docs/releasenotes/5.3.0.rst | 39 ++++++++++++++++++ docs/releasenotes/index.rst | 1 + src/PIL/ImageOps.py | 5 ++- 5 files changed, 98 insertions(+), 45 deletions(-) create mode 100644 docs/releasenotes/5.3.0.rst diff --git a/Tests/helper.py b/Tests/helper.py index 834589723..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 @@ -307,15 +317,6 @@ def hopper(mode=None, cache={}): return im.copy() -def tuple_approx_equal(actual, target, threshold): - """Tests if tuple actual has values within threshold from tuple target""" - - value = True - for i, target in enumerate(target): - value *= (target - threshold <= actual[i] <= target + threshold) - return value - - def command_succeeds(cmd): """ Runs the command, which must be a list of strings. Returns True if the diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 07e4dd343..9c4da2463 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper, tuple_approx_equal +from helper import unittest, PillowTestCase, hopper from PIL import ImageOps from PIL import Image @@ -109,15 +109,18 @@ class TestImageOps(PillowTestCase): left = (0, 1) middle = (127, 1) right = (255, 1) - self.assertTrue(tuple_approx_equal(im_test.getpixel(left), - (255, 0, 0), threshold=1), - '2-color image black incorrect') - self.assertTrue(tuple_approx_equal(im_test.getpixel(middle), - (127, 63, 0), threshold=1), - '2-color image mid incorrect') - self.assertTrue(tuple_approx_equal(im_test.getpixel(right), - (0, 127, 0), threshold=1), - '2-color image white incorrect') + 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 @@ -137,15 +140,18 @@ class TestImageOps(PillowTestCase): left = (25, 1) middle = (75, 1) right = (125, 1) - self.assertTrue(tuple_approx_equal(im_test.getpixel(left), - (255, 0, 0), threshold=1), - '2-color image (with offset) black incorrect') - self.assertTrue(tuple_approx_equal(im_test.getpixel(middle), - (127, 63, 0), threshold=1), - '2-color image (with offset) mid incorrect') - self.assertTrue(tuple_approx_equal(im_test.getpixel(right), - (0, 127, 0), threshold=1), - '2-color image (with offset) white incorrect') + 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 @@ -169,21 +175,26 @@ class TestImageOps(PillowTestCase): middle = (100, 1) right_middle = (150, 1) right = (225, 1) - self.assertTrue(tuple_approx_equal(im_test.getpixel(left), - (255, 0, 0), threshold=1), - '3-color image (with offset) black incorrect') - self.assertTrue(tuple_approx_equal(im_test.getpixel(left_middle), - (127, 0, 127), threshold=1), - '3-color image (with offset) low-mid incorrect') - self.assertTrue(tuple_approx_equal(im_test.getpixel(middle), - (0, 0, 255), threshold=1), - '3-color image (with offset) mid incorrect') - self.assertTrue(tuple_approx_equal(im_test.getpixel(right_middle), - (0, 63, 127), threshold=1), - '3-color image (with offset) high-mid incorrect') - self.assertTrue(tuple_approx_equal(im_test.getpixel(right), - (0, 127, 0), threshold=1), - '3-color image (with offset) white incorrect') + 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__': 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 0ba7ce069..9b470062a 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -147,8 +147,9 @@ def colorize(image, black, white, mid=None, blackpoint=0, 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 in [0, 255] corresponding to where the corresponding color - should be mapped. + 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.