Skip to content

Commit

Permalink
Merge pull request #3242 from tsennott/feature-imageops-colorize-thre…
Browse files Browse the repository at this point in the history
…e-color

Add feature: ImageOps.colorize to support three colors
  • Loading branch information
hugovk authored Jul 11, 2018
2 parents 937443f + 50d6611 commit 6652f7d
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 9 deletions.
10 changes: 10 additions & 0 deletions Tests/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@ def assert_all_same(self, items, msg=None):
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
Expand Down
Binary file added Tests/images/bw_gradient.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
102 changes: 102 additions & 0 deletions Tests/test_imageops.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from helper import unittest, PillowTestCase, hopper

from PIL import ImageOps
from PIL import Image


class TestImageOps(PillowTestCase):
Expand Down Expand Up @@ -94,6 +95,107 @@ def test_scale(self):
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()
39 changes: 39 additions & 0 deletions docs/releasenotes/5.3.0.rst
Original file line number Diff line number Diff line change
@@ -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
=============

1 change: 1 addition & 0 deletions docs/releasenotes/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Release Notes
.. toctree::
:maxdepth: 2

5.3.0
5.2.0
5.1.0
5.0.0
Expand Down
77 changes: 68 additions & 9 deletions src/PIL/ImageOps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit 6652f7d

Please sign in to comment.