mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-08-09 14:54:46 +03:00
Rework handling of awkward image modes and rescaling
Expose Image.use_display_hook_features that can be used to control how images are seen in IPython / Jupyter. For example: Image.use_display_hook_features(F_mode='unit') special cases F mode images, by assuming values are in [0., 1.]. Alternatively, the user could do: Image.use_display_hook_features('auto') and it will function similarly to my previous patch.
This commit is contained in:
parent
8bdb6553a4
commit
f6433362cd
|
@ -138,50 +138,52 @@ class TestImage:
|
|||
assert_image_equal(im, im2, 17)
|
||||
|
||||
# make sure higher bit depths get converted down to 8BPC with warnings
|
||||
high = Image.new("I;16", (100, 100))
|
||||
with pytest.warns(UserWarning):
|
||||
bundle = high._repr_mimebundle_()
|
||||
high = Image.new("I;16", (100, 100), 65535)
|
||||
bundle = high._repr_mimebundle_()
|
||||
with Image.open(io.BytesIO(bundle["image/png"])) as im2:
|
||||
assert im2.format == "PNG"
|
||||
assert_image_equal(im, im2)
|
||||
assert_image_equal(high.convert("I"), im2)
|
||||
|
||||
high = Image.new("F", (100, 100))
|
||||
with pytest.warns(UserWarning):
|
||||
bundle = high._repr_mimebundle_()
|
||||
with Image.open(io.BytesIO(bundle["image/png"])) as im2:
|
||||
assert im2.format == "PNG"
|
||||
assert_image_equal(im, im2)
|
||||
bundle = high._repr_mimebundle_()
|
||||
assert bundle["image/jpeg"] is None and bundle["image/png"] is None
|
||||
|
||||
high = Image.new("I", (100, 100))
|
||||
with pytest.warns(UserWarning):
|
||||
bundle = high._repr_mimebundle_()
|
||||
bundle = high._repr_mimebundle_()
|
||||
with Image.open(io.BytesIO(bundle["image/png"])) as im2:
|
||||
assert im2.format == "PNG"
|
||||
assert_image_equal(im, im2)
|
||||
assert_image_equal(high, im2)
|
||||
|
||||
# make sure large image gets scaled down with a warning
|
||||
im = Image.new("L", [3000, 3000])
|
||||
with pytest.warns(UserWarning):
|
||||
bundle = im._repr_mimebundle_()
|
||||
def test_repr_mimebundle_hooks(self):
|
||||
previous = Image._to_ipython_image
|
||||
try:
|
||||
Image.use_display_hook_features("auto")
|
||||
|
||||
with Image.open(io.BytesIO(bundle["image/png"])) as im2:
|
||||
assert im2.size == (1500, 1500)
|
||||
assert_image_equal(im.resize(im2.size), im2)
|
||||
# fmake sure large image gets scaled down with a warning
|
||||
im = Image.new("L", [3000, 3000])
|
||||
with pytest.warns(UserWarning):
|
||||
bundle = im._repr_mimebundle_()
|
||||
|
||||
# make sure common modes get converted without a warning
|
||||
im = Image.new("LAB", (100, 100))
|
||||
with pytest.warns(None) as record:
|
||||
bundle = im._repr_mimebundle_()
|
||||
assert len(record) == 0
|
||||
with Image.open(io.BytesIO(bundle["image/png"])) as im2:
|
||||
assert_image_equal(im.convert("RGB"), im2)
|
||||
with Image.open(io.BytesIO(bundle["image/png"])) as im2:
|
||||
assert im2.size == (1500, 1500)
|
||||
assert_image_equal(im.resize(im2.size), im2)
|
||||
|
||||
im = Image.new("HSV", (100, 100))
|
||||
with pytest.warns(None) as record:
|
||||
bundle = im._repr_mimebundle_()
|
||||
assert len(record) == 0
|
||||
with Image.open(io.BytesIO(bundle["image/png"])) as im2:
|
||||
assert_image_equal(im.convert("RGB"), im2)
|
||||
# make sure common modes get converted without a warning
|
||||
im = Image.new("LAB", (100, 100))
|
||||
with pytest.warns(None) as record:
|
||||
bundle = im._repr_mimebundle_()
|
||||
assert len(record) == 0
|
||||
with Image.open(io.BytesIO(bundle["image/png"])) as im2:
|
||||
assert_image_equal(im.convert("RGB"), im2)
|
||||
|
||||
im = Image.new("HSV", (100, 100))
|
||||
with pytest.warns(None) as record:
|
||||
bundle = im._repr_mimebundle_()
|
||||
assert len(record) == 0
|
||||
with Image.open(io.BytesIO(bundle["image/png"])) as im2:
|
||||
assert_image_equal(im.convert("RGB"), im2)
|
||||
finally:
|
||||
Image._to_ipython_image = previous
|
||||
|
||||
def test_open_formats(self):
|
||||
PNGFILE = "Tests/images/hopper.png"
|
||||
|
|
162
src/PIL/Image.py
162
src/PIL/Image.py
|
@ -471,41 +471,157 @@ _IPYTHON_MODE_MAP = {
|
|||
|
||||
_VALID_MODES_FOR_FORMAT = {
|
||||
"JPEG": {"L", "RGB", "YCbCr", "CMYK"},
|
||||
"PNG": {"1", "P", "L", "RGB", "RGBA", "LA", "PA"},
|
||||
"PNG": {"1", "P", "L", "RGB", "RGBA", "LA", "PA", "I", "I;16"},
|
||||
}
|
||||
|
||||
|
||||
def _to_ipython_image(image):
|
||||
"""Simplify image to something suitable for display in IPython/Jupyter.
|
||||
Notably, convert to 8BPC and rescale by some integer factor.
|
||||
"""Transform image to something suitable for display in IPython/Jupyter.
|
||||
|
||||
See use_display_hook_features.
|
||||
"""
|
||||
if image.mode in {"I", "F"}:
|
||||
warnings.warn(
|
||||
"image mode doesn't have well defined min/max value, using extrema"
|
||||
)
|
||||
return image
|
||||
|
||||
|
||||
def use_display_hook_features(
|
||||
default=None,
|
||||
*,
|
||||
I_mode=None,
|
||||
F_mode=None,
|
||||
I16_mode=None,
|
||||
resize_threshold=None,
|
||||
mode_map=None,
|
||||
):
|
||||
"""Set IPython/Jupyter display hook up based on parameters.
|
||||
|
||||
:param default: setting to use when unspecified
|
||||
:param I_mode: what to do with I mode images
|
||||
:param F_mode: what to do with F mode images
|
||||
:param I16_mode: what to do with F mode images
|
||||
:param resize_threshold: images with width or height much
|
||||
larger than this will be resized
|
||||
:param mode_map: a dictionary like _IPYTHON_MODE_MAP
|
||||
|
||||
when modes are set luminance values will be linearly rescaled such that;
|
||||
'extrema': darkest value to black, brightest to white
|
||||
'scale max': 0 value to black, brightest to white
|
||||
'unit': 0 to black, 1 to white
|
||||
'assume i16': 0 to black, 2^16-1 to white
|
||||
'assume i32': 0 to black, 2^32-1 to white
|
||||
|
||||
All parameters can be specified as 'auto', which will cause the following
|
||||
settings to be used:
|
||||
I_mode, F_mode = 'scale_max'
|
||||
I16_mode = 'assume i16'
|
||||
resize_threshold = IPYTHON_RESIZE_THRESHOLD
|
||||
mode_map = _IPYTHON_MODE_MAP
|
||||
"""
|
||||
|
||||
def identity(image):
|
||||
"leave the image as is"
|
||||
return image
|
||||
|
||||
def extrema_to_L(image):
|
||||
# linearly transform extrema to fit in [0, 255]
|
||||
# this should have a similar result as Image.histogram
|
||||
lo, hi = image.getextrema()
|
||||
scale = 256 / (hi - lo) if lo != hi else 1
|
||||
image = image.point(lambda e: (e - lo) * scale).convert("L")
|
||||
elif image.mode == "I;16":
|
||||
warnings.warn("converting 16BPC image to 8BPC for display in IPython/Jupyter")
|
||||
# linearly transform max down to 255
|
||||
image = image.point(lambda e: e / 256).convert("L")
|
||||
|
||||
# shrink large images so they don't take too long to transfer/render
|
||||
factor = max(image.size) // IPYTHON_RESIZE_THRESHOLD
|
||||
if factor > 1:
|
||||
warnings.warn(
|
||||
"scaling large image down to improve performance in IPython/Jupyter"
|
||||
f"converting image to 8BPC using black={lo}, white={hi} for display"
|
||||
)
|
||||
image = image.reduce(factor)
|
||||
scale = 256 / (hi - lo) if lo != hi else 1
|
||||
return image.point(lambda e: (e - lo) * scale).convert("L")
|
||||
|
||||
# process remaining modes into things supported by writers
|
||||
if image.mode in _IPYTHON_MODE_MAP:
|
||||
image = image.convert(_IPYTHON_MODE_MAP[image.mode])
|
||||
def scale_max_to_L(image):
|
||||
_, hi = image.getextrema()
|
||||
warnings.warn(f"converting image to 8BPC using black=0, white={hi} for display")
|
||||
if hi > 0:
|
||||
scale = 256 / hi
|
||||
return image.point(lambda e: e * scale).convert("L")
|
||||
return image
|
||||
|
||||
return image
|
||||
def unit_to_L(image):
|
||||
warnings.warn("converting 8BPC for display, with range [0, 1]")
|
||||
return image.point(lambda e: e * 255).convert("L")
|
||||
|
||||
def I16_to_L(image):
|
||||
warnings.warn("converting 16BPC image to 8BPC for display")
|
||||
# linearly transform max down to 255
|
||||
return image.point(lambda e: e / 256).convert("L")
|
||||
|
||||
def I32_to_L(image):
|
||||
warnings.warn("converting 32BPC image to 8BPC for display")
|
||||
return image.point(lambda e: e / 2**24).convert("L")
|
||||
|
||||
def hook_for_mode(value, auto):
|
||||
if value is None:
|
||||
return identity
|
||||
if value in "auto":
|
||||
return auto
|
||||
if value == "extrema":
|
||||
return extrema_to_L
|
||||
if value == "scale max":
|
||||
return scale_max_to_L
|
||||
if value == "unit":
|
||||
return unit_to_L
|
||||
if value == "assume i16":
|
||||
return I16_to_L
|
||||
if value == "assume i32":
|
||||
return I32_to_L
|
||||
assert isinstance(value, Callable)
|
||||
return value
|
||||
|
||||
def threshold_resize(image):
|
||||
# smaller images are quicker to process
|
||||
factor = max(image.size) // resize_threshold
|
||||
if factor > 1:
|
||||
warnings.warn(
|
||||
"scaling large image down to improve performance in IPython/Jupyter"
|
||||
)
|
||||
return image.reduce(factor)
|
||||
return image
|
||||
|
||||
# decode arguments
|
||||
handle_I = hook_for_mode(I_mode or default, scale_max_to_L)
|
||||
handle_F = hook_for_mode(F_mode or default, scale_max_to_L)
|
||||
handle_I16 = hook_for_mode(I16_mode or default, I16_to_L)
|
||||
|
||||
resize_threshold = resize_threshold or default
|
||||
if resize_threshold is None:
|
||||
resize_image = identity
|
||||
else:
|
||||
if resize_threshold == "auto":
|
||||
resize_threshold = IPYTHON_RESIZE_THRESHOLD
|
||||
assert isinstance(resize_threshold, int)
|
||||
resize_image = threshold_resize
|
||||
|
||||
mode_map = mode_map or default
|
||||
if mode_map == "auto":
|
||||
mode_map = _IPYTHON_MODE_MAP
|
||||
elif mode_map is None:
|
||||
mode_map = {}
|
||||
else:
|
||||
assert isinstance(mode_map, dict)
|
||||
|
||||
# define our hook
|
||||
def fn(image):
|
||||
if image.mode == "I":
|
||||
image = handle_I(image)
|
||||
elif image.mode == "F":
|
||||
image = handle_F(image)
|
||||
elif image.mode == "I;16":
|
||||
image = handle_I16(image)
|
||||
|
||||
image = resize_image(image)
|
||||
|
||||
# process remaining modes into things supported by writers
|
||||
if image.mode in mode_map:
|
||||
image = image.convert(mode_map[image.mode])
|
||||
|
||||
return image
|
||||
|
||||
# install it
|
||||
global _to_ipython_image
|
||||
_to_ipython_image = fn
|
||||
|
||||
|
||||
def _encode_ipython_image(image, image_format):
|
||||
|
|
Loading…
Reference in New Issue
Block a user