Skip to content

Commit 7ef32e6

Browse files
committed
Correct ImageResize
1 parent 67095ff commit 7ef32e6

File tree

2 files changed

+120
-81
lines changed

2 files changed

+120
-81
lines changed

mathics/builtin/drawing/image.py

Lines changed: 28 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
pixels_as_float,
5252
pixels_as_ubyte,
5353
pixels_as_uint,
54+
resize_width_height,
5455
)
5556

5657
SymbolColorQuantize = Symbol("ColorQuantize")
@@ -85,7 +86,7 @@
8586
from io import BytesIO
8687

8788
# The following classes are used to allow inclusion of
88-
# Buultin Functions only when certain Python packages
89+
# Builtin Functions only when certain Python packages
8990
# are available. They do this by setting the `requires` class variable.
9091

9192

@@ -399,7 +400,6 @@ def eval(self, minval, maxval, w, h, evaluation, options):
399400

400401
class ImageResize(_ImageBuiltin):
401402
"""
402-
403403
<url>:WMA link:https://reference.wolfram.com/language/ref/ImageResize.html</url>
404404
405405
<dl>
@@ -410,38 +410,37 @@ class ImageResize(_ImageBuiltin):
410410
<dd>
411411
</dl>
412412
413-
S> ein = Import["ExampleData/Einstein.jpg"]
414-
= -Image-
415-
416-
S> ImageDimensions[ein]
417-
= {615, 768}
418-
S> ImageResize[ein, {400, 600}]
419-
= -Image-
420-
S> ImageDimensions[%]
421-
= {400, 600}
422-
423-
S> ImageResize[ein, {256}]
424-
= -Image-
425-
426-
S> ImageDimensions[%]
427-
= {256, 256}
428-
429413
The Resampling option can be used to specify how to resample the image. Options are:
430414
<ul>
431415
<li>Automatic
432416
<li>Bicubic
433-
<li>Gaussian
417+
<li>Bilinear
418+
<li>Box
419+
<li>Hamming
420+
<li>Lanczos
434421
<li>Nearest
435422
</ul>
436423
437-
The default sampling method is Bicubic.
424+
See <url>
425+
:Pillow Filters:
426+
https://pillow.readthedocs.io/en/stable/handbook/concepts.html#filters</url>\
427+
for a description of these.
438428
439-
S> ImageResize[ein, 256, Resampling -> "Bicubic"]
429+
S> alice = Import["ExampleData/MadTeaParty.gif"]
440430
= -Image-
441431
442-
S> ImageResize[ein, 256, Resampling -> "Gaussian"]
443-
= ...
444-
: ...
432+
S> shape = ImageDimensions[alice]
433+
= {640, 487}
434+
435+
S> ImageResize[alice, shape / 2]
436+
= -Image-
437+
438+
The default sampling method is "Bicubic" which has pretty good upscaling \
439+
and downscaling quality. However "Box" is the fastest:
440+
441+
442+
S> ImageResize[alice, shape / 2, Resampling -> "Box"]
443+
= -Image-
445444
"""
446445

447446
messages = {
@@ -480,7 +479,7 @@ def eval_resize_width_height(self, image, width, height, evaluation, options):
480479
):
481480
resampling_name = "Bicubic"
482481
else:
483-
resampling_name = resampling.get_string_value()
482+
resampling_name = resampling.value
484483

485484
# find new size
486485
old_w, old_h = image.pixels.shape[1], image.pixels.shape[0]
@@ -507,66 +506,14 @@ def eval_resize_width_height(self, image, width, height, evaluation, options):
507506
h, w = int(round(h)), int(round(w))
508507

509508
# perform the resize
510-
if resampling_name == "Nearest":
511-
return image.filter(
512-
lambda im: im.resize((w, h), resample=PIL.Image.NEAREST)
513-
)
514-
elif resampling_name == "Bicubic":
515-
# After Python 3.6 support is dropped, this can be simplified
516-
# to for Pillow 9+ and use PIL.Image.Resampling.BICUBIC only.
517-
bicubic = (
518-
PIL.Image.Resampling.BICUBIC
519-
if hasattr(PIL.Image, "Resampling")
520-
else PIL.Image.BICUBIC
521-
)
522-
return image.filter(lambda im: im.resize((w, h), resample=bicubic))
523-
elif resampling_name != "Gaussian":
524-
return evaluation.message("ImageResize", "imgrsm", resampling)
525-
526-
try:
527-
from skimage import __version__ as skimage_version, transform
528-
529-
multichannel = image.pixels.ndim == 3
530-
531-
sy = h / old_h
532-
sx = w / old_w
533-
if sy > sx:
534-
err = abs((sy * old_w) - (sx * old_w))
535-
s = sy
536-
else:
537-
err = abs((sy * old_h) - (sx * old_h))
538-
s = sx
539-
if err > 1.5:
540-
# TODO overcome this limitation
541-
return evaluation.message("ImageResize", "gaussaspect")
542-
elif s > 1:
543-
pixels = transform.pyramid_expand(
544-
image.pixels, upscale=s, multichannel=multichannel
545-
).clip(0, 1)
546-
else:
547-
kwargs = {"downscale": (1.0 / s)}
548-
# scikit_image in version 0.19 changes the resize parameter deprecating
549-
# "multichannel". scikit_image also doesn't support older Pythons like 3.6.15.
550-
# If we drop suport for 3.6 we can probably remove
551-
if skimage_version >= "0.19":
552-
# Not totally sure that we want channel_axis=1, but it makes the
553-
# test work. multichannel is deprecated in scikit-image-19.2
554-
# Previously we used multichannel (=3)
555-
# as in the above s > 1 case.
556-
kwargs["channel_axis"] = 2
557-
else:
558-
kwargs["multichannel"] = multichannel
559-
560-
pixels = transform.pyramid_reduce(image.pixels, **kwargs).clip(0, 1)
561-
562-
return Image(pixels, image.color_space)
563-
except ImportError:
564-
evaluation.message("ImageResize", "skimage")
509+
return resize_width_height(image, w, h, resampling_name, evaluation)
565510

566511

567512
class ImageReflect(_ImageBuiltin):
568513
"""
569-
<url>:WMA link:https://reference.wolfram.com/language/ref/ImageReflect.html</url>
514+
<url>
515+
:WMA link:
516+
https://reference.wolfram.com/language/ref/ImageReflect.html</url>
570517
<dl>
571518
<dt>'ImageReflect[$image$]'
572519
<dd>Flips $image$ top to bottom.

mathics/eval/image.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import List, Optional
99

1010
import numpy
11+
import PIL
1112

1213
from mathics.builtin.base import String
1314
from mathics.core.atoms import Rational
@@ -34,6 +35,27 @@
3435
40963: "PixelYDimension",
3536
}
3637

38+
# After Python 3.6 support is dropped, this can be simplified
39+
# to for Pillow 9+ and use PIL.Image.Resampling only.
40+
if hasattr(PIL.Image, "Resampling"):
41+
pil_resize = PIL.Image.Resampling
42+
else:
43+
pil_resize = PIL.Image
44+
45+
# See:
46+
# https://pillow.readthedocs.io/en/stable/handbook/concepts.html#filters
47+
# For a list and comparison of the filters.
48+
49+
resampling_names2PIL = {
50+
"Automatic": getattr(pil_resize, "BICUBIC"),
51+
"Bicubic": getattr(pil_resize, "BICUBIC"),
52+
"Bilinear": getattr(pil_resize, "BILINEAR"),
53+
"Box": getattr(pil_resize, "BOX"),
54+
"Hamming": getattr(pil_resize, "HAMMING"),
55+
"Lanczos": getattr(pil_resize, "LANCZOS"),
56+
"Nearest": getattr(pil_resize, "NEAREST"),
57+
}
58+
3759

3860
def convolve(in1, in2, fixed=True):
3961
"""
@@ -204,3 +226,73 @@ def pixels_as_uint(pixels):
204226
return pixels.astype(numpy.uint8) * 65535
205227
else:
206228
raise NotImplementedError
229+
230+
231+
def resize_width_height(
232+
image, width, height, resampling_name: str, evaluation: Evaluation
233+
):
234+
"""
235+
workhorse part of ImageResize[] after mathic options have been processed.
236+
"""
237+
from mathics.builtin.drawing.image import Image
238+
239+
if resampling_name not in resampling_names2PIL.keys():
240+
return evaluation.message("ImageResize", "imgrsm", resampling_name)
241+
resample = resampling_names2PIL[resampling_name]
242+
243+
# perform the resize
244+
if hasattr(image, "pillow"):
245+
if resampling_name not in resampling_names2PIL.keys():
246+
return evaluation.message("ImageResize", "imgrsm", resampling_name)
247+
pillow = image.pillow.resize(size=(width, height), resample=resample)
248+
pixels = numpy.asarray(pillow)
249+
return Image(pixels, image.color_space, pillow=pillow)
250+
251+
return image.filter(lambda im: im.resize((width, height), resample=resample))
252+
253+
# The Below code is hand-crapted Guassian resampling code, which is what
254+
# WMA does. For now, are going to punt on this, and we use PIL methods only.
255+
256+
# Gaussian need sto unrounded values to compute scaling ratios.
257+
# round to closest pixel for other methods.
258+
259+
# h, w = int(round(height)), int(round(width))
260+
# try:
261+
# from skimage import __version__ as skimage_version, transform
262+
263+
# multichannel = image.pixels.ndim == 3
264+
265+
# sy = height / old_h
266+
# sx = width / old_w
267+
# if sy > sx:
268+
# err = abs((sy * old_w) - (sx * old_w))
269+
# s = sy
270+
# else:
271+
# err = abs((sy * old_h) - (sx * old_h))
272+
# s = sx
273+
# if err > 1.5:
274+
# # TODO overcome this limitation
275+
# return evaluation.message("ImageResize", "gaussaspect")
276+
# elif s > 1:
277+
# pixels = transform.pyramid_expand(
278+
# image.pixels, upscale=s, multichannel=multichannel
279+
# ).clip(0, 1)
280+
# else:
281+
# kwargs = {"downscale": (1.0 / s)}
282+
# # scikit_image in version 0.19 changes the resize parameter deprecating
283+
# # "multichannel". scikit_image also doesn't support older Pythons like 3.6.15.
284+
# # If we drop suport for 3.6 we can probably remove
285+
# if skimage_version >= "0.19":
286+
# # Not totally sure that we want channel_axis=1, but it makes the
287+
# # test work. multichannel is deprecated in scikit-image-19.2
288+
# # Previously we used multichannel (=3)
289+
# # as in the above s > 1 case.
290+
# kwargs["channel_axis"] = 2
291+
# else:
292+
# kwargs["multichannel"] = multichannel
293+
294+
# pixels = transform.pyramid_reduce(image.pixels, **kwargs).clip(0, 1)
295+
296+
# return Image(pixels, image.color_space)
297+
# except ImportError:
298+
# evaluation.message("ImageResize", "skimage")

0 commit comments

Comments
 (0)