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/autoload/formats/JPEG/Export.m b/mathics/autoload/formats/JPEG/Export.m
new file mode 100644
index 0000000000..8c2283f63d
--- /dev/null
+++ b/mathics/autoload/formats/JPEG/Export.m
@@ -0,0 +1,12 @@
+(* Text Exporter *)
+
+Begin["System`Convert`JPEG`"]
+
+RegisterExport[
+ "JPEG",
+ System`ImageExport,
+ Options -> {},
+ BinaryFormat -> True
+]
+
+End[]
diff --git a/mathics/autoload/formats/JPEG/Import.m b/mathics/autoload/formats/JPEG/Import.m
new file mode 100644
index 0000000000..f6c318befe
--- /dev/null
+++ b/mathics/autoload/formats/JPEG/Import.m
@@ -0,0 +1,14 @@
+(* JPEG Importer *)
+
+Begin["System`Convert`JPEG`"]
+
+RegisterImport[
+ "JPEG",
+ System`ImageImport,
+ {},
+ AvailableElements -> {"Image"},
+ DefaultElement -> "Image",
+ FunctionChannels -> {"FileNames"}
+]
+
+End[]
diff --git a/mathics/builtin/__init__.py b/mathics/builtin/__init__.py
index d2c591f769..7c5f313d97 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]
@@ -121,3 +121,18 @@ def contribute(definitions):
if not definitions.have_definition(ensure_context(operator)):
op = ensure_context(operator)
definitions.builtin[op] = Definition(name=op)
+
+ # Special case for Image[]: Image[] is an atom, and so Image[...]
+ # will not usually evaluate to anything, since there are no rules
+ # attached to it. we're adding one special rule here, that allows
+ # to construct Image atoms by using Image[] (using the helper
+ # builin ImageCreate).
+ from mathics.core.rules import Rule
+ from mathics.builtin.image import Image
+ from mathics.core.parser import parse_builtin_rule
+
+ definition = Definition(
+ name='System`Image', rules=[
+ Rule(parse_builtin_rule('Image[x_]'),
+ parse_builtin_rule('ImageCreate[x]'), system=True)])
+ definitions.builtin['System`Image'] = definition
diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py
new file mode 100644
index 0000000000..daeba9091f
--- /dev/null
+++ b/mathics/builtin/image.py
@@ -0,0 +1,736 @@
+'''
+A place for Image[] and related functions.
+
+Note that you need scikit-image installed in order for this module to work.
+
+This module is part of the Mathics/iMathics branch, since the regular Mathics
+notebook seems to lack the functionality to inject
tags from the kernel
+into the notebook interface (yielding an error 'Unknown node type: img').
+Jupyter does not have this limitation though.
+'''
+
+from mathics.builtin.base import (
+ Builtin, Test, BoxConstruct, String)
+from mathics.core.expression import (
+ Atom, Expression, Integer, Real, Symbol, from_python)
+
+import six
+import base64
+
+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 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,
+ }
+
+# import and export
+
+class ImageImport(Builtin):
+ messages = {
+ 'noskimage': 'image import needs scikit-image in order to work.'
+ }
+
+ def apply(self, path, evaluation):
+ '''ImageImport[path_?StringQ]'''
+ if not _enabled:
+ return evaluation.message('ImageImport', 'noskimage')
+ else:
+ 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(Builtin):
+ messages = {
+ 'noskimage': 'image export needs scikit-image in order to work.',
+ 'noimage': 'only an Image[] can be exported into an image file'
+ }
+
+ def apply(self, path, expr, opts, evaluation):
+ '''ImageExport[path_?StringQ, expr_, opts___]'''
+ if not _enabled:
+ return evaluation.message('ImageExport', 'noskimage')
+ elif isinstance(expr, Image):
+ skimage.io.imsave(path.get_string_value(), expr.pixels)
+ return Symbol('Null')
+ else:
+ return evaluation.message('ImageExport', 'noimage')
+
+# image math
+
+class ImageAdd(Builtin):
+ def apply(self, image, x, evaluation):
+ 'ImageAdd[image_Image, x_?RealNumberQ]'
+ return Image((skimage.img_as_float(image.pixels) + float(x.to_python())).clip(0, 1), image.color_space)
+
+
+class ImageSubtract(Builtin):
+ def apply(self, image, x, evaluation):
+ 'ImageSubtract[image_Image, x_?RealNumberQ]'
+ return Image((skimage.img_as_float(image.pixels) - float(x.to_python())).clip(0, 1), image.color_space)
+
+
+class ImageMultiply(Builtin):
+ def apply(self, image, x, evaluation):
+ 'ImageMultiply[image_Image, x_?RealNumberQ]'
+ return Image((skimage.img_as_float(image.pixels) * float(x.to_python())).clip(0, 1), image.color_space)
+
+
+class RandomImage(Builtin):
+ rules = {
+ 'RandomImage[max_?RealNumberQ, {w_Integer, h_Integer}]': 'RandomImage[{0, max}, {w, h}]'
+ }
+
+ def apply(self, minval, maxval, w, h, evaluation):
+ 'RandomImage[{minval_?RealNumberQ, maxval_?RealNumberQ}, {w_Integer, h_Integer}]'
+ try:
+ x0 = max(minval.to_python(), 0)
+ x1 = min(maxval.to_python(), 1)
+ return Image((numpy.random.rand(h.to_python(), w.to_python()) * (x1 - x0) + x0), 'Grayscale')
+ except:
+ import sys
+ return String(repr(sys.exc_info()))
+
+# simple image manipulation
+
+class ImageResize(Builtin):
+ options = {
+ 'Resampling': '"Bicubic"'
+ }
+
+ messages = {
+ 'resamplingerr': 'Resampling mode `` is not supported.',
+ 'gaussaspect': 'Gaussian resampling needs to main aspect ratio.'
+ }
+
+ def apply_resize_width(self, image, width, evaluation, options):
+ 'ImageResize[image_Image, width_Integer, OptionsPattern[ImageResize]]'
+ shape = image.pixels.shape
+ height = int((float(shape[0]) / float(shape[1])) * width.to_python())
+ return self.apply_resize_width_height(image, width, Integer(height), evaluation, options)
+
+ def apply_resize_width_height(self, image, width, height, evaluation, options):
+ 'ImageResize[image_Image, {width_Integer, height_Integer}, OptionsPattern[ImageResize]]'
+ resampling = self.get_option(options, 'Resampling', evaluation)
+ resampling_name = resampling.get_string_value() if isinstance(resampling, String) else resampling.to_python()
+
+ w = int(width.to_python())
+ h = int(height.to_python())
+ 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':
+ old_shape = image.pixels.shape
+ sy = h / old_shape[0]
+ sx = w / old_shape[1]
+ if sy > sx:
+ err = abs((sy * old_shape[1]) - (sx * old_shape[1]))
+ s = sy
+ else:
+ err = abs((sy * old_shape[0]) - (sx * old_shape[0]))
+ s = sx
+ if err > 1.5:
+ return evaluation.error('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.error('ImageResize', 'resamplingerr', resampling_name)
+
+ return Image(pixels, image.color_space)
+
+
+class ImageReflect(Builtin):
+ def apply(self, image, evaluation):
+ 'ImageReflect[image_Image]'
+ return Image(numpy.flipud(image.pixels), image.color_space)
+
+ def apply_ud(self, image, evaluation):
+ 'ImageReflect[image_Image, Top|Bottom]'
+ return Image(numpy.flipud(image.pixels), image.color_space)
+
+ def apply_lr(self, image, evaluation):
+ 'ImageReflect[image_Image, Left|Right]'
+ return Image(numpy.fliplr(image.pixels), image.color_space)
+
+
+class ImageRotate(Builtin):
+ rules = {
+ 'ImageRotate[i_Image]': 'ImageRotate[i, 90]'
+ }
+
+ def apply(self, image, angle, evaluation):
+ 'ImageRotate[image_Image, angle_?RealNumberQ]'
+ return Image(skimage.transform.rotate(image.pixels, angle.to_python(), resize=True), image.color_space)
+
+
+class ImagePartition(Builtin):
+ rules = {
+ 'ImagePartition[i_Image, s_Integer]': 'ImagePartition[i, {s, s}]'
+ }
+
+ def apply(self, image, w, h, evaluation):
+ 'ImagePartition[image_Image, {w_Integer, h_Integer}]'
+ w = w.to_python()
+ h = h.to_python()
+ pixels = image.pixels
+ shape = pixels.shape
+ parts = [Image(pixels[y:y + w, x:x + w], image.color_space)
+ for x in range(0, shape[1], w) for y in range(0, shape[0], h)]
+ return Expression('List', *parts)
+
+# simple image filters
+
+class ImageAdjust(Builtin):
+ def apply_auto(self, image, evaluation):
+ 'ImageAdjust[image_Image]'
+ pixels = skimage.img_as_ubyte(image.pixels)
+ return Image(numpy.array(PIL.ImageOps.equalize(PIL.Image.fromarray(pixels))), image.color_space)
+
+ def apply_contrast(self, image, c, evaluation):
+ 'ImageAdjust[image_Image, c_?RealNumberQ]'
+ enhancer_c = PIL.ImageEnhance.Contrast(image.pil())
+ return Image(numpy.array(enhancer_c.enhance(c.to_python())), image.color_space)
+
+ def apply_contrast_brightness(self, image, c, b, evaluation):
+ 'ImageAdjust[image_Image, {c_?RealNumberQ, b_?RealNumberQ}]'
+ im = image.pil()
+ enhancer_b = PIL.ImageEnhance.Brightness(im)
+ im = enhancer_b.enhance(b.to_python()) # brightness first!
+ enhancer_c = PIL.ImageEnhance.Contrast(im)
+ return Image(numpy.array(enhancer_c.enhance(c.to_python())), image.color_space)
+
+
+class Blur(Builtin):
+ 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(Builtin):
+ 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(Builtin):
+ 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(Builtin):
+ 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(Builtin):
+ 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(Builtin):
+ 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(Builtin):
+ def apply(self, r, evaluation):
+ 'DiskMatrix[r_?RealNumberQ]'
+ return from_python(skimage.morphology.disk(r).tolist())
+
+
+class DiamondMatrix(Builtin):
+ def apply(self, r, evaluation):
+ 'DiamondMatrix[r_?RealNumberQ]'
+ return from_python(skimage.morphology.diamond(r).tolist())
+
+
+class MorphologyFilter(Builtin):
+ messages = {
+ 'grayscale': 'Your image has been converted to grayscale as color images are not supported yet.'
+ }
+
+ def compute(self, image, f, k, evaluation):
+ if image.color_space != 'Grayscale':
+ image = image.color_convert('Grayscale')
+ evaluation.message('MorphologyFilter', 'grayscale')
+ return Image(f(image.pixels, numpy.array(k.to_python())), 'Grayscale')
+
+
+class Dilation(MorphologyFilter):
+ rules = {
+ 'Dilation[i_Image, r_?RealNumberQ]': 'Dilation[i, BoxMatrix[r]]'
+ }
+
+ def apply(self, image, k, evaluation):
+ 'Dilation[image_Image, k_?MatrixQ]'
+ return self.compute(image, skimage.morphology.dilation, k, evaluation)
+
+
+class Erosion(MorphologyFilter):
+ rules = {
+ 'Erosion[i_Image, r_?RealNumberQ]': 'Erosion[i, BoxMatrix[r]]'
+ }
+
+ def apply(self, image, k, evaluation):
+ 'Erosion[image_Image, k_?MatrixQ]'
+ return self.compute(image, skimage.morphology.erosion, k, evaluation)
+
+
+class Opening(MorphologyFilter):
+ rules = {
+ 'Opening[i_Image, r_?RealNumberQ]': 'Opening[i, BoxMatrix[r]]'
+ }
+
+ def apply(self, image, k, evaluation):
+ 'Opening[image_Image, k_?MatrixQ]'
+ return self.compute(image, skimage.morphology.opening, k, evaluation)
+
+
+class Closing(MorphologyFilter):
+ rules = {
+ 'Closing[i_Image, r_?RealNumberQ]': 'Closing[i, BoxMatrix[r]]'
+ }
+
+ def apply(self, image, k, evaluation):
+ 'Closing[image_Image, k_?MatrixQ]'
+ return self.compute(image, skimage.morphology.closing, k, evaluation)
+
+
+class MorphologicalComponents(Builtin):
+ 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(Builtin):
+ def apply(self, image, evaluation):
+ 'ImageColorSpace[image_Image]'
+ return String(image.color_space)
+
+
+class ColorConvert(Builtin):
+ def apply(self, image, colorspace, evaluation):
+ 'ColorConvert[image_Image, colorspace_String]'
+ return image.color_convert(colorspace.get_string_value())
+
+
+class ColorQuantize(Builtin):
+ 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(Builtin):
+ 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.error('Threshold', 'illegalmethod', method)
+
+ return Real(threshold)
+
+
+class Binarize(Builtin):
+ 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(Builtin):
+ 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(Builtin):
+ 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)
+
+
+class Colorize(Builtin):
+ messages = {
+ 'toomany': 'Too many levels.'
+ }
+
+ def apply(self, a, evaluation):
+ 'Colorize[a_?MatrixQ]'
+
+ a = numpy.array(a.to_python())
+ n = int(numpy.max(a)) + 1
+ if n > 8192:
+ return evaluation.error('Colorize', 'toomany')
+
+ 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(Builtin):
+ 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.error('ImageData', 'pixelfmt', stype);
+ return from_python(pixels.tolist())
+
+
+class ImageTake(Builtin):
+ def apply(self, image, n, evaluation):
+ 'ImageTake[image_Image, n_Integer]'
+ return Image(image.pixels[:int(n.to_python())], image.color_space)
+
+
+class PixelValue(Builtin):
+ 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(Builtin):
+ 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(Builtin):
+ def apply(self, image, evaluation):
+ 'ImageDimensions[image_Image]'
+ return Expression('List', *image.dimensions())
+
+class ImageAspectRatio(Builtin):
+ def apply(self, image, evaluation):
+ 'ImageAspectRatio[image_Image]'
+ dim = image.dimensions()
+ return Real(dim[1] / float(dim[0]))
+
+class ImageChannels(Builtin):
+ def apply(self, image, evaluation):
+ 'ImageChannels[image_Image]'
+ return Integer(image.channels())
+
+class ImageType(Builtin):
+ def apply(self, image, evaluation):
+ 'ImageType[image_Image]'
+ return String(image.storage_type())
+
+class BinaryImageQ(Test):
+ def apply(self, image, evaluation):
+ 'BinaryImageQ[image_Image]'
+ return Symbol('True') if image.storage_type() == 'Bit' else Symbol('False')
+
+# Image core classes
+
+class ImageCreate(Builtin):
+ messages = {
+ 'noskimage': 'image creation needs scikit-image in order to work.'
+ }
+
+ def apply(self, array, evaluation):
+ '''ImageCreate[array_?MatrixQ]'''
+ if not _enabled:
+ return evaluation.message('ImageCreate', 'noskimage')
+ else:
+ pixels = numpy.array(array.to_python(), dtype='float64')
+ shape = pixels.shape
+ is_rgb = (len(shape) == 3 and shape[2] == 3)
+ if len(shape) == 2 or (len(shape) == 3 and shape[2] in (1, 3)):
+ return Image(pixels.clip(0, 1), 'RGB' if is_rgb else 'Grayscale')
+ else:
+ return Expression('Image', array)
+
+
+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
+ img = '
' % (leaves[0].get_string_value())
+ return '%s