Skip to content

Commit

Permalink
Merge pull request #5350 from elejke/master
Browse files Browse the repository at this point in the history
Add preserve_tone option to autocontrast
  • Loading branch information
radarhere committed Mar 29, 2021
2 parents 6eae8fd + b855d75 commit b0b4fee
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 3 deletions.
52 changes: 51 additions & 1 deletion Tests/test_imageops.py
Expand Up @@ -29,6 +29,7 @@ def test_sanity():
ImageOps.autocontrast(hopper("L"), cutoff=(2, 10))
ImageOps.autocontrast(hopper("L"), ignore=[0, 255])
ImageOps.autocontrast(hopper("L"), mask=hopper("L"))
ImageOps.autocontrast(hopper("L"), preserve_tone=True)

ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255))
ImageOps.colorize(hopper("L"), "black", "white")
Expand Down Expand Up @@ -336,7 +337,7 @@ def test_autocontrast_mask_toy_input():
assert ImageStat.Stat(result_nomask).median == [128]


def test_auto_contrast_mask_real_input():
def test_autocontrast_mask_real_input():
# Test the autocontrast with a rectangular mask
with Image.open("Tests/images/iptc.jpg") as img:

Expand All @@ -362,3 +363,52 @@ def test_auto_contrast_mask_real_input():
threshold=2,
msg="autocontrast without mask pixel incorrect",
)


def test_autocontrast_preserve_tone():
def autocontrast(mode, preserve_tone):
im = hopper(mode)
return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram()

assert autocontrast("RGB", True) != autocontrast("RGB", False)
assert autocontrast("L", True) == autocontrast("L", False)


def test_autocontrast_preserve_gradient():
gradient = Image.linear_gradient("L")

# test with a grayscale gradient that extends to 0,255.
# Should be a noop.
out = ImageOps.autocontrast(gradient, cutoff=0, preserve_tone=True)

assert_image_equal(gradient, out)

# cutoff the top and bottom
# autocontrast should make the first and last histogram entries equal
# and, with rounding, should be 10% of the image pixels
out = ImageOps.autocontrast(gradient, cutoff=10, preserve_tone=True)
hist = out.histogram()
assert hist[0] == hist[-1]
assert hist[-1] == 256 * round(256 * 0.10)

# in rgb
img = gradient.convert("RGB")
out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True)
assert_image_equal(img, out)


@pytest.mark.parametrize(
"color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0))
)
def test_autocontrast_preserve_one_color(color):
img = Image.new("RGB", (10, 10), color)

# single color images shouldn't change
out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True)
assert_image_equal(img, out) # single color, no cutoff

# even if there is a cutoff
out = ImageOps.autocontrast(
img, cutoff=10, preserve_tone=True
) # single color 10 cutoff
assert_image_equal(img, out)
9 changes: 9 additions & 0 deletions docs/releasenotes/8.2.0.rst
Expand Up @@ -83,6 +83,15 @@ be specified through a keyword argument::

im.save("out.tif", icc_profile=...)


ImageOps.autocontrast: preserve_tone
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The default behaviour of :py:meth:`~PIL.ImageOps.autocontrast` is to normalize
separate histograms for each color channel, changing the tone of the image. The new
``preserve_tone`` argument keeps the tone unchanged by using one luminance histogram
for all channels.

Security
========

Expand Down
12 changes: 10 additions & 2 deletions src/PIL/ImageOps.py
Expand Up @@ -61,7 +61,7 @@ def _lut(image, lut):
# actions


def autocontrast(image, cutoff=0, ignore=None, mask=None):
def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False):
"""
Maximize (normalize) image contrast. This function calculates a
histogram of the input image (or mask region), removes ``cutoff`` percent of the
Expand All @@ -77,9 +77,17 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None):
:param mask: Histogram used in contrast operation is computed using pixels
within the mask. If no mask is given the entire image is used
for histogram computation.
:param preserve_tone: Preserve image tone in Photoshop-like style autocontrast.
.. versionadded:: 8.2.0
:return: An image.
"""
histogram = image.histogram(mask)
if preserve_tone:
histogram = image.convert("L").histogram(mask)
else:
histogram = image.histogram(mask)

lut = []
for layer in range(0, len(histogram), 256):
h = histogram[layer : layer + 256]
Expand Down

0 comments on commit b0b4fee

Please sign in to comment.