diff --git a/mathics/autoload/formats/Image/Export.m b/mathics/autoload/formats/Image/Export.m new file mode 100644 index 0000000000..425834e4af --- /dev/null +++ b/mathics/autoload/formats/Image/Export.m @@ -0,0 +1,14 @@ +(* Image Exporter *) + +Begin["System`Convert`Image`"] + +RegisterImageExport[type_] := RegisterExport[ + type, + System`ImageExport, + Options -> {}, + BinaryFormat -> True +]; + +RegisterImageExport[#]& /@ {"BMP", "GIF", "JPEG2000", "JPEG", "PCX", "PNG", "PPM", "PBM", "PGM", "TIFF"}; + +End[] diff --git a/mathics/autoload/formats/Image/Import.m b/mathics/autoload/formats/Image/Import.m new file mode 100644 index 0000000000..176e5ac301 --- /dev/null +++ b/mathics/autoload/formats/Image/Import.m @@ -0,0 +1,16 @@ +(* Image Importer *) + +Begin["System`Convert`Image`"] + +RegisterImageImport[type_] := RegisterImport[ + type, + System`ImageImport, + {}, + AvailableElements -> {"Image"}, + DefaultElement -> "Image", + FunctionChannels -> {"FileNames"} +]; + +RegisterImageImport[#]& /@ {"BMP", "GIF", "JPEG2000", "JPEG", "PCX", "PNG", "PPM", "PBM", "PGM", "TIFF", "ICO", "TGA"}; + +End[] diff --git a/mathics/builtin/__init__.py b/mathics/builtin/__init__.py index 67e822f493..ae61814af6 100644 --- a/mathics/builtin/__init__.py +++ b/mathics/builtin/__init__.py @@ -7,7 +7,7 @@ from mathics.builtin import ( algebra, arithmetic, assignment, attributes, calculus, combinatorial, comparison, control, datentime, diffeqns, evaluation, exptrig, functional, - graphics, graphics3d, inout, integer, linalg, lists, logic, numbertheory, + graphics, graphics3d, image, inout, integer, linalg, lists, logic, numbertheory, numeric, options, patterns, plot, physchemdata, randomnumbers, recurrence, specialfunctions, scoping, strings, structure, system, tensors) @@ -19,7 +19,7 @@ modules = [ algebra, arithmetic, assignment, attributes, calculus, combinatorial, comparison, control, datentime, diffeqns, evaluation, exptrig, functional, - graphics, graphics3d, inout, integer, linalg, lists, logic, numbertheory, + graphics, graphics3d, image, inout, integer, linalg, lists, logic, numbertheory, numeric, options, patterns, plot, physchemdata, randomnumbers, recurrence, specialfunctions, scoping, strings, structure, system, tensors] diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py index 7c9253bf1a..5c5893984a 100644 --- a/mathics/builtin/base.py +++ b/mathics/builtin/base.py @@ -147,11 +147,13 @@ def contextify_form_name(f): makeboxes_def.add_rule(rule) @classmethod - def get_name(cls): + def get_name(cls, short=False): if cls.name is None: shortname = cls.__name__ else: shortname = cls.name + if short: + return shortname return cls.context + shortname def get_operator(self): diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py new file mode 100644 index 0000000000..9a1cf8ee0e --- /dev/null +++ b/mathics/builtin/image.py @@ -0,0 +1,1422 @@ +''' +Image[] and image related functions. + +Note that you (currently) need scikit-image installed in order for this module to work. +''' + +from __future__ import division + +from mathics.builtin.base import ( + Builtin, AtomBuiltin, Test, BoxConstruct, String) +from mathics.core.expression import ( + Atom, Expression, Integer, Rational, Real, Symbol, from_python) + +import six +import base64 +import functools +import math + +_image_requires = ( + 'skimage', + 'warnings', + 'numpy', + 'scipy', + 'PIL', + 'matplotlib', +) + +try: + import skimage + import skimage.io + import skimage.transform + import skimage.filters + import skimage.exposure + import skimage.feature + import skimage.filters.rank + import skimage.morphology + import skimage.measure + + import warnings + + import PIL + import PIL.ImageEnhance + import PIL.ImageOps + import PIL.ImageFilter + + import numpy + + import matplotlib.cm + + _enabled = True +except ImportError: + _enabled = False + +from io import BytesIO + + +if _enabled: + _color_space_conversions = { + 'RGB2Grayscale': skimage.color.rgb2gray, + 'Grayscale2RGB': skimage.color.gray2rgb, + + 'HSV2RGB': skimage.color.hsv2rgb, + 'RGB2HSV': skimage.color.rgb2hsv, + + 'LAB2LCH': skimage.color.lab2lch, + 'LCH2LAB': skimage.color.lch2lab, + + 'LAB2RGB': skimage.color.lab2rgb, + 'LAB2XYZ': skimage.color.lab2xyz, + + 'LUV2RGB': skimage.color.luv2rgb, + 'LUV2XYZ': skimage.color.luv2xyz, + + 'RGB2LAB': skimage.color.rgb2lab, + 'RGB2LUV': skimage.color.rgb2luv, + 'RGB2XYZ': skimage.color.rgb2xyz, + + 'XYZ2LAB': skimage.color.xyz2lab, + 'XYZ2LUV': skimage.color.xyz2luv, + 'XYZ2RGB': skimage.color.xyz2rgb, + } + + +class _ImageBuiltin(Builtin): + requires = _image_requires + + +class _ImageTest(Test): + requires = _image_requires + +# import and export + + +class ImageImport(_ImageBuiltin): + """ + ## Image + >> Import["ExampleData/Einstein.jpg"] + = -Image- + #> Import["ExampleData/sunflowers.jpg"] + = -Image- + >> Import["ExampleData/MadTeaParty.gif"] + = -Image- + >> Import["ExampleData/moon.tif"] + = -Image- + #> Import["ExampleData/lena.tif"] + = -Image- + """ + def apply(self, path, evaluation): + '''ImageImport[path_?StringQ]''' + pixels = skimage.io.imread(path.get_string_value()) + is_rgb = len(pixels.shape) >= 3 and pixels.shape[2] >= 3 + atom = Image(pixels, 'RGB' if is_rgb else 'Grayscale') + return Expression('List', Expression('Rule', String('Image'), atom)) + + +class ImageExport(_ImageBuiltin): + messages = { + 'noimage': 'only an Image[] can be exported into an image file' + } + + def apply(self, path, expr, opts, evaluation): + '''ImageExport[path_?StringQ, expr_, opts___]''' + if isinstance(expr, Image): + skimage.io.imsave(path.get_string_value(), expr.pixels) + return Symbol('Null') + else: + return evaluation.message('ImageExport', 'noimage') + + +# image math + + +class _ImageArithmetic(_ImageBuiltin): + messages = { + 'bddarg': 'Expecting a number, image, or graphics instead of `1`.', + } + + @staticmethod + def convert_Image(image): + assert isinstance(image, Image) + return skimage.img_as_float(image.pixels) + + @staticmethod + def convert_args(*args): + images = [] + for arg in args: + if isinstance(arg, Image): + images.append(_ImageArithmetic.convert_Image(arg)) + elif isinstance(arg, (Integer, Rational, Real)): + images.append(float(arg.to_python())) + else: + return None, arg + return images, None + + @staticmethod + def _reduce(iterable, ufunc): + result = None + for i in iterable: + if result is None: + # ufunc is destructive so copy first + result = numpy.copy(i) + else: + # e.g. result *= i + ufunc(result, i, result) + return result + + def apply(self, image, args, evaluation): + '%(name)s[image_Image, args__]' + images, arg = self.convert_args(image, *args.get_sequence()) + if images is None: + return evaluation.message(self.get_name(), 'bddarg', arg) + ufunc = getattr(numpy, self.get_name(True)[5:].lower()) + result = self._reduce(images, ufunc).clip(0, 1) + return Image(result, image.color_space) + + +class ImageAdd(_ImageArithmetic): + ''' +
+
'ImageAdd[$image$, $expr_1$, $expr_2$, ...]' +
adds all $expr_i$ to $image$ where each $expr_i$ must be an image or a real number. +
+ + >> i = Image[{{0, 0.5, 0.2, 0.1, 0.9}, {1.0, 0.1, 0.3, 0.8, 0.6}}]; + + >> ImageAdd[i, 0.5] + = -Image- + + >> ImageAdd[i, i] + = -Image- + + #> ImageAdd[i, 0.2, i, 0.1] + = -Image- + + #> ImageAdd[i, x] + : Expecting a number, image, or graphics instead of x. + = ImageAdd[-Image-, x] + + >> ein = Import["ExampleData/Einstein.jpg"]; + >> noise = RandomImage[{-0.1, 0.1}, ImageDimensions[ein]]; + >> ImageAdd[noise, ein] + = -Image- + + >> lena = Import["ExampleData/lena.tif"]; + >> noise = RandomImage[{-0.2, 0.2}, ImageDimensions[lena], ColorSpace -> "RGB"]; + >> ImageAdd[noise, lena] + = -Image- + ''' + + +class ImageSubtract(_ImageArithmetic): + ''' +
+
'ImageSubtract[$image$, $expr_1$, $expr_2$, ...]' +
subtracts all $expr_i$ from $image$ where each $expr_i$ must be an image or a real number. +
+ + >> i = Image[{{0, 0.5, 0.2, 0.1, 0.9}, {1.0, 0.1, 0.3, 0.8, 0.6}}]; + + >> ImageSubtract[i, 0.2] + = -Image- + + >> ImageSubtract[i, i] + = -Image- + + #> ImageSubtract[i, 0.2, i, 0.1] + = -Image- + + #> ImageSubtract[i, x] + : Expecting a number, image, or graphics instead of x. + = ImageSubtract[-Image-, x] + ''' + + +class ImageMultiply(_ImageArithmetic): + ''' +
+
'ImageMultiply[$image$, $expr_1$, $expr_2$, ...]' +
multiplies all $expr_i$ with $image$ where each $expr_i$ must be an image or a real number. +
+ + >> i = Image[{{0, 0.5, 0.2, 0.1, 0.9}, {1.0, 0.1, 0.3, 0.8, 0.6}}]; + + >> ImageMultiply[i, 0.2] + = -Image- + + >> ImageMultiply[i, i] + = -Image- + + #> ImageMultiply[i, 0.2, i, 0.1] + = -Image- + + #> ImageMultiply[i, x] + : Expecting a number, image, or graphics instead of x. + = ImageMultiply[-Image-, x] + + >> ein = Import["ExampleData/Einstein.jpg"]; + >> noise = RandomImage[{0.7, 1.3}, ImageDimensions[ein]]; + >> ImageMultiply[noise, ein] + = -Image- + ''' + + +class RandomImage(_ImageBuiltin): + ''' +
+
'RandomImage[$max$]' +
creates an image of random pixels with values 0 to $max$. +
'RandomImage[{$min$, $max$}]' +
creates an image of random pixels with values $min$ to $max$. +
'RandomImage[..., $size$]' +
creates an image of the given $size$. +
+ + >> RandomImage[1, {100, 100}] + = -Image- + + #> RandomImage[0.5] + = -Image- + #> RandomImage[{0.1, 0.9}] + = -Image- + #> RandomImage[0.9, {400, 600}] + = -Image- + #> RandomImage[{0.1, 0.5}, {400, 600}] + = -Image- + + #> RandomImage[{0.1, 0.5}, {400, 600}, ColorSpace -> "RGB"] + = -Image- + ''' + + options = { + 'ColorSpace': 'Automatic', + } + + rules = { + 'RandomImage[]': 'RandomImage[{0, 1}, {150, 150}]', + 'RandomImage[max_?RealNumberQ]': 'RandomImage[{0, max}, {150, 150}]', + 'RandomImage[{minval_?RealNumberQ, maxval_?RealNumberQ}]': 'RandomImage[{minval, maxval}, {150, 150}]', + 'RandomImage[max_?RealNumberQ, {w_Integer, h_Integer}]': 'RandomImage[{0, max}, {w, h}]', + } + + messages = { + 'bddim': 'The specified dimension `1` should be a pair of positive integers.', + 'imgcstype': '`1` is an invalid color space specification.', + } + + def apply(self, minval, maxval, w, h, evaluation, options): + 'RandomImage[{minval_?RealNumberQ, maxval_?RealNumberQ}, {w_Integer, h_Integer}, OptionsPattern[RandomImage]]' + color_space = self.get_option(options, 'ColorSpace', evaluation) + if isinstance(color_space, Symbol) and color_space.get_name() == 'System`Automatic': + cs = 'Grayscale' + else: + cs = color_space.get_string_value() + size = [w.get_int_value(), h.get_int_value()] + if size[0] <= 0 or size[1] <= 0: + return evaluation.message('RandomImage', 'bddim', from_python(size)) + minrange, maxrange = minval.get_real_value(), maxval.get_real_value() + + if cs == 'Grayscale': + data = numpy.random.rand(size[1], size[0]) * (maxrange - minrange) + minrange + elif cs == 'RGB': + data = numpy.random.rand(size[1], size[0], 3) * (maxrange - minrange) + minrange + else: + return evaluation.message('RandomImage', 'imgcstype', color_space) + return Image(data, cs) + + +# simple image manipulation + + +class ImageResize(_ImageBuiltin): + ''' +
+
'ImageResize[$image$, $width$]' +
+
'ImageResize[$image$, {$width$, $height$}]' +
+
+ + >> ein = Import["ExampleData/Einstein.jpg"]; + >> ImageDimensions[ein] + = {615, 768} + >> ImageResize[ein, {400, 600}] + = -Image- + #> ImageDimensions[%] + = {400, 600} + + >> ImageResize[ein, 256] + = -Image- + >> ImageDimensions[%] + = {256, 320} + + The default sampling method is Bicubic + >> ImageResize[ein, 256, Resampling -> "Bicubic"] + = -Image- + #> ImageDimensions[%] + = {256, 320} + >> ImageResize[ein, 256, Resampling -> "Nearest"] + = -Image- + #> ImageDimensions[%] + = {256, 320} + >> ImageResize[ein, 256, Resampling -> "Gaussian"] + = -Image- + #> ImageDimensions[%] + = {256, 320} + #> ImageResize[ein, {256, 256}, Resampling -> "Gaussian"] + : Gaussian resampling needs to maintain aspect ratio. + = ImageResize[-Image-, {256, 256}, Resampling -> Gaussian] + #> ImageResize[ein, 256, Resampling -> "Invalid"] + : Invalid resampling method Invalid. + = ImageResize[-Image-, 256, Resampling -> Invalid] + + #> ImageDimensions[ImageResize[ein, {256}]] + = {256, 256} + + #> ImageResize[ein, {x}] + : The size {x} is not a valid image size specification. + = ImageResize[-Image-, {x}] + #> ImageResize[ein, x] + : The size x is not a valid image size specification. + = ImageResize[-Image-, x] + ''' + + options = { + 'Resampling': 'Automatic', + } + + messages = { + 'imgrssz': 'The size `1` is not a valid image size specification.', + 'imgrsm': 'Invalid resampling method `1`.', + 'gaussaspect': 'Gaussian resampling needs to maintain aspect ratio.', + } + + def _get_image_size_spec(self, old_size, new_size): + predefined_sizes = { + 'System`Tiny': 75, + 'System`Small': 150, + 'System`Medium': 300, + 'System`Large': 450, + 'System`Automatic': 0, # placeholder + } + result = new_size.get_real_value() + if result is not None: + result = int(result) + if result <= 0: + return None + return result + + if isinstance(new_size, Symbol): + name = new_size.get_name() + if name == 'System`All': + return old_size + return predefined_sizes.get(name, None) + if new_size.has_form('Scaled', 1): + s = new_size.leaves[0].get_real_value() + if s is None: + return None + return max(1, old_size * s) # handle negative s values silently + return None + + def apply_resize_width(self, image, s, evaluation, options): + 'ImageResize[image_Image, s_, OptionsPattern[ImageResize]]' + old_w = image.pixels.shape[1] + if s.has_form('List', 1): + width = s.leaves[0] + else: + width = s + w = self._get_image_size_spec(old_w, width) + if w is None: + return evaluation.message('ImageResize', 'imgrssz', s) + if s.has_form('List', 1): + height = width + else: + height = Symbol('Automatic') + return self.apply_resize_width_height(image, width, height, evaluation, options) + + def apply_resize_width_height(self, image, width, height, evaluation, options): + 'ImageResize[image_Image, {width_, height_}, OptionsPattern[ImageResize]]' + # resampling method + resampling = self.get_option(options, 'Resampling', evaluation) + if isinstance(resampling, Symbol) and resampling.get_name() == 'System`Automatic': + resampling_name = 'Bicubic' + else: + resampling_name = resampling.get_string_value() + + # find new size + old_w, old_h = image.pixels.shape[1], image.pixels.shape[0] + w = self._get_image_size_spec(old_w, width) + h = self._get_image_size_spec(old_h, height) + if h is None or w is None: + return evaluation.message('ImageResize', 'imgrssz', Expression('List', width, height)) + + # handle Automatic + old_aspect_ratio = old_w / old_h + if w == 0 and h == 0: + # if both width and height are Automatic then use old values + w, h = old_w, old_h + elif w == 0: + w = max(1, h * old_aspect_ratio) + elif h == 0: + h = max(1, w / old_aspect_ratio) + + if resampling_name != 'Gaussian': + # Gaussian need to unrounded values to compute scaling ratios. + # round to closest pixel for other methods. + h, w = int(round(h)), int(round(w)) + + # perform the resize + if resampling_name == 'Nearest': + pixels = skimage.transform.resize(image.pixels, (h, w), order=0) + elif resampling_name == 'Bicubic': + pixels = skimage.transform.resize(image.pixels, (h, w), order=3) + elif resampling_name == 'Gaussian': + sy = h / old_h + sx = w / old_w + if sy > sx: + err = abs((sy * old_w) - (sx * old_w)) + s = sy + else: + err = abs((sy * old_h) - (sx * old_h)) + s = sx + if err > 1.5: + # TODO overcome this limitation + return evaluation.message('ImageResize', 'gaussaspect') + elif s > 1: + pixels = skimage.transform.pyramid_expand(image.pixels, upscale=s).clip(0, 1) + else: + pixels = skimage.transform.pyramid_reduce(image.pixels, downscale=1 / s).clip(0, 1) + else: + return evaluation.message('ImageResize', 'imgrsm', resampling) + + return Image(pixels, image.color_space) + + +class ImageReflect(_ImageBuiltin): + ''' +
+
'ImageReflect[$image$]' +
Flips $image$ top to bottom. +
'ImageReflect[$image$, $side$]' +
Flips $image$ so that $side$ is interchanged with its opposite. +
'ImageReflect[$image$, $side_1$ -> $side_2$]' +
Flips $image$ so that $side_1$ is interchanged with $side_2$. +
+ + >> ein = Import["ExampleData/Einstein.jpg"]; + >> ImageReflect[ein] + = -Image- + >> ImageReflect[ein, Left] + = -Image- + >> ImageReflect[ein, Left -> Top] + = -Image- + + #> ein == ImageReflect[ein, Left -> Left] == ImageReflect[ein, Right -> Right] == ImageReflect[ein, Top -> Top] == ImageReflect[ein, Bottom -> Bottom] + = True + #> ImageReflect[ein, Left -> Right] == ImageReflect[Right -> Left] == ImageReflect[ein, Left] == ImageReflect[ein, Right] + = True + #> ImageReflect[ein, Bottom -> Top] == ImageReflect[ein, Top -> Bottom] == ImageReflect[ein, Top] == ImageReflect[ein, Bottom] + = True + #> ImageReflect[ein, Left -> Top] == ImageReflect[ein, Right -> Bottom] (* Transpose *) + = True + #> ImageReflect[ein, Left -> Bottom] == ImageReflect[ein, Right -> Top] (* Anti-Transpose *) + = True + + #> ImageReflect[ein, x -> Top] + : x -> Top is not a valid 2D reflection specification. + = ImageReflect[-Image-, x -> Top] + ''' + + rules = { + 'ImageReflect[image_Image]': 'ImageReflect[image, Top -> Bottom]', + 'ImageReflect[image_Image, Top|Bottom]': 'ImageReflect[image, Top -> Bottom]', + 'ImageReflect[image_Image, Left|Right]': 'ImageReflect[image, Left -> Right]', + } + + messages = { + 'bdrfl2': '`1` is not a valid 2D reflection specification.', + } + + def apply(self, image, orig, dest, evaluation): + 'ImageReflect[image_Image, Rule[orig_, dest_]]' + if isinstance(orig, Symbol) and isinstance(dest, Symbol): + specs = [orig.get_name(), dest.get_name()] + specs.sort() # `Top -> Bottom` is the same as `Bottom -> Top` + + anti_transpose = lambda i: numpy.flipud(numpy.transpose(numpy.flipud(i))) + no_op = lambda i: i + + method = { + ('System`Bottom', 'System`Top'): numpy.flipud, + ('System`Left', 'System`Right'): numpy.fliplr, + ('System`Left', 'System`Top'): numpy.transpose, + ('System`Right', 'System`Top'): anti_transpose, + ('System`Bottom', 'System`Left'): anti_transpose, + ('System`Bottom', 'System`Right'): numpy.transpose, + ('System`Bottom', 'System`Bottom'): no_op, + ('System`Top', 'System`Top'): no_op, + ('System`Left', 'System`Left'): no_op, + ('System`Right', 'System`Right'): no_op, + }.get(tuple(specs), None) + + if method is None: + return evaluation.message('ImageReflect', 'bdrfl2', Expression('Rule', orig, dest)) + + return Image(method(image.pixels), image.color_space) + + +class ImageRotate(_ImageBuiltin): + ''' +
+
'ImageRotate[$image$]' +
Rotates $image$ 90 degrees counterclockwise. +
'ImageRotate[$image$, $theta$]' +
Rotates $image$ by a given angle $theta$ +
+ + >> ein = Import["ExampleData/Einstein.jpg"]; + + >> ImageRotate[ein] + = -Image- + + >> ImageRotate[ein, 45 Degree] + = -Image- + + >> ImageRotate[ein, Pi / 2] + = -Image- + + #> ImageRotate[ein, ein] + : Angle -Image- should be a real number, one of Top, Bottom, Left, Right, or a rule from one to another. + = ImageRotate[-Image-, -Image-] + ''' + + rules = { + 'ImageRotate[i_Image]': 'ImageRotate[i, 90 Degree]' + } + + messages = { + 'imgang': 'Angle `1` should be a real number, one of Top, Bottom, Left, Right, or a rule from one to another.', + } + + def apply(self, image, angle, evaluation): + 'ImageRotate[image_Image, angle_]' + py_angle = angle.to_python(n_evaluation=evaluation) + if not isinstance(py_angle, six.integer_types + (float,)): + return evaluation.message('ImageRotate', 'imgang', angle) + return Image(skimage.transform.rotate(image.pixels, 180 * py_angle / math.pi, resize=True), image.color_space) + + +class ImagePartition(_ImageBuiltin): + ''' +
+
'ImagePartition[$image$, $s$]' +
Partitions an image into an array of $s$ x $s$ pixel subimages. +
'ImagePartition[$image$, {$w$, $h$}]' +
Partitions an image into an array of $w$ x $h$ pixel subimages. +
+ + >> lena = Import["ExampleData/lena.tif"]; + >> ImageDimensions[lena] + = {512, 512} + >> ImagePartition[lena, 256] + = {{-Image-, -Image-}, {-Image-, -Image-}} + + >> ImagePartition[lena, {512, 128}] + = {{-Image-}, {-Image-}, {-Image-}, {-Image-}} + + #> ImagePartition[lena, 257] + = {{-Image-}} + #> ImagePartition[lena, 512] + = {{-Image-}} + #> ImagePartition[lena, 513] + = {} + #> ImagePartition[lena, {256, 300}] + = {{-Image-, -Image-}} + + #> ImagePartition[lena, {0, 300}] + : {0, 300} is not a valid size specification for image partitions. + = ImagePartition[-Image-, {0, 300}] + ''' + + rules = { + 'ImagePartition[i_Image, s_Integer]': 'ImagePartition[i, {s, s}]' + } + + messages = { + 'arg2': '`1` is not a valid size specification for image partitions.', + } + + def apply(self, image, w, h, evaluation): + 'ImagePartition[image_Image, {w_Integer, h_Integer}]' + w = w.get_int_value() + h = h.get_int_value() + if w <= 0 or h <= 0: + return evaluation.message('ImagePartition', 'arg2', from_python([w, h])) + pixels = image.pixels + shape = pixels.shape + + # drop blocks less than w x h + parts = [] + for yi in range(shape[0] // h): + row = [] + for xi in range(shape[1] // w): + p = pixels[yi * h : (yi + 1) * h, xi * w : (xi + 1) * w] + row.append(Image(p, image.color_space)) + if row: + parts.append(row) + return from_python(parts) + +# simple image filters + + +class ImageAdjust(_ImageBuiltin): + ''' +
+
'ImageAdjust[$image$]' +
adjusts the levels in $image$. +
'ImageAdjust[$image$, $c$]' +
adjusts the contrast in $image$ by $c$. +
'ImageAdjust[$image$, {$c$, $b$}]' +
adjusts the contrast $c$, and brightness $b$ in $image$. +
'ImageAdjust[$image$, {$c$, $b$, $g$}]' +
adjusts the contrast $c$, brightness $b$, and gamma $g$ in $image$. +
+ + >> lena = Import["ExampleData/lena.tif"]; + >> ImageAdjust[lena] + = -Image- + + #> img = Image[{{0.1, 0.5}, {0.5, 0.9}}]; + #> ImageData[ImageAdjust[img]] + = {{0., 0.5}, {0.5, 1.}} + ''' + + rules = { + 'ImageAdjust[image_Image, c_?RealNumberQ]': 'ImageAdjust[image, {c, 0, 1}]', + 'ImageAdjust[image_Image, {c_?RealNumberQ, b_?RealNumberQ}]': 'ImageAdjust[image, {c, b, 1}]', + } + + def apply_auto(self, image, evaluation): + 'ImageAdjust[image_Image]' + pixels = skimage.img_as_float(image.pixels) + + # channel limits + axis = (0, 1) + cmaxs, cmins = pixels.max(axis=axis), pixels.min(axis=axis) + + # normalise channels + scales = cmaxs - cmins + if not scales.shape: + scales = numpy.array([scales]) + scales[scales == 0.0] = 1 + pixels -= cmins + pixels /= scales + return Image(pixels, image.color_space) + + def apply_contrast_brightness_gamma(self, image, c, b, g, evaluation): + 'ImageAdjust[image_Image, {c_?RealNumberQ, b_?RealNumberQ, g_?RealNumberQ}]' + im = image.pil() + + # gamma + g = g.to_python() + if g != 1: + im = PIL.ImageEnhance.Color(im).enhance(g) + + # brightness + b = b.to_python() + if b != 0: + im = PIL.ImageEnhance.Brightness(im).enhance(b + 1) + + # contrast + c = c.to_python() + if c != 0: + im = PIL.ImageEnhance.Contrast(im).enhance(c + 1) + + return Image(numpy.array(im), image.color_space) + + +class Blur(_ImageBuiltin): + rules = { + 'Blur[i_Image]': 'Blur[i, 2]' + } + + def apply(self, image, r, evaluation): + 'Blur[image_Image, r_?RealNumberQ]' + return Image(numpy.array(image.pil().filter( + PIL.ImageFilter.GaussianBlur(r.to_python()))), image.color_space) + + +class Sharpen(_ImageBuiltin): + rules = { + 'Sharpen[i_Image]': 'Sharpen[i, 2]' + } + + def apply(self, image, r, evaluation): + 'Sharpen[image_Image, r_?RealNumberQ]' + return Image(numpy.array(image.pil().filter( + PIL.ImageFilter.UnsharpMask(r.to_python()))), image.color_space) + + +class GaussianFilter(_ImageBuiltin): + messages = { + 'only3': 'GaussianFilter only supports up to three channels.' + } + + def apply_radius(self, image, radius, evaluation): + 'GaussianFilter[image_Image, radius_?RealNumberQ]' + if len(image.pixels.shape) > 2 and image.pixels.shape[2] > 3: + return evaluation.message('GaussianFilter', 'only3') + else: + return Image(skimage.filters.gaussian( + skimage.img_as_float(image.pixels), + sigma=radius.to_python() / 2, multichannel=True), image.color_space) + + +# morphological image filters + + +class PillowImageFilter(_ImageBuiltin): + def compute(self, image, f): + return Image(numpy.array(image.pil().filter(f)), image.color_space) + + +class MinFilter(PillowImageFilter): + def apply(self, image, r, evaluation): + 'MinFilter[image_Image, r_Integer]' + return self.compute(image, PIL.ImageFilter.MinFilter(1 + 2 * r.to_python())) + + +class MaxFilter(PillowImageFilter): + def apply(self, image, r, evaluation): + 'MaxFilter[image_Image, r_Integer]' + return self.compute(image, PIL.ImageFilter.MaxFilter(1 + 2 * r.to_python())) + + +class MedianFilter(PillowImageFilter): + def apply(self, image, r, evaluation): + 'MedianFilter[image_Image, r_Integer]' + return self.compute(image, PIL.ImageFilter.MedianFilter(1 + 2 * r.to_python())) + + +class EdgeDetect(_ImageBuiltin): + rules = { + 'EdgeDetect[i_Image]': 'EdgeDetect[i, 2, 0.2]', + 'EdgeDetect[i_Image, r_?RealNumberQ]': 'EdgeDetect[i, r, 0.2]' + } + + def apply(self, image, r, t, evaluation): + 'EdgeDetect[image_Image, r_?RealNumberQ, t_?RealNumberQ]' + return Image(skimage.feature.canny( + image.grayscale().pixels, sigma=r.to_python() / 2, + low_threshold=0.5 * t.to_python(), high_threshold=t.to_python()), + 'Grayscale') + + +class BoxMatrix(_ImageBuiltin): + def apply(self, r, evaluation): + 'BoxMatrix[r_?RealNumberQ]' + s = 1 + 2 * r.to_python() + return from_python(skimage.morphology.rectangle(s, s).tolist()) + + +class DiskMatrix(_ImageBuiltin): + def apply(self, r, evaluation): + 'DiskMatrix[r_?RealNumberQ]' + return from_python(skimage.morphology.disk(r).tolist()) + + +class DiamondMatrix(_ImageBuiltin): + def apply(self, r, evaluation): + 'DiamondMatrix[r_?RealNumberQ]' + return from_python(skimage.morphology.diamond(r).tolist()) + + +class _MorphologyFilter(_ImageBuiltin): + messages = { + 'grayscale': 'Your image has been converted to grayscale as color images are not supported yet.' + } + + rules = { + '%(name)s[i_Image, r_?RealNumberQ]': '%(name)s[i, BoxMatrix[r]]' + } + + def apply(self, image, k, evaluation): + '%(name)s[image_Image, k_?MatrixQ]' + if image.color_space != 'Grayscale': + image = image.color_convert('Grayscale') + evaluation.message(self.name, 'grayscale') + f = getattr(skimage.morphology, self.get_name(True).lower()) + img = f(image.pixels, numpy.array(k.to_python())) + return Image(img, 'Grayscale') + + +class Dilation(_MorphologyFilter): + ''' +
+
'Dilation[$image$, $ker$]' +
Gives the morphological dilation of $image$ with respect to structuring element $ker$. +
+ + >> ein = Import["ExampleData/Einstein.jpg"]; + >> Dilation[ein, 2.5] + = -Image- + ''' + + +class Erosion(_MorphologyFilter): + ''' +
+
'Erosion[$image$, $ker$]' +
Gives the morphological erosion of $image$ with respect to structuring element $ker$. +
+ + >> ein = Import["ExampleData/Einstein.jpg"]; + >> Erosion[ein, 2.5] + = -Image- + ''' + + +class Opening(_MorphologyFilter): + ''' +
+
'Opening[$image$, $ker$]' +
Gives the morphological opening of $image$ with respect to structuring element $ker$. +
+ + >> ein = Import["ExampleData/Einstein.jpg"]; + >> Opening[ein, 2.5] + = -Image- + ''' + + +class Closing(_MorphologyFilter): + ''' +
+
'Closing[$image$, $ker$]' +
Gives the morphological closing of $image$ with respect to structuring element $ker$. +
+ + >> ein = Import["ExampleData/Einstein.jpg"]; + >> Closing[ein, 2.5] + = -Image- + ''' + + +class MorphologicalComponents(_ImageBuiltin): + rules = { + 'MorphologicalComponents[i_Image]': 'MorphologicalComponents[i, 0]' + } + + def apply(self, image, t, evaluation): + 'MorphologicalComponents[image_Image, t_?RealNumberQ]' + pixels = skimage.img_as_ubyte(skimage.img_as_float(image.grayscale().pixels) > t.to_python()) + return from_python(skimage.measure.label(pixels, background=0, connectivity=2).tolist()) + + +# color space + + +class ImageColorSpace(_ImageBuiltin): + def apply(self, image, evaluation): + 'ImageColorSpace[image_Image]' + return String(image.color_space) + + +class ColorConvert(_ImageBuiltin): + def apply(self, image, colorspace, evaluation): + 'ColorConvert[image_Image, colorspace_String]' + return image.color_convert(colorspace.get_string_value()) + + +class ColorQuantize(_ImageBuiltin): + def apply(self, image, n, evaluation): + 'ColorQuantize[image_Image, n_Integer]' + pixels = skimage.img_as_ubyte(image.color_convert('RGB').pixels) + im = PIL.Image.fromarray(pixels).quantize(n.to_python()) + im = im.convert('RGB') + return Image(numpy.array(im), 'RGB') + + +class Threshold(_ImageBuiltin): + options = { + 'Method': '"Cluster"' + } + + messages = { + 'illegalmethod': 'Method `` is not supported.' + } + + def apply(self, image, evaluation, options): + 'Threshold[image_Image, OptionsPattern[Threshold]]' + pixels = image.grayscale().pixels + + method = self.get_option(options, 'Method', evaluation) + method_name = method.get_string_value() if isinstance(method, String) else method.to_python() + if method_name == 'Cluster': + threshold = skimage.filters.threshold_otsu(pixels) + elif method_name == 'Median': + threshold = numpy.median(pixels) + elif method_name == 'Mean': + threshold = numpy.mean(pixels) + else: + return evaluation.message('Threshold', 'illegalmethod', method) + + return Real(threshold) + + +class Binarize(_ImageBuiltin): + def apply(self, image, evaluation): + 'Binarize[image_Image]' + image = image.grayscale() + threshold = Expression('Threshold', image).evaluate(evaluation).to_python() + return Image(image.pixels > threshold, 'Grayscale') + + def apply_t(self, image, t, evaluation): + 'Binarize[image_Image, t_?RealNumberQ]' + pixels = image.grayscale().pixels + return Image(pixels > t.to_python(), 'Grayscale') + + def apply_t1_t2(self, image, t1, t2, evaluation): + 'Binarize[image_Image, {t1_?RealNumberQ, t2_?RealNumberQ}]' + pixels = image.grayscale().pixels + mask1 = pixels > t1.to_python() + mask2 = pixels < t2.to_python() + return Image(mask1 * mask2, 'Grayscale') + + +class ColorNegate(_ImageBuiltin): + def apply(self, image, evaluation): + 'ColorNegate[image_Image]' + pixels = image.pixels + anchor = numpy.ndarray(pixels.shape, dtype=pixels.dtype) + anchor.fill(skimage.dtype_limits(pixels)[1]) + return Image(anchor - pixels, image.color_space) + + +class ColorSeparate(_ImageBuiltin): + def apply(self, image, evaluation): + 'ColorSeparate[image_Image]' + images = [] + pixels = image.pixels + if len(pixels.shape) < 3: + images.append(pixels) + else: + for i in range(pixels.shape[2]): + images.append(Image(pixels[:, :, i], 'Grayscale')) + return Expression('List', *images) + + +def _linearize(a): + # this uses a vectorized binary search to compute + # strictly sequential indices for all values in a. + + orig_shape = a.shape + a = a.reshape((functools.reduce(lambda x, y: x*y, a.shape), )) # 1 dimension + + u = numpy.unique(a) + n = len(u) + + lower = numpy.ndarray(a.shape, dtype=numpy.int) + lower.fill(0) + upper = numpy.ndarray(a.shape, dtype=numpy.int) + upper.fill(n - 1) + + h = numpy.sort(u) + q = n # worst case partition size + + while q > 2: + m = numpy.right_shift(lower + upper, 1) + f = a <= h[m] + # (lower, m) vs (m + 1, upper) + lower = numpy.where(f, lower, m + 1) + upper = numpy.where(f, m, upper) + q = (q + 1) // 2 + + return numpy.where(a == h[lower], lower, upper).reshape(orig_shape), n + + +class Colorize(_ImageBuiltin): + def apply(self, a, evaluation): + 'Colorize[a_?MatrixQ]' + + a, n = _linearize(numpy.array(a.to_python())) + # the maximum value for n is the number of pixels in a, which is acceptable and never too large. + + cmap = matplotlib.cm.get_cmap('hot', n) + p = numpy.transpose(numpy.array([cmap(i) for i in range(n)])[:, 0:3]) + s = (a.shape[0], a.shape[1], 1) + return Image(numpy.concatenate([p[i][a].reshape(s) for i in range(3)], axis=2), color_space='RGB') + + +# pixel access + + +class ImageData(_ImageBuiltin): + rules = { + 'ImageData[image_Image]': 'ImageData[image, "Real"]' + } + + messages = { + 'pixelfmt': 'unsupported pixel format "``"' + } + + def apply(self, image, stype, evaluation): + 'ImageData[image_Image, stype_String]' + pixels = image.pixels + stype = stype.get_string_value() + if stype == 'Real': + pixels = skimage.img_as_float(pixels) + elif stype == 'Byte': + pixels = skimage.img_as_ubyte(pixels) + elif stype == 'Bit16': + pixels = skimage.img_as_uint(pixels) + elif stype == 'Bit': + pixels = pixels.as_dtype(numpy.bool) + else: + return evaluation.message('ImageData', 'pixelfmt', stype) + return from_python(pixels.tolist()) + + +class ImageTake(_ImageBuiltin): + def apply(self, image, n, evaluation): + 'ImageTake[image_Image, n_Integer]' + return Image(image.pixels[:int(n.to_python())], image.color_space) + + +class PixelValue(_ImageBuiltin): + def apply(self, image, x, y, evaluation): + 'PixelValue[image_Image, {x_?RealNumberQ, y_?RealNumberQ}]' + return Real(image.pixels[int(y.to_python() - 1), int(x.to_python() - 1)]) + + +class PixelValuePositions(_ImageBuiltin): + def apply(self, image, val, evaluation): + 'PixelValuePositions[image_Image, val_?RealNumberQ]' + rows, cols = numpy.where(skimage.img_as_float(image.pixels) == float(val.to_python())) + p = numpy.dstack((cols, rows)) + numpy.array([1, 1]) + return from_python(p.tolist()) + + +# image attribute queries + + +class ImageDimensions(_ImageBuiltin): + def apply(self, image, evaluation): + 'ImageDimensions[image_Image]' + return Expression('List', *image.dimensions()) + + +class ImageAspectRatio(_ImageBuiltin): + def apply(self, image, evaluation): + 'ImageAspectRatio[image_Image]' + dim = image.dimensions() + return Real(dim[1] / float(dim[0])) + + +class ImageChannels(_ImageBuiltin): + def apply(self, image, evaluation): + 'ImageChannels[image_Image]' + return Integer(image.channels()) + + +class ImageType(_ImageBuiltin): + def apply(self, image, evaluation): + 'ImageType[image_Image]' + return String(image.storage_type()) + + +class BinaryImageQ(_ImageTest): + ''' +
+
'BinaryImageQ[$image]' +
returns True if the pixels of $image are binary bit values, and False otherwise. +
+ ''' + + def test(self, expr): + return isinstance(expr, Image) and expr.storage_type() == 'Bit' + + +# Image core classes + + +def _image_pixels(matrix): + try: + pixels = numpy.array(matrix, dtype='float64') + except ValueError: # irregular array, e.g. {{0, 1}, {0, 1, 1}} + return None + shape = pixels.shape + if len(shape) == 2 or (len(shape) == 3 and shape[2] in (1, 3)): + return pixels + else: + return None + + +class ImageQ(_ImageTest): + ''' +
+
'ImageQ[Image[$pixels]]' +
returns True if $pixels has dimensions from which an Image can be constructed, and False otherwise. +
+ + >> ImageQ[Image[{{0, 1}, {1, 0}}]] + = True + + >> ImageQ[Image[{{{0, 0, 0}, {0, 1, 0}}, {{0, 1, 0}, {0, 1, 1}}}]] + = True + + >> ImageQ[Image[{{{0, 0, 0}, {0, 1}}, {{0, 1, 0}, {0, 1, 1}}}]] + = False + + >> ImageQ[Image[{1, 0, 1}]] + = False + + >> ImageQ["abc"] + = False + ''' + + def test(self, expr): + return isinstance(expr, Image) + + +class ImageBox(BoxConstruct): + def boxes_to_text(self, leaves, **options): + return '-Image-' + + def boxes_to_xml(self, leaves, **options): + # see https://tools.ietf.org/html/rfc2397 + return '' % ( + leaves[0].get_string_value(), leaves[1].get_int_value(), leaves[2].get_int_value()) + + def boxes_to_tex(self, leaves, **options): + return '-Image-' + + +class Image(Atom): + def __init__(self, pixels, color_space, **kwargs): + super(Image, self).__init__(**kwargs) + self.pixels = pixels + self.color_space = color_space + + def pil(self): + return PIL.Image.fromarray(self.pixels) + + def color_convert(self, to_color_space): + if to_color_space == self.color_space: + return self + else: + conversion = '%s2%s' % (self.color_space, to_color_space) + if conversion in _color_space_conversions: + return Image(_color_space_conversions[conversion](self.pixels), to_color_space) + else: + raise ValueError('cannot convert from color space %s to %s' % (self.color_space, to_color_space)) + + def grayscale(self): + return self.color_convert('Grayscale') + + def atom_to_boxes(self, f, evaluation): + try: + if self.color_space == 'Grayscale': + pixels = self.pixels + else: + pixels = self.color_convert('RGB').pixels + + if pixels.dtype == numpy.bool: + pixels = skimage.img_as_ubyte(pixels) + + shape = pixels.shape + + width = shape[1] + height = shape[0] + scaled_width = width + scaled_height = height + + # if the image is very small, scale it up using nearest neighbour. + min_size = 128 + if width < min_size and height < min_size: + scale = min_size / max(width, height) + scaled_width = int(scale * width) + scaled_height = int(scale * height) + pixels = skimage.transform.resize(pixels, (scaled_height, scaled_width), order=0) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + stream = BytesIO() + skimage.io.imsave(stream, pixels, 'pil', format_str='png') + stream.seek(0) + contents = stream.read() + stream.close() + + encoded = base64.b64encode(contents) + if not six.PY2: + encoded = encoded.decode('utf8') + encoded = 'data:image/png;base64,' + encoded + + return Expression('ImageBox', String(encoded), Integer(scaled_width), Integer(scaled_height)) + except: + return Symbol("$Failed") + + def __str__(self): + return '-Image-' + + def do_copy(self): + return Image(self.pixels) + + def default_format(self, evaluation, form): + return '-Image-' + + def get_sort_key(self, pattern_sort=False): + if pattern_sort: + return super(Image, self).get_sort_key(True) + else: + return hash(self) + + def same(self, other): + return isinstance(other, Image) and numpy.array_equal(self.pixels, other.pixels) + + def to_python(self, *args, **kwargs): + return self.pixels + + def __hash__(self): + return hash(("Image", self.pixels.tobytes())) + + def dimensions(self): + shape = self.pixels.shape + return (shape[1], shape[0]) + + def channels(self): + shape = self.pixels.shape + if len(shape) < 3: + return 1 + else: + return shape[2] + + def storage_type(self): + dtype = self.pixels.dtype + if dtype in (numpy.float32, numpy.float64): + return 'Real' + elif dtype == numpy.uint32: + return 'Bit32' + elif dtype == numpy.uint16: + return 'Bit16' + elif dtype == numpy.uint8: + return 'Byte' + elif dtype == numpy.bool: + return 'Bit' + else: + return str(dtype) + + +class ImageAtom(AtomBuiltin): + requires = _image_requires + + def apply_create(self, array, evaluation): + 'Image[array_]' + pixels = _image_pixels(array.to_python()) + if pixels is not None: + shape = pixels.shape + is_rgb = (len(shape) == 3 and shape[2] == 3) + return Image(pixels.clip(0, 1), 'RGB' if is_rgb else 'Grayscale') + else: + return Expression('Image', array) + + +# word clouds + +class WordCloud(Builtin): + ''' +
+
'WordCloud[{$word1$, $word2$, ...}]' +
Gives a word cloud with the given list of words. +
+ + >> WordCloud[StringSplit[Import["ExampleData/EinsteinSzilLetter.txt"]]] + = -Image- + ''' + + requires = _image_requires + ( + 'wordcloud', + ) + + options = { + 'IgnoreCase': 'True', + 'ImageSize': 'Automatic', + 'MaxItems': 'Automatic', + } + + # this is the palettable.colorbrewer.qualitative.Dark2_8 palette + default_colors = ( + (27, 158, 119), + (217, 95, 2), + (117, 112, 179), + (231, 41, 138), + (102, 166, 30), + (230, 171, 2), + (166, 118, 29), + (102, 102, 102), + ) + + def apply_words(self, words, evaluation, options): + 'WordCloud[words_List, OptionsPattern[%(name)s]]' + ignore_case = self.get_option(options, 'IgnoreCase', evaluation) == Symbol('True') + + freq = dict() + for word in words.leaves: + if not isinstance(word, String): + return + py_word = word.get_string_value() + if ignore_case: + key = py_word.lower() + else: + key = py_word + record = freq.get(key, None) + if record is None: + freq[key] = [py_word, 1] + else: + record[1] += 1 + + max_items = self.get_option(options, 'MaxItems', evaluation) + if isinstance(max_items, Integer): + py_max_items = max_items.get_int_value() + else: + py_max_items = 200 + + image_size = self.get_option(options, 'ImageSize', evaluation) + if image_size == Symbol('Automatic'): + py_image_size = (800, 600) + elif image_size.get_head_name() == 'System`List' and len(image_size.leaves) == 2: + py_image_size = [] + for leaf in image_size.leaves: + if not isinstance(leaf, Integer): + return + py_image_size.append(leaf.get_int_value()) + elif isinstance(image_size, Integer): + size = image_size.get_int_value() + py_image_size = (size, size) + else: + return + + # inspired by http://minimaxir.com/2016/05/wordclouds/ + import random + import os + + def color_func(word, font_size, position, orientation, random_state=None, **kwargs): + return self.default_colors[random.randint(0, 7)] + + font_base_path = os.path.dirname(os.path.abspath(__file__)) + '/../fonts/' + + font_path = os.path.realpath(font_base_path + 'AmaticSC-Bold.ttf') + if not os.path.exists(font_path): + font_path = None + + from wordcloud import WordCloud + wc = WordCloud( + width=py_image_size[0], height=py_image_size[1], + font_path=font_path, max_font_size=300, mode='RGB', + background_color='white', max_words=py_max_items, + color_func=color_func, random_state=42, stopwords=set()) + wc.generate_from_frequencies(freq.values()) + + image = wc.to_image() + return Image(numpy.array(image), 'RGB') diff --git a/mathics/builtin/importexport.py b/mathics/builtin/importexport.py index fa621dfd5c..f08b061fe8 100644 --- a/mathics/builtin/importexport.py +++ b/mathics/builtin/importexport.py @@ -34,7 +34,7 @@ class ImportFormats(Predefined): >> $ImportFormats - = {CSV, JSON, Text} + = {...CSV,...JSON,...Text...} """ name = '$ImportFormats' @@ -51,7 +51,7 @@ class ExportFormats(Predefined): >> $ExportFormats - = {CSV, SVG, Text} + = {...CSV,...SVG,...Text...} """ name = '$ExportFormats' @@ -275,12 +275,6 @@ class Import(Builtin): = {colorsArray -> {{colorName -> black, rgbValue -> (0, 0, 0), hexValue -> #000000}, {colorName -> red, rgbValue -> (255, 0, 0), hexValue -> #FF0000}, {colorName -> green, rgbValue -> (0, 255, 0), hexValue -> #00FF00}, {colorName -> blue, rgbValue -> (0, 0, 255), hexValue -> #0000FF}, {colorName -> yellow, rgbValue -> (255, 255, 0), hexValue -> #FFFF00}, {colorName -> cyan, rgbValue -> (0, 255, 255), hexValue -> #00FFFF}, {colorName -> magenta, rgbValue -> (255, 0, 255), hexValue -> #FF00FF}, {colorName -> white, rgbValue -> (255, 255, 255), hexValue -> #FFFFFF}}} """ - # TODO: Images tests - """ - >> Import["ExampleData/sunflowers.jpg"] - = -Image- - """ - messages = { 'nffil': 'File not found during Import.', 'chtype': ('First argument `1` is not a valid file, directory, ' @@ -506,7 +500,16 @@ class Export(Builtin): } _extdict = { + 'bmp': 'BMP', + 'gif': 'GIF', + 'jp2': 'JPEG2000', 'jpg': 'JPEG', + 'pcx': 'PCX', + 'png': 'PNG', + 'ppm': 'PPM', + 'pbm': 'PBM', + 'pgm': 'PGM', + 'tif': 'TIFF', 'txt': 'Text', 'csv': 'CSV', 'svg': 'SVG', diff --git a/mathics/builtin/scoping.py b/mathics/builtin/scoping.py index 208a14ce99..6777721de4 100644 --- a/mathics/builtin/scoping.py +++ b/mathics/builtin/scoping.py @@ -266,7 +266,7 @@ class Contexts(Builtin): ## this assignment makes sure that a definition in Global` exists >> x = 5; >> Contexts[] // InputForm - = {"Combinatorica`", "Global`", "ImportExport`", "Internal`", "System`", "System`Convert`JSONDump`", "System`Convert`TableDump`", "System`Convert`TextDump`", "System`Private`"} + = {"Combinatorica`", "Global`", "ImportExport`", "Internal`", "System`", "System`Convert`Image`", "System`Convert`JSONDump`", "System`Convert`TableDump`", "System`Convert`TextDump`", "System`Private`"} """ def apply(self, evaluation): diff --git a/mathics/web/media/js/mathics.js b/mathics/web/media/js/mathics.js index 45e34f3227..5f5ed6dda5 100644 --- a/mathics/web/media/js/mathics.js +++ b/mathics/web/media/js/mathics.js @@ -202,11 +202,11 @@ function translateDOMElement(element, svg) { drawGraphics3D(div, data); dom = div; } - if (nodeName == 'svg' || nodeName == 'graphics3d') { + if (nodeName == 'svg' || nodeName == 'graphics3d' || nodeName.toLowerCase() == 'img') { // create that will contain the graphics object = createMathNode('mspace'); var width, height; - if (nodeName == 'svg') { + if (nodeName == 'svg' || nodeName.toLowerCase() == 'img') { width = dom.getAttribute('width'); height = dom.getAttribute('height'); } else {