Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add preserve_tone option to autocontrast #5350

Merged
merged 16 commits into from Mar 29, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 42 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,43 @@ def test_auto_contrast_mask_real_input():
threshold=2,
msg="autocontrast without mask pixel incorrect",
)


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 list histogram entries equal
elejke marked this conversation as resolved.
Show resolved Hide resolved
# and should be 10% of the image pixels (+-, because integers)
elejke marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -73,6 +73,15 @@ be specified through a keyword argument::

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


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

The default behaviour of the :py:meth:`~PIL.ImageOps.autocontrast` is to normalize
elejke marked this conversation as resolved.
Show resolved Hide resolved
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.
elejke marked this conversation as resolved.
Show resolved Hide resolved

.. 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