From 66d3431ba5e64fc701788c90ba47f88a866e059d Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 27 Apr 2016 22:39:47 +0200 Subject: [PATCH 01/47] basic support for images (python 3 version) --- mathics/autoload/formats/JPEG/Import.m | 12 ++ mathics/builtin/image.py | 245 +++++++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 mathics/autoload/formats/JPEG/Import.m create mode 100644 mathics/builtin/image.py diff --git a/mathics/autoload/formats/JPEG/Import.m b/mathics/autoload/formats/JPEG/Import.m new file mode 100644 index 0000000000..9be9d25234 --- /dev/null +++ b/mathics/autoload/formats/JPEG/Import.m @@ -0,0 +1,12 @@ +(* JPEG Importer *) + +Begin["System`Convert`JPEG`"] + +RegisterImport[ + "JPEG", + System`ImportImage, + {}, + AvailableElements -> {"Image"}, + DefaultElement -> "Image", + FunctionChannels -> {"FileNames"} +] diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py new file mode 100644 index 0000000000..67d2284f5e --- /dev/null +++ b/mathics/builtin/image.py @@ -0,0 +1,245 @@ +from mathics.builtin.base import ( + Builtin, InstancableBuiltin, BoxConstruct, BoxConstructError, String) +from mathics.core.expression import ( + Atom, Expression, Integer, Real, NumberError, Symbol, strip_context, + system_symbols, system_symbols_dict) + +import sys +import numpy +import base64 + +import skimage +import skimage.io +import skimage.transform +import skimage.filters +import skimage.exposure +import skimage.feature +import skimage.filters.rank +from skimage.morphology import disk + +import PIL +from PIL import ImageEnhance + +try: + import io # python3 +except ImportError: + from io import StringIO + +class ImportImage(Builtin): + def apply_load(self, path, evaluation): + '''ImportImage[path_?StringQ]''' + from mathics.core.parser import parse_builtin_rule + pixels = skimage.io.imread(path.get_string_value()) + atom = ImageAtom(skimage.img_as_float(pixels)) + return Expression('List', Expression('Rule', String('Image'), atom)) + + +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' % img + + def boxes_to_tex(self, leaves, **options): + return '-Image-' + + +class ImageResize(Builtin): + def apply_resize_width(self, image, width, evaluation): + 'ImageResize[image_Image, width_?RealNumberQ]' + shape = image.pixels.shape + height = int((float(shape[0]) / float(shape[1])) * width.value) + return self.apply_resize_width_height(image, width, Integer(height), evaluation) + + def apply_resize_width_height(self, image, width, height, evaluation): + 'ImageResize[image_Image, {width_?RealNumberQ, height_?RealNumberQ}]' + return ImageAtom(skimage.transform.resize(image.pixels, (int(height.value), int(width.value)))) + + +class ImageRotate(Builtin): + def apply_rotate_90(self, image, evaluation): + 'ImageRotate[image_Image]' + return self.apply_rotate(image, Real(90), evaluation) + + def apply_rotate(self, image, angle, evaluation): + 'ImageRotate[image_Image, angle_?RealNumberQ]' + return ImageAtom(skimage.transform.rotate(image.pixels, angle.value, resize=True)) + + +class ImageAdjust(Builtin): + def apply_auto(self, image, evaluation): + 'ImageAdjust[image_Image]' + try: + return ImageAtom(skimage.filters.rank.autolevel( + skimage.img_as_float(image.pixels), disk(5, dtype='float64'))) + except: + import sys + return String(repr(sys.exc_info())) + + def apply_contrast(self, image, c, evaluation): + 'ImageAdjust[image_Image, c_?RealNumberQ]' + enhancer_c = ImageEnhance.Contrast(image.as_pil()) + return ImageAtom(numpy.array(enhancer_c.enhance(c.value))) + + def apply_contrast_brightness(self, image, c, b, evaluation): + 'ImageAdjust[image_Image, {c_?RealNumberQ, b_?RealNumberQ}]' + im = image.as_pil() + enhancer_b = ImageEnhance.Brightness(im) + im = enhancer_b.enhance(b.value) # brightness first! + enhancer_c = ImageEnhance.Contrast(im) + return ImageAtom(numpy.array(enhancer_c.enhance(c.value))) + + +class GaussianFilter(Builtin): + def apply_radius(self, image, radius, evaluation): + 'GaussianFilter[image_Image, radius_?RealNumberQ]' + if len(image.pixels.shape) > 2 and image.pixels.shape[2] > 3: + pass # FIXME + return ImageAtom(skimage.filters.gaussian( + skimage.img_as_float(image.pixels), sigma=radius.value / 2, multichannel=True)) + + # def apply_radius_sigma(self, image, radius, sigma, evaluation): + # 'GaussianFilter[image_Image, {radius_?RealNumberQ, sigma_?RealNumberQ}]' + + +class PixelValue(Builtin): + def apply(self, image, x, y, evaluation): + 'PixelValue[image_Image, {x_?RealNumberQ, y_?RealNumberQ}]' + return Real(image.pixels[int(y.value), int(x.value)]) + + +class ImageAdd(Builtin): + def apply(self, image, x, evaluation): + 'ImageAdd[image_Image, x_?RealNumberQ]' + # v = x.value * im.shape[2] + return ImageAtom(image.pixels + v) + + +class ColorSeparate(Builtin): + def apply(self, image, evaluation): + 'ColorSeparate[image_Image]' + images = [] + pixels = image.pixels + for i in range(im.shape[2]): + images.append(ImageAtom(pixels[:, :, i])) + return Expression('List', *images) + + +class Binarize(Builtin): + def apply(self, image, evaluation): + 'Binarize[image_Image]' + pixels = image.grey() + threshold = skimage.filters.threshold_otsu(pixels) + return ImageAtom(skimage.img_as_float(pixels > threshold)) + + def apply_t(self, image, t, evaluation): + 'Binarize[image_Image, t_?RealNumberQ]' + pixels = image.grey() + return ImageAtom(skimage.img_as_float(pixels > t.value)) + + def apply_t1_t2(self, image, t1, t2, evaluation): + 'Binarize[image_Image, {t1_?RealNumberQ, t2_?RealNumberQ}]' + pixels = image.grey() + mask1 = pixels > t1.value + mask2 = pixels < t2.value + return ImageAtom(skimage.img_as_float(mask1 * mask2)) + + +class EdgeDetect(Builtin): + def apply(self, image, evaluation): + 'EdgeDetect[image_Image]' + return self._compute(image) + + def apply_r(self, image, r, evaluation): + 'EdgeDetect[image_Image, r_?RealNumberQ]' + return self._compute(image, r.value) + + def apply_r_t(self, image, r, t, evaluation): + 'EdgeDetect[image_Image, r_?RealNumberQ, t_?RealNumberQ]' + return self._compute(image, r.value, t.value) + + def _compute(self, image, radius=2, threshold=0.2): + return ImageAtom(skimage.img_as_float(skimage.feature.canny(image.grey(), sigma=radius / 2, + low_threshold=0.5 * threshold, high_threshold=threshold))) + + +class Image(Builtin): + def apply_create(self, array, evaluation): + '''Image[array_?MatrixQ]''' + pixels = numpy.array(array.to_python(), dtype='float64') + shape = pixels.shape + if len(shape) == 2 or (len(shape) == 3 and shape[2] in (1, 3)): + return ImageAtom(skimage.img_as_float(pixels.clip(0, 1))) + else: + return Expression('Image', array) + +class ImageAtom(Atom): + def __init__(self, pixels, **kwargs): + super(ImageAtom, self).__init__(**kwargs) + self.pixels = pixels + + def as_pil(self): + return PIL.Image.fromarray(self.pixels) + + def grey(self): + pixels = self.pixels + if len(pixels.shape) >= 3 and pixels.shape[2] > 1: + return skimage.color.rgb2gray(pixels) + else: + return pixels + + def make_boxes(self, form): + try: + pixels = self.pixels + shape = pixels.shape + + width = shape[1] + height = shape[0] + + # 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) + pixels = skimage.transform.resize(pixels, (int(scale * height), int(scale * width)), order=0) + + # python3 version + stream = io.BytesIO() + skimage.io.imsave(stream, pixels, 'pil', format_str='png') + stream.seek(0) + contents = stream.read() + stream.close() + + return Expression('ImageBox', String(base64.b64encode(contents).decode('utf8')), + Integer(width), Integer(height)) + except: + return String("error while streaming image: " + repr(sys.exc_info())) + + def __str__(self): + return '-Image-' + + def do_copy(self): + return ImageAtom(self.pixels) + + def default_format(self, evaluation, form): + return '-Image-' + + def get_sort_key(self, pattern_sort=False): + if pattern_sort: + return super(ImageAtom, self).get_sort_key(True) + else: + return hash(self) + + def same(self, other): + return isinstance(other, ImageAtom) and numpy.array_equal(self.pixels, other.pixels) + + def to_sympy(self, **kwargs): + return '-Image-' + + def to_python(self, *args, **kwargs): + return self.pixels + + def __hash__(self): + return hash(("Image", self.pixels.tobytes())) From 7a629259a1c42ee25eae800f1968418b05d503ba Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 28 Apr 2016 22:38:46 +0200 Subject: [PATCH 02/47] work in progress: basic Image[] functionality --- mathics/autoload/formats/JPEG/Export.m | 12 + mathics/autoload/formats/JPEG/Import.m | 4 +- mathics/builtin/__init__.py | 15 + mathics/builtin/image.py | 431 +++++++++++++++++++------ 4 files changed, 364 insertions(+), 98 deletions(-) create mode 100644 mathics/autoload/formats/JPEG/Export.m 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 index 9be9d25234..f6c318befe 100644 --- a/mathics/autoload/formats/JPEG/Import.m +++ b/mathics/autoload/formats/JPEG/Import.m @@ -4,9 +4,11 @@ RegisterImport[ "JPEG", - System`ImportImage, + System`ImageImport, {}, AvailableElements -> {"Image"}, DefaultElement -> "Image", FunctionChannels -> {"FileNames"} ] + +End[] diff --git a/mathics/builtin/__init__.py b/mathics/builtin/__init__.py index 67e822f493..dbdeafbe16 100644 --- a/mathics/builtin/__init__.py +++ b/mathics/builtin/__init__.py @@ -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 index 67d2284f5e..adfaa777b6 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -1,37 +1,99 @@ from mathics.builtin.base import ( - Builtin, InstancableBuiltin, BoxConstruct, BoxConstructError, String) + Builtin, Test, BoxConstruct, String) from mathics.core.expression import ( - Atom, Expression, Integer, Real, NumberError, Symbol, strip_context, - system_symbols, system_symbols_dict) + Atom, Expression, Integer, Real, Symbol, from_python) -import sys +''' +A place for Image[] and related functions. +''' + +import six import numpy import base64 -import skimage -import skimage.io -import skimage.transform -import skimage.filters -import skimage.exposure -import skimage.feature -import skimage.filters.rank -from skimage.morphology import disk - -import PIL -from PIL import ImageEnhance - try: - import io # python3 + import skimage + import skimage.io + import skimage.transform + import skimage.filters + import skimage.exposure + import skimage.feature + import skimage.filters.rank + + from skimage.morphology import disk + + import PIL + import PIL.ImageEnhance + import PIL.ImageOps + import PIL.ImageFilter + + _enabled = True except ImportError: + _enabled = False + +if six.PY2: from io import StringIO +else: + import io + +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 ImportImage(Builtin): - def apply_load(self, path, evaluation): - '''ImportImage[path_?StringQ]''' - from mathics.core.parser import parse_builtin_rule - pixels = skimage.io.imread(path.get_string_value()) - atom = ImageAtom(skimage.img_as_float(pixels)) - return Expression('List', Expression('Rule', String('Image'), atom)) + +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') class ImageBox(BoxConstruct): @@ -49,60 +111,88 @@ def boxes_to_tex(self, leaves, **options): class ImageResize(Builtin): def apply_resize_width(self, image, width, evaluation): - 'ImageResize[image_Image, width_?RealNumberQ]' + 'ImageResize[image_Image, width_Integer]' shape = image.pixels.shape height = int((float(shape[0]) / float(shape[1])) * width.value) return self.apply_resize_width_height(image, width, Integer(height), evaluation) def apply_resize_width_height(self, image, width, height, evaluation): - 'ImageResize[image_Image, {width_?RealNumberQ, height_?RealNumberQ}]' - return ImageAtom(skimage.transform.resize(image.pixels, (int(height.value), int(width.value)))) + 'ImageResize[image_Image, {width_Integer, height_Integer}]' + return Image(skimage.transform.resize( + image.pixels, (int(height.value), int(width.value))), image.color_space) + + +class ImageReflect(Builtin): + def apply(self, image, evaluation): + 'ImageReflect[image_Image]' + return Image(numpy.flipud(image.pixels), image.color_space) class ImageRotate(Builtin): - def apply_rotate_90(self, image, evaluation): - 'ImageRotate[image_Image]' - return self.apply_rotate(image, Real(90), evaluation) + rules = { + 'ImageRotate[i_Image]': 'ImageRotate[i, 90]' + } - def apply_rotate(self, image, angle, evaluation): + def apply(self, image, angle, evaluation): 'ImageRotate[image_Image, angle_?RealNumberQ]' - return ImageAtom(skimage.transform.rotate(image.pixels, angle.value, resize=True)) + return Image(skimage.transform.rotate(image.pixels, angle.value, resize=True), image.color_space) class ImageAdjust(Builtin): def apply_auto(self, image, evaluation): 'ImageAdjust[image_Image]' - try: - return ImageAtom(skimage.filters.rank.autolevel( - skimage.img_as_float(image.pixels), disk(5, dtype='float64'))) - except: - import sys - return String(repr(sys.exc_info())) + 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 = ImageEnhance.Contrast(image.as_pil()) - return ImageAtom(numpy.array(enhancer_c.enhance(c.value))) + enhancer_c = PIL.ImageEnhance.Contrast(image.as_pil()) + return Image(numpy.array(enhancer_c.enhance(c.value)), image.color_space) def apply_contrast_brightness(self, image, c, b, evaluation): 'ImageAdjust[image_Image, {c_?RealNumberQ, b_?RealNumberQ}]' im = image.as_pil() - enhancer_b = ImageEnhance.Brightness(im) + enhancer_b = PIL.ImageEnhance.Brightness(im) im = enhancer_b.enhance(b.value) # brightness first! - enhancer_c = ImageEnhance.Contrast(im) - return ImageAtom(numpy.array(enhancer_c.enhance(c.value))) + enhancer_c = PIL.ImageEnhance.Contrast(im) + return Image(numpy.array(enhancer_c.enhance(c.value)), 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(PIL.Image.fromarray(image.pixels).filter( + PIL.ImageFilter.GaussianBlur(r.value))), 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(PIL.Image.fromarray(image.pixels).filter( + PIL.ImageFilter.UnsharpMask(r.value))), 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: - pass # FIXME - return ImageAtom(skimage.filters.gaussian( - skimage.img_as_float(image.pixels), sigma=radius.value / 2, multichannel=True)) - - # def apply_radius_sigma(self, image, radius, sigma, evaluation): - # 'GaussianFilter[image_Image, {radius_?RealNumberQ, sigma_?RealNumberQ}]' + return evaluation.message('GaussianFilter', 'only3') + else: + return Image(skimage.filters.gaussian( + skimage.img_as_float(image.pixels), + sigma=radius.value / 2, multichannel=True), image.color_space) class PixelValue(Builtin): @@ -114,8 +204,19 @@ def apply(self, image, x, y, evaluation): class ImageAdd(Builtin): def apply(self, image, x, evaluation): 'ImageAdd[image_Image, x_?RealNumberQ]' - # v = x.value * im.shape[2] - return ImageAtom(image.pixels + v) + return Image((skimage.img_as_float(image.pixels) + float(x.value)).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.value)).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.value)).clip(0, 1), image.color_space) class ColorSeparate(Builtin): @@ -123,77 +224,185 @@ def apply(self, image, evaluation): 'ColorSeparate[image_Image]' images = [] pixels = image.pixels - for i in range(im.shape[2]): - images.append(ImageAtom(pixels[:, :, i])) + 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 Binarize(Builtin): def apply(self, image, evaluation): 'Binarize[image_Image]' - pixels = image.grey() + pixels = image.grayscale().pixels threshold = skimage.filters.threshold_otsu(pixels) - return ImageAtom(skimage.img_as_float(pixels > threshold)) + return Image(pixels > threshold, 'Grayscale') def apply_t(self, image, t, evaluation): 'Binarize[image_Image, t_?RealNumberQ]' - pixels = image.grey() - return ImageAtom(skimage.img_as_float(pixels > t.value)) + pixels = image.grayscale().pixels + return Image(pixels > t.value, 'Grayscale') def apply_t1_t2(self, image, t1, t2, evaluation): 'Binarize[image_Image, {t1_?RealNumberQ, t2_?RealNumberQ}]' - pixels = image.grey() + pixels = image.grayscale().pixels mask1 = pixels > t1.value mask2 = pixels < t2.value - return ImageAtom(skimage.img_as_float(mask1 * mask2)) + return Image(mask1 * mask2, 'Grayscale') -class EdgeDetect(Builtin): +class ColorNegate(Builtin): def apply(self, image, evaluation): - 'EdgeDetect[image_Image]' - return self._compute(image) + '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) - def apply_r(self, image, r, evaluation): - 'EdgeDetect[image_Image, r_?RealNumberQ]' - return self._compute(image, r.value) - def apply_r_t(self, image, r, t, evaluation): - 'EdgeDetect[image_Image, r_?RealNumberQ, t_?RealNumberQ]' - return self._compute(image, r.value, t.value) +class ImageDimensions(Builtin): + def apply(self, image, evaluation): + 'ImageDimensions[image_Image]' + return Expression('List', *image.dimensions()) - def _compute(self, image, radius=2, threshold=0.2): - return ImageAtom(skimage.img_as_float(skimage.feature.canny(image.grey(), sigma=radius / 2, - low_threshold=0.5 * threshold, high_threshold=threshold))) +class ImageAspectRatio(Builtin): + def apply(self, image, evaluation): + 'ImageAspectRatio[image_Image]' + dim = image.dimensions() + return Real(dim[1] / float(dim[0])) -class Image(Builtin): - def apply_create(self, array, evaluation): - '''Image[array_?MatrixQ]''' - pixels = numpy.array(array.to_python(), dtype='float64') + +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') + + +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 ImageData(Builtin): + def apply(self, image, evaluation): + 'ImageData[image_Image]' + return from_python(skimage.img_as_float(image.pixels).tolist()) + + +class ImageTake(Builtin): + def apply(self, image, n, evaluation): + 'ImageTake[image_Image, n_Integer]' + return Image(image.pixels[:int(n.value)], 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.value + h = h.value + pixels = image.pixels shape = pixels.shape - if len(shape) == 2 or (len(shape) == 3 and shape[2] in (1, 3)): - return ImageAtom(skimage.img_as_float(pixels.clip(0, 1))) + 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) + +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.value) + im = im.convert('RGB') + return Image(numpy.array(im), 'RGB') + + +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.value / 2, + low_threshold=0.5 * t.value, high_threshold=t.value), 'Grayscale') + + +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: - return Expression('Image', array) + 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 ImageAtom(Atom): - def __init__(self, pixels, **kwargs): - super(ImageAtom, self).__init__(**kwargs) +class Image(Atom): + def __init__(self, pixels, color_space, **kwargs): + super(Image, self).__init__(**kwargs) self.pixels = pixels + self.color_space = color_space def as_pil(self): return PIL.Image.fromarray(self.pixels) - def grey(self): - pixels = self.pixels - if len(pixels.shape) >= 3 and pixels.shape[2] > 1: - return skimage.color.rgb2gray(pixels) + def color_convert(self, to_color_space): + if to_color_space == self.color_space: + return self else: - return pixels + 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 make_boxes(self, form): try: - pixels = self.pixels + 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] @@ -205,35 +414,37 @@ def make_boxes(self, form): scale = min_size / max(width, height) pixels = skimage.transform.resize(pixels, (int(scale * height), int(scale * width)), order=0) - # python3 version - stream = io.BytesIO() - skimage.io.imsave(stream, pixels, 'pil', format_str='png') - stream.seek(0) - contents = stream.read() - stream.close() + if six.PY2: + pass + else: + stream = io.BytesIO() + skimage.io.imsave(stream, pixels, 'pil', format_str='png') + stream.seek(0) + contents = stream.read() + stream.close() return Expression('ImageBox', String(base64.b64encode(contents).decode('utf8')), Integer(width), Integer(height)) except: - return String("error while streaming image: " + repr(sys.exc_info())) + return Symbol("$Failed") def __str__(self): return '-Image-' def do_copy(self): - return ImageAtom(self.pixels) + 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(ImageAtom, self).get_sort_key(True) + return super(Image, self).get_sort_key(True) else: return hash(self) def same(self, other): - return isinstance(other, ImageAtom) and numpy.array_equal(self.pixels, other.pixels) + return isinstance(other, Image) and numpy.array_equal(self.pixels, other.pixels) def to_sympy(self, **kwargs): return '-Image-' @@ -243,3 +454,29 @@ def to_python(self, *args, **kwargs): 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) From fdfc2a565ef3f1fc8236df48200a82ae9b4e2c26 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 28 Apr 2016 23:14:26 +0200 Subject: [PATCH 03/47] some morphology functions; inout.py fix --- mathics/builtin/image.py | 79 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index adfaa777b6..96e57920d9 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -1,12 +1,12 @@ +''' +A place for Image[] and related functions. +''' + from mathics.builtin.base import ( Builtin, Test, BoxConstruct, String) from mathics.core.expression import ( Atom, Expression, Integer, Real, Symbol, from_python) -''' -A place for Image[] and related functions. -''' - import six import numpy import base64 @@ -195,6 +195,77 @@ def apply_radius(self, image, radius, evaluation): sigma=radius.value / 2, multichannel=True), image.color_space) +class BoxMatrix(Builtin): + def apply(self, r, evaluation): + 'BoxMatrix[r_?RealNumberQ]' + s = 1 + 2 * r.value + 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 PixelValue(Builtin): def apply(self, image, x, y, evaluation): 'PixelValue[image_Image, {x_?RealNumberQ, y_?RealNumberQ}]' From 02e73e4b10496ee3506b114fee506bd7b9813ae7 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 00:11:37 +0200 Subject: [PATCH 04/47] min, max, median filters --- mathics/builtin/image.py | 62 +++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 96e57920d9..d4a1e3d5ad 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -19,8 +19,7 @@ import skimage.exposure import skimage.feature import skimage.filters.rank - - from skimage.morphology import disk + import skimage.morphology import PIL import PIL.ImageEnhance @@ -96,19 +95,6 @@ def apply(self, path, expr, opts, evaluation): return evaluation.message('ImageExport', 'noimage') -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' % img - - def boxes_to_tex(self, leaves, **options): - return '-Image-' - - class ImageResize(Builtin): def apply_resize_width(self, image, width, evaluation): 'ImageResize[image_Image, width_Integer]' @@ -146,12 +132,12 @@ def apply_auto(self, image, evaluation): def apply_contrast(self, image, c, evaluation): 'ImageAdjust[image_Image, c_?RealNumberQ]' - enhancer_c = PIL.ImageEnhance.Contrast(image.as_pil()) + enhancer_c = PIL.ImageEnhance.Contrast(image.pil()) return Image(numpy.array(enhancer_c.enhance(c.value)), image.color_space) def apply_contrast_brightness(self, image, c, b, evaluation): 'ImageAdjust[image_Image, {c_?RealNumberQ, b_?RealNumberQ}]' - im = image.as_pil() + im = image.pil() enhancer_b = PIL.ImageEnhance.Brightness(im) im = enhancer_b.enhance(b.value) # brightness first! enhancer_c = PIL.ImageEnhance.Contrast(im) @@ -165,7 +151,7 @@ class Blur(Builtin): def apply(self, image, r, evaluation): 'Blur[image_Image, r_?RealNumberQ]' - return Image(numpy.array(PIL.Image.fromarray(image.pixels).filter( + return Image(numpy.array(image.pil().filter( PIL.ImageFilter.GaussianBlur(r.value))), image.color_space) @@ -176,7 +162,7 @@ class Sharpen(Builtin): def apply(self, image, r, evaluation): 'Sharpen[image_Image, r_?RealNumberQ]' - return Image(numpy.array(PIL.Image.fromarray(image.pixels).filter( + return Image(numpy.array(image.pil().filter( PIL.ImageFilter.UnsharpMask(r.value))), image.color_space) @@ -195,6 +181,29 @@ def apply_radius(self, image, radius, evaluation): sigma=radius.value / 2, multichannel=True), image.color_space) +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.value)) + + +class MaxFilter(PillowImageFilter): + def apply(self, image, r, evaluation): + 'MaxFilter[image_Image, r_Integer]' + return self.compute(image, PIL.ImageFilter.MaxFilter(1 + 2 * r.value)) + + +class MedianFilter(PillowImageFilter): + def apply(self, image, r, evaluation): + 'MedianFilter[image_Image, r_Integer]' + return self.compute(image, PIL.ImageFilter.MedianFilter(1 + 2 * r.value)) + + class BoxMatrix(Builtin): def apply(self, r, evaluation): 'BoxMatrix[r_?RealNumberQ]' @@ -442,13 +451,26 @@ def apply(self, array, evaluation): 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' % img + + 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 as_pil(self): + def pil(self): return PIL.Image.fromarray(self.pixels) def color_convert(self, to_color_space): From 5a885aca3949c20c939287a7fc47b35aee20690c Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 07:07:27 +0200 Subject: [PATCH 05/47] Colorize --- mathics/builtin/image.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index d4a1e3d5ad..7f487ff208 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -20,12 +20,15 @@ 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 matplotlib.cm + _enabled = True except ImportError: _enabled = False @@ -113,6 +116,14 @@ 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 = { @@ -275,6 +286,32 @@ def apply(self, image, k, evaluation): 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.value) + return from_python(skimage.measure.label(pixels, background=0, connectivity=2).tolist()) + + +class Colorize(Builtin): + def apply(self, a, evaluation): + 'Colorize[a_?MatrixQ]' + + a = numpy.array(a.to_python()) + n = int(numpy.max(a)) + 1 + if n > 8192: + return Symbol('$Failed') + + 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') + + class PixelValue(Builtin): def apply(self, image, x, y, evaluation): 'PixelValue[image_Image, {x_?RealNumberQ, y_?RealNumberQ}]' From ebc2f0d6d175ba9b85d4241ba34633c9cabd10e7 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 07:15:24 +0200 Subject: [PATCH 06/47] cleanup --- mathics/builtin/image.py | 228 ++++++++++++++++++++------------------- 1 file changed, 117 insertions(+), 111 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 7f487ff208..41ac760aa1 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -64,6 +64,7 @@ 'XYZ2RGB': skimage.color.xyz2rgb, } +# import and export class ImageImport(Builtin): messages = { @@ -97,6 +98,26 @@ def apply(self, path, expr, opts, evaluation): 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.value)).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.value)).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.value)).clip(0, 1), image.color_space) + +# simple image manipulation class ImageResize(Builtin): def apply_resize_width(self, image, width, evaluation): @@ -135,6 +156,23 @@ def apply(self, image, angle, evaluation): return Image(skimage.transform.rotate(image.pixels, angle.value, 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.value + h = h.value + 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]' @@ -191,6 +229,7 @@ def apply_radius(self, image, radius, evaluation): skimage.img_as_float(image.pixels), sigma=radius.value / 2, multichannel=True), image.color_space) +# morphological image filters class PillowImageFilter(Builtin): def compute(self, image, f): @@ -215,6 +254,18 @@ def apply(self, image, r, evaluation): return self.compute(image, PIL.ImageFilter.MedianFilter(1 + 2 * r.value)) +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.value / 2, + low_threshold=0.5 * t.value, high_threshold=t.value), 'Grayscale') + + class BoxMatrix(Builtin): def apply(self, r, evaluation): 'BoxMatrix[r_?RealNumberQ]' @@ -296,57 +347,27 @@ def apply(self, image, t, evaluation): pixels = skimage.img_as_ubyte(skimage.img_as_float(image.grayscale().pixels) > t.value) return from_python(skimage.measure.label(pixels, background=0, connectivity=2).tolist()) +# color space -class Colorize(Builtin): - def apply(self, a, evaluation): - 'Colorize[a_?MatrixQ]' - - a = numpy.array(a.to_python()) - n = int(numpy.max(a)) + 1 - if n > 8192: - return Symbol('$Failed') - - 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') - - -class PixelValue(Builtin): - def apply(self, image, x, y, evaluation): - 'PixelValue[image_Image, {x_?RealNumberQ, y_?RealNumberQ}]' - return Real(image.pixels[int(y.value), int(x.value)]) - - -class ImageAdd(Builtin): - def apply(self, image, x, evaluation): - 'ImageAdd[image_Image, x_?RealNumberQ]' - return Image((skimage.img_as_float(image.pixels) + float(x.value)).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.value)).clip(0, 1), image.color_space) +class ImageColorSpace(Builtin): + def apply(self, image, evaluation): + 'ImageColorSpace[image_Image]' + return String(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.value)).clip(0, 1), 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 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 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.value) + im = im.convert('RGB') + return Image(numpy.array(im), 'RGB') class Binarize(Builtin): @@ -378,48 +399,34 @@ def apply(self, image, evaluation): return Image(anchor - pixels, image.color_space) -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): +class ColorSeparate(Builtin): def apply(self, image, evaluation): - 'BinaryImageQ[image_Image]' - return Symbol('True') if image.storage_type() == 'Bit' else Symbol('False') + '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 ImageColorSpace(Builtin): - def apply(self, image, evaluation): - 'ImageColorSpace[image_Image]' - return String(image.color_space) +class Colorize(Builtin): + def apply(self, a, evaluation): + 'Colorize[a_?MatrixQ]' + a = numpy.array(a.to_python()) + n = int(numpy.max(a)) + 1 + if n > 8192: + return Symbol('$Failed') -class ColorConvert(Builtin): - def apply(self, image, colorspace, evaluation): - 'ColorConvert[image_Image, colorspace_String]' - return image.color_convert(colorspace.get_string_value()) + 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): def apply(self, image, evaluation): @@ -433,41 +440,40 @@ def apply(self, image, n, evaluation): return Image(image.pixels[:int(n.value)], image.color_space) -class ImagePartition(Builtin): - rules = { - 'ImagePartition[i_Image, s_Integer]': 'ImagePartition[i, {s, s}]' - } +class PixelValue(Builtin): + def apply(self, image, x, y, evaluation): + 'PixelValue[image_Image, {x_?RealNumberQ, y_?RealNumberQ}]' + return Real(image.pixels[int(y.value), int(x.value)]) - def apply(self, image, w, h, evaluation): - 'ImagePartition[image_Image, {w_Integer, h_Integer}]' - w = w.value - h = h.value - 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) +# image attribute queries -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.value) - im = im.convert('RGB') - return Image(numpy.array(im), 'RGB') + 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 EdgeDetect(Builtin): - rules = { - 'EdgeDetect[i_Image]': 'EdgeDetect[i, 2, 0.2]', - 'EdgeDetect[i_Image, r_?RealNumberQ]': 'EdgeDetect[i, r, 0.2]' - } + class ImageChannels(Builtin): + def apply(self, image, evaluation): + 'ImageChannels[image_Image]' + return Integer(image.channels()) - def apply(self, image, r, t, evaluation): - 'EdgeDetect[image_Image, r_?RealNumberQ, t_?RealNumberQ]' - return Image(skimage.feature.canny(image.grayscale().pixels, sigma=r.value / 2, - low_threshold=0.5 * t.value, high_threshold=t.value), 'Grayscale') + 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 = { From 1c48b56b5c1a0dfbe2256c259aafd3defd12e51b Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 07:36:22 +0200 Subject: [PATCH 07/47] PixelValuePositions --- mathics/builtin/image.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 41ac760aa1..0b049292b6 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -443,7 +443,19 @@ def apply(self, image, n, evaluation): class PixelValue(Builtin): def apply(self, image, x, y, evaluation): 'PixelValue[image_Image, {x_?RealNumberQ, y_?RealNumberQ}]' - return Real(image.pixels[int(y.value), int(x.value)]) + return Real(image.pixels[int(y.value - 1), int(x.value - 1)]) + + +class PixelValuePositions(Builtin): + def apply(self, image, val, evaluation): + 'PixelValuePositions[image_Image, val_?RealNumberQ]' + try: + rows, cols = numpy.where(skimage.img_as_float(image.pixels) == float(val.value)) + p = numpy.dstack((cols, rows)) + numpy.array([1, 1]) + return from_python(p.tolist()) + except: + import sys + return String(repr(sys.exc_info())) # image attribute queries From dd997314c570a4250449d4889d723982cb32d02b Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 18:37:47 +0200 Subject: [PATCH 08/47] import and export for multiple formats, some image method tuning --- mathics/autoload/formats/Image/Export.m | 14 ++ mathics/autoload/formats/Image/Import.m | 16 ++ mathics/builtin/image.py | 248 +++++++++++++++++------- mathics/builtin/importexport.py | 9 + 4 files changed, 214 insertions(+), 73 deletions(-) create mode 100644 mathics/autoload/formats/Image/Export.m create mode 100644 mathics/autoload/formats/Image/Import.m 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/image.py b/mathics/builtin/image.py index 0b049292b6..7953a7bd97 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -8,7 +8,6 @@ Atom, Expression, Integer, Real, Symbol, from_python) import six -import numpy import base64 try: @@ -27,6 +26,8 @@ import PIL.ImageOps import PIL.ImageFilter + import numpy + import matplotlib.cm _enabled = True @@ -103,33 +104,85 @@ def apply(self, path, expr, opts, evaluation): class ImageAdd(Builtin): def apply(self, image, x, evaluation): 'ImageAdd[image_Image, x_?RealNumberQ]' - return Image((skimage.img_as_float(image.pixels) + float(x.value)).clip(0, 1), image.color_space) + 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.value)).clip(0, 1), image.color_space) + 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.value)).clip(0, 1), image.color_space) + 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): - def apply_resize_width(self, image, width, evaluation): - 'ImageResize[image_Image, width_Integer]' + 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.value) - return self.apply_resize_width_height(image, width, Integer(height), evaluation) + 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) - def apply_resize_width_height(self, image, width, height, evaluation): - 'ImageResize[image_Image, {width_Integer, height_Integer}]' - return Image(skimage.transform.resize( - image.pixels, (int(height.value), int(width.value))), image.color_space) + return Image(pixels, image.color_space) class ImageReflect(Builtin): @@ -153,7 +206,7 @@ class ImageRotate(Builtin): def apply(self, image, angle, evaluation): 'ImageRotate[image_Image, angle_?RealNumberQ]' - return Image(skimage.transform.rotate(image.pixels, angle.value, resize=True), image.color_space) + return Image(skimage.transform.rotate(image.pixels, angle.to_python(), resize=True), image.color_space) class ImagePartition(Builtin): @@ -163,8 +216,8 @@ class ImagePartition(Builtin): def apply(self, image, w, h, evaluation): 'ImagePartition[image_Image, {w_Integer, h_Integer}]' - w = w.value - h = h.value + 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) @@ -182,15 +235,15 @@ def apply_auto(self, image, evaluation): 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.value)), image.color_space) + 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.value) # brightness first! + im = enhancer_b.enhance(b.to_python()) # brightness first! enhancer_c = PIL.ImageEnhance.Contrast(im) - return Image(numpy.array(enhancer_c.enhance(c.value)), image.color_space) + return Image(numpy.array(enhancer_c.enhance(c.to_python())), image.color_space) class Blur(Builtin): @@ -201,7 +254,7 @@ class Blur(Builtin): def apply(self, image, r, evaluation): 'Blur[image_Image, r_?RealNumberQ]' return Image(numpy.array(image.pil().filter( - PIL.ImageFilter.GaussianBlur(r.value))), image.color_space) + PIL.ImageFilter.GaussianBlur(r.to_python()))), image.color_space) class Sharpen(Builtin): @@ -212,7 +265,7 @@ class Sharpen(Builtin): def apply(self, image, r, evaluation): 'Sharpen[image_Image, r_?RealNumberQ]' return Image(numpy.array(image.pil().filter( - PIL.ImageFilter.UnsharpMask(r.value))), image.color_space) + PIL.ImageFilter.UnsharpMask(r.to_python()))), image.color_space) class GaussianFilter(Builtin): @@ -227,7 +280,7 @@ def apply_radius(self, image, radius, evaluation): else: return Image(skimage.filters.gaussian( skimage.img_as_float(image.pixels), - sigma=radius.value / 2, multichannel=True), image.color_space) + sigma=radius.to_python() / 2, multichannel=True), image.color_space) # morphological image filters @@ -239,19 +292,19 @@ def compute(self, image, f): class MinFilter(PillowImageFilter): def apply(self, image, r, evaluation): 'MinFilter[image_Image, r_Integer]' - return self.compute(image, PIL.ImageFilter.MinFilter(1 + 2 * r.value)) + 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.value)) + 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.value)) + return self.compute(image, PIL.ImageFilter.MedianFilter(1 + 2 * r.to_python())) class EdgeDetect(Builtin): @@ -262,14 +315,16 @@ class EdgeDetect(Builtin): def apply(self, image, r, t, evaluation): 'EdgeDetect[image_Image, r_?RealNumberQ, t_?RealNumberQ]' - return Image(skimage.feature.canny(image.grayscale().pixels, sigma=r.value / 2, - low_threshold=0.5 * t.value, high_threshold=t.value), 'Grayscale') + 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.value + s = 1 + 2 * r.to_python() return from_python(skimage.morphology.rectangle(s, s).tolist()) @@ -344,7 +399,7 @@ class MorphologicalComponents(Builtin): def apply(self, image, t, evaluation): 'MorphologicalComponents[image_Image, t_?RealNumberQ]' - pixels = skimage.img_as_ubyte(skimage.img_as_float(image.grayscale().pixels) > t.value) + 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 @@ -365,28 +420,55 @@ 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.value) + 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]' - pixels = image.grayscale().pixels - threshold = skimage.filters.threshold_otsu(pixels) - return Image(pixels > threshold, 'Grayscale') + 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.value, 'Grayscale') + 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.value - mask2 = pixels < t2.value + mask1 = pixels > t1.to_python() + mask2 = pixels < t2.to_python() return Image(mask1 * mask2, 'Grayscale') @@ -413,13 +495,17 @@ def apply(self, image, evaluation): 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 Symbol('$Failed') + 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]) @@ -429,61 +515,77 @@ def apply(self, a, evaluation): # pixel access class ImageData(Builtin): - def apply(self, image, evaluation): - 'ImageData[image_Image]' - return from_python(skimage.img_as_float(image.pixels).tolist()) + 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.value)], image.color_space) + 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.value - 1), int(x.value - 1)]) + 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]' - try: - rows, cols = numpy.where(skimage.img_as_float(image.pixels) == float(val.value)) - p = numpy.dstack((cols, rows)) + numpy.array([1, 1]) - return from_python(p.tolist()) - except: - import sys - return String(repr(sys.exc_info())) + 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') +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 diff --git a/mathics/builtin/importexport.py b/mathics/builtin/importexport.py index fa621dfd5c..4a5c542067 100644 --- a/mathics/builtin/importexport.py +++ b/mathics/builtin/importexport.py @@ -506,7 +506,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', From 6fd64063e01b02f6eb4871719f34c57b680e32a3 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 18:54:00 +0200 Subject: [PATCH 09/47] added image module to builtin module's init --- mathics/builtin/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mathics/builtin/__init__.py b/mathics/builtin/__init__.py index dbdeafbe16..6aac4f1d89 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] From ab4032c9633e2d244186676258241fb07caef69b Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 19:19:22 +0200 Subject: [PATCH 10/47] a bit of python2 compatibility --- mathics/builtin/image.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 7953a7bd97..d9b6ccae5d 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -34,10 +34,7 @@ except ImportError: _enabled = False -if six.PY2: - from io import StringIO -else: - import io +from io import BytesIO if _enabled: _color_space_conversions = { @@ -664,14 +661,15 @@ def make_boxes(self, form): scale = min_size / max(width, height) pixels = skimage.transform.resize(pixels, (int(scale * height), int(scale * width)), order=0) - if six.PY2: - pass - else: - stream = io.BytesIO() - skimage.io.imsave(stream, pixels, 'pil', format_str='png') - stream.seek(0) - contents = stream.read() - stream.close() + 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') return Expression('ImageBox', String(base64.b64encode(contents).decode('utf8')), Integer(width), Integer(height)) From 3f81a6d9f66c5210833f39b2b24798fbbdde71ef Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 20:04:08 +0200 Subject: [PATCH 11/47] some comments in the file head --- mathics/builtin/image.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index d9b6ccae5d..9e3d893ff9 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -1,5 +1,12 @@ ''' 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 ( From 618abd158abb2383737cad1b1d484363c1f18b27 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 20:16:49 +0200 Subject: [PATCH 12/47] fixed encoding logic for python2/3 --- mathics/builtin/image.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 9e3d893ff9..daeba9091f 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -673,13 +673,12 @@ def make_boxes(self, form): stream.seek(0) contents = stream.read() stream.close() - encoded = base64.b64encode(contents) + encoded = base64.b64encode(contents) if not six.PY2: encoded = encoded.decode('utf8') - return Expression('ImageBox', String(base64.b64encode(contents).decode('utf8')), - Integer(width), Integer(height)) + return Expression('ImageBox', String(encoded), Integer(width), Integer(height)) except: return Symbol("$Failed") From a90e6b2a42652f410b13a0c1b0e3ae50243c12df Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Sun, 1 May 2016 10:21:31 +1000 Subject: [PATCH 13/47] implement arbitrary arg Image arithmetic --- mathics/builtin/image.py | 66 ++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index daeba9091f..39be126505 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -12,7 +12,7 @@ from mathics.builtin.base import ( Builtin, Test, BoxConstruct, String) from mathics.core.expression import ( - Atom, Expression, Integer, Real, Symbol, from_python) + Atom, Expression, Integer, Rational, Real, Symbol, from_python) import six import base64 @@ -105,22 +105,62 @@ def apply(self, path, expr, opts, evaluation): # 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 _ImageArithmetic(Builtin): + ufunc = None # must be implemented -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) + 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) + result = self._reduce(images, self.ufunc) + return Image(result.clip(0, 1), image.color_space) + + +class ImageAdd(_ImageArithmetic): + ufunc = numpy.add + + +class ImageSubtract(_ImageArithmetic): + ufunc = numpy.subtract -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 ImageMultiply(_ImageArithmetic): + ufunc = numpy.multiply class RandomImage(Builtin): From 6d88929768c983fc05dec2e1a7c13860163718ac Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Sun, 1 May 2016 10:32:07 +1000 Subject: [PATCH 14/47] Image arithmetic docs and tests --- mathics/builtin/image.py | 63 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 39be126505..c168397218 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -152,14 +152,77 @@ def apply(self, image, args, evaluation): 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] + ''' ufunc = numpy.add 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] + ''' ufunc = numpy.subtract 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] + ''' ufunc = numpy.multiply From f1aeb53d232a1ec8a837d06e364271fea997d335 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Sun, 1 May 2016 22:14:14 +1000 Subject: [PATCH 15/47] wrap image in (within ) to allow images within expressions --- mathics/builtin/image.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index c168397218..d264b7246f 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -722,7 +722,10 @@ def boxes_to_text(self, leaves, **options): def boxes_to_xml(self, leaves, **options): # see https://tools.ietf.org/html/rfc2397 img = '' % (leaves[0].get_string_value()) - return '%s' % img + + # see https://github.com/mathjax/MathJax/issues/896 + xml = '%s' % img + return xml def boxes_to_tex(self, leaves, **options): return '-Image-' From da735616b83af99dc3a16edee3055527e379d93d Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Wed, 4 May 2016 21:40:52 +1000 Subject: [PATCH 16/47] tests and docs for RandomImage --- mathics/builtin/image.py | 48 ++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index d264b7246f..225bd30473 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -148,7 +148,7 @@ def apply(self, image, args, evaluation): if images is None: return evaluation.message(self.get_name(), 'bddarg', arg) result = self._reduce(images, self.ufunc) - return Image(result.clip(0, 1), image.color_space) + return Image(result, image.color_space) class ImageAdd(_ImageArithmetic): @@ -227,19 +227,49 @@ class ImageMultiply(_ImageArithmetic): class RandomImage(Builtin): + ''' +
+
'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- + ''' + rules = { - 'RandomImage[max_?RealNumberQ, {w_Integer, h_Integer}]': 'RandomImage[{0, max}, {w, h}]' + '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.', } 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())) + 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() + data = numpy.random.rand(size[1], size[0]) * (maxrange - minrange) + minrange + return Image(data, 'Grayscale') + # simple image manipulation From 38f5fe704add591064e8de07d8ce569d955e905e Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Wed, 4 May 2016 22:20:12 +1000 Subject: [PATCH 17/47] image import examples --- mathics/builtin/importexport.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/mathics/builtin/importexport.py b/mathics/builtin/importexport.py index 4a5c542067..19ac5abbc3 100644 --- a/mathics/builtin/importexport.py +++ b/mathics/builtin/importexport.py @@ -273,11 +273,17 @@ class Import(Builtin): ## JSON >> Import["ExampleData/colors.json"] = {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 + >> 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- """ From 8e011d277b413b2e58c87d48edfc3268101c7311 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Wed, 4 May 2016 22:29:35 +1000 Subject: [PATCH 18/47] (failing) multiplicative and additive noise examples --- mathics/builtin/image.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 225bd30473..894be90e3f 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -172,6 +172,11 @@ class ImageAdd(_ImageArithmetic): #> 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- ''' ufunc = numpy.add @@ -222,6 +227,11 @@ class ImageMultiply(_ImageArithmetic): #> 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- ''' ufunc = numpy.multiply From c8ef3c5726921f33a898330d958af6fb1aba04f7 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Wed, 4 May 2016 23:55:17 +1000 Subject: [PATCH 19/47] remove duplicate jpeg import/exporters --- mathics/autoload/formats/JPEG/Export.m | 12 ------------ mathics/autoload/formats/JPEG/Import.m | 14 -------------- 2 files changed, 26 deletions(-) delete mode 100644 mathics/autoload/formats/JPEG/Export.m delete mode 100644 mathics/autoload/formats/JPEG/Import.m diff --git a/mathics/autoload/formats/JPEG/Export.m b/mathics/autoload/formats/JPEG/Export.m deleted file mode 100644 index 8c2283f63d..0000000000 --- a/mathics/autoload/formats/JPEG/Export.m +++ /dev/null @@ -1,12 +0,0 @@ -(* 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 deleted file mode 100644 index f6c318befe..0000000000 --- a/mathics/autoload/formats/JPEG/Import.m +++ /dev/null @@ -1,14 +0,0 @@ -(* JPEG Importer *) - -Begin["System`Convert`JPEG`"] - -RegisterImport[ - "JPEG", - System`ImageImport, - {}, - AvailableElements -> {"Image"}, - DefaultElement -> "Image", - FunctionChannels -> {"FileNames"} -] - -End[] From 48ef0eeb0251d9cf91217368b2d4fb27681aac73 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Wed, 4 May 2016 23:55:57 +1000 Subject: [PATCH 20/47] fixup image tests --- mathics/builtin/importexport.py | 2 +- mathics/builtin/scoping.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mathics/builtin/importexport.py b/mathics/builtin/importexport.py index 19ac5abbc3..d80ea8c7d6 100644 --- a/mathics/builtin/importexport.py +++ b/mathics/builtin/importexport.py @@ -34,7 +34,7 @@ class ImportFormats(Predefined): >> $ImportFormats - = {CSV, JSON, Text} + = {BMP, CSV, GIF, ICO, JPEG, JPEG2000, JSON, PBM, PCX, PGM, PNG, PPM, TGA, TIFF, Text} """ name = '$ImportFormats' 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): From c3b514c97090609821993c140116bc27bbb0a068 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Wed, 4 May 2016 23:56:17 +1000 Subject: [PATCH 21/47] pep8 cleanups --- mathics/builtin/image.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 894be90e3f..79eb10f7f2 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -69,8 +69,10 @@ 'XYZ2RGB': skimage.color.xyz2rgb, } + # import and export + class ImageImport(Builtin): messages = { 'noskimage': 'image import needs scikit-image in order to work.' @@ -103,6 +105,7 @@ def apply(self, path, expr, opts, evaluation): else: return evaluation.message('ImageExport', 'noimage') + # image math @@ -283,6 +286,7 @@ def apply(self, minval, maxval, w, h, evaluation): # simple image manipulation + class ImageResize(Builtin): options = { 'Resampling': '"Bicubic"' @@ -371,8 +375,10 @@ def apply(self, image, w, h, evaluation): 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]' @@ -429,8 +435,10 @@ def apply_radius(self, image, radius, evaluation): 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) @@ -549,8 +557,10 @@ def apply(self, image, t, evaluation): 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]' @@ -594,7 +604,7 @@ def apply(self, image, evaluation, options): elif method_name == 'Mean': threshold = numpy.mean(pixels) else: - return evaluation.error('Threshold', 'illegalmethod', method) + return evaluation.error('Threshold', 'illegalmethod', method) return Real(threshold) @@ -659,8 +669,10 @@ def apply(self, a, evaluation): 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"]' @@ -683,7 +695,7 @@ def apply(self, image, stype, evaluation): elif stype == 'Bit': pixels = pixels.as_dtype(numpy.bool) else: - return evaluation.error('ImageData', 'pixelfmt', stype); + return evaluation.error('ImageData', 'pixelfmt', stype) return from_python(pixels.tolist()) @@ -706,36 +718,44 @@ def apply(self, image, val, evaluation): 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.' From 25cef78bbd3cf3b2ba10067087831f4fd3cc03a5 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Thu, 5 May 2016 04:27:52 +1000 Subject: [PATCH 22/47] cleanup ImageResize --- mathics/builtin/image.py | 147 ++++++++++++++++++++++++++++++++++----- 1 file changed, 128 insertions(+), 19 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 79eb10f7f2..4706824fa5 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -9,6 +9,8 @@ Jupyter does not have this limitation though. ''' +from __future__ import division + from mathics.builtin.base import ( Builtin, Test, BoxConstruct, String) from mathics.core.expression import ( @@ -288,50 +290,157 @@ def apply(self, minval, maxval, w, h, evaluation): class ImageResize(Builtin): + ''' +
+
'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[ImageResize[ein, 256, Resampling -> "Nearest"]] + = -Image- + >> ImageDimensions[ImageResize[ein, 256, Resampling -> "Gaussian"]] + = -Image- + #> 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': '"Bicubic"' + 'Resampling': 'Automatic', } messages = { - 'resamplingerr': 'Resampling mode `` is not supported.', - 'gaussaspect': 'Gaussian resampling needs to main aspect ratio.' + 'imgrssz': 'The size `1` is not a valid image size specification.', + 'imgrsm': 'Invalid resampling method `1`.', + '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) + @staticmethod + def _round_pixels(value): + return max(1, int(round(value))) + + 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 self._round_pixels(old_size * s) # handles 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_Integer, height_Integer}, OptionsPattern[ImageResize]]' + 'ImageResize[image_Image, {width_, height_}, OptionsPattern[ImageResize]]' + + # 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 = self._round_pixels(h * old_aspect_ratio) + elif h == 0: + h = self._round_pixels(w / old_aspect_ratio) + + assert isinstance(w, int) and w > 0 + assert isinstance(h, int) and h > 0 + + # resampling method resampling = self.get_option(options, 'Resampling', evaluation) - resampling_name = resampling.get_string_value() if isinstance(resampling, String) else resampling.to_python() + if isinstance(resampling, Symbol) and resampling.get_name() == 'System`Automatic': + resampling_name = 'Bicubic' + else: + resampling_name = resampling.get_string_value() - 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] + sy = h / old_h + sx = w / old_w if sy > sx: - err = abs((sy * old_shape[1]) - (sx * old_shape[1])) + err = abs((sy * old_w) - (sx * old_w)) s = sy else: - err = abs((sy * old_shape[0]) - (sx * old_shape[0])) + err = abs((sy * old_h) - (sx * old_h)) s = sx if err > 1.5: - return evaluation.error('ImageResize', 'gaussaspect') + 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.error('ImageResize', 'resamplingerr', resampling_name) + return evaluation.message('ImageResize', 'imgrsm', resampling) return Image(pixels, image.color_space) From 5635d1cf245a0405fce0bbf504c87620d8a68ff0 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Thu, 5 May 2016 06:44:46 +1000 Subject: [PATCH 23/47] fix ImageResize option bug and Gaussian resize rounding bug --- mathics/builtin/image.py | 42 ++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 4706824fa5..117b371428 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -314,10 +314,16 @@ class ImageResize(Builtin): The default sampling method is Bicubic >> ImageResize[ein, 256, Resampling -> "Bicubic"] = -Image- - >> ImageDimensions[ImageResize[ein, 256, Resampling -> "Nearest"]] + #> ImageDimensions[%] + = {256, 320} + >> ImageResize[ein, 256, Resampling -> "Nearest"] = -Image- - >> ImageDimensions[ImageResize[ein, 256, Resampling -> "Gaussian"]] + #> ImageDimensions[%] + = {256, 320} + >> ImageResize[ein, 256, Resampling -> "Gaussian"] = -Image- + #> ImageDimensions[%] + = {256, 320} #> ImageResize[ein, 256, Resampling -> "Invalid"] : Invalid resampling method Invalid. = ImageResize[-Image-, 256, Resampling -> Invalid] @@ -343,10 +349,6 @@ class ImageResize(Builtin): 'gaussaspect': 'Gaussian resampling needs to main aspect ratio.', } - @staticmethod - def _round_pixels(value): - return max(1, int(round(value))) - def _get_image_size_spec(self, old_size, new_size): predefined_sizes = { 'System`Tiny': 75, @@ -371,11 +373,11 @@ def _get_image_size_spec(self, old_size, new_size): s = new_size.leaves[0].get_real_value() if s is None: return None - return self._round_pixels(old_size * s) # handles negative s values silently + 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]]' + 'ImageResize[image_Image, s_, OptionsPattern[ImageResize]]' old_w = image.pixels.shape[1] if s.has_form('List', 1): width = s.leaves[0] @@ -392,6 +394,12 @@ def apply_resize_width(self, image, s, 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] @@ -406,20 +414,16 @@ def apply_resize_width_height(self, image, width, height, evaluation, options): # if both width and height are Automatic then use old values w, h = old_w, old_h elif w == 0: - w = self._round_pixels(h * old_aspect_ratio) + w = max(1, h * old_aspect_ratio) elif h == 0: - h = self._round_pixels(w / old_aspect_ratio) + h = max(1, w / old_aspect_ratio) - assert isinstance(w, int) and w > 0 - assert isinstance(h, int) and h > 0 - - # 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() + 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': From 0b9c5cfd8189a897ba57902ad3f4013ce1a0e6a0 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Thu, 5 May 2016 06:50:01 +1000 Subject: [PATCH 24/47] fix Gaussian sampling aspect ratio message --- mathics/builtin/image.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 117b371428..c2d9c469aa 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -324,6 +324,9 @@ class ImageResize(Builtin): = -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] @@ -346,7 +349,7 @@ class ImageResize(Builtin): messages = { 'imgrssz': 'The size `1` is not a valid image size specification.', 'imgrsm': 'Invalid resampling method `1`.', - 'gaussaspect': 'Gaussian resampling needs to main aspect ratio.', + 'gaussaspect': 'Gaussian resampling needs to maintain aspect ratio.', } def _get_image_size_spec(self, old_size, new_size): @@ -438,6 +441,7 @@ def apply_resize_width_height(self, image, width, height, evaluation, options): 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) From 21c9d26d689d39c92c3e2fb1f3842f86bf36e045 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Thu, 5 May 2016 07:12:39 +1000 Subject: [PATCH 25/47] cleanup morphology filters --- mathics/builtin/base.py | 4 ++- mathics/builtin/image.py | 69 ++++++++++++++++++++-------------------- 2 files changed, 38 insertions(+), 35 deletions(-) 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 index c2d9c469aa..a9065cd19b 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -612,56 +612,57 @@ def apply(self, r, evaluation): return from_python(skimage.morphology.diamond(r).tolist()) -class MorphologyFilter(Builtin): +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]]' + '%(name)s[i_Image, r_?RealNumberQ]': '%(name)s[i, BoxMatrix[r]]' } + func = None # not implemented + def apply(self, image, k, evaluation): - 'Dilation[image_Image, k_?MatrixQ]' - return self.compute(image, skimage.morphology.dilation, 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 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 Dilation(_MorphologyFilter): + ''' + >> ein = Import["ExampleData/Einstein.jpg"]; + >> Dilation[ein, 2.5] + = -Image- + ''' -class Opening(MorphologyFilter): - rules = { - 'Opening[i_Image, r_?RealNumberQ]': 'Opening[i, BoxMatrix[r]]' - } +class Erosion(_MorphologyFilter): + ''' + >> ein = Import["ExampleData/Einstein.jpg"]; + >> Erosion[ein, 2.5] + = -Image- + ''' - def apply(self, image, k, evaluation): - 'Opening[image_Image, k_?MatrixQ]' - return self.compute(image, skimage.morphology.opening, k, evaluation) +class Opening(_MorphologyFilter): + ''' + >> ein = Import["ExampleData/Einstein.jpg"]; + >> Opening[ein, 2.5] + = -Image- + ''' -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 Closing(_MorphologyFilter): + ''' + >> ein = Import["ExampleData/Einstein.jpg"]; + >> Closing[ein, 2.5] + = -Image- + ''' class MorphologicalComponents(Builtin): From 8515878a794d9fab0c1e0a5e8dc2a4fcb7b5c0cd Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Thu, 5 May 2016 07:40:46 +1000 Subject: [PATCH 26/47] morphology docs --- mathics/builtin/image.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index a9065cd19b..bd48a4417e 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -635,6 +635,11 @@ def apply(self, image, k, evaluation): 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- @@ -643,6 +648,11 @@ class Dilation(_MorphologyFilter): 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- @@ -651,6 +661,11 @@ class Erosion(_MorphologyFilter): 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- @@ -659,6 +674,11 @@ class Opening(_MorphologyFilter): 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- From db970d4218c85e2567d7e15e54c39f95ccc515d2 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Thu, 5 May 2016 07:46:21 +1000 Subject: [PATCH 27/47] cleanup image arith functions --- mathics/builtin/image.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index bd48a4417e..e520206a1e 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -112,8 +112,6 @@ def apply(self, path, expr, opts, evaluation): class _ImageArithmetic(Builtin): - ufunc = None # must be implemented - messages = { 'bddarg': 'Expecting a number, image, or graphics instead of `1`.', } @@ -152,7 +150,8 @@ def apply(self, image, args, evaluation): images, arg = self.convert_args(image, *args.get_sequence()) if images is None: return evaluation.message(self.get_name(), 'bddarg', arg) - result = self._reduce(images, self.ufunc) + ufunc = getattr(numpy, self.get_name(True)[5:].lower()) + result = self._reduce(images, ufunc) return Image(result, image.color_space) @@ -183,7 +182,6 @@ class ImageAdd(_ImageArithmetic): >> ImageAdd[noise, ein] = -Image- ''' - ufunc = numpy.add class ImageSubtract(_ImageArithmetic): @@ -208,7 +206,6 @@ class ImageSubtract(_ImageArithmetic): : Expecting a number, image, or graphics instead of x. = ImageSubtract[-Image-, x] ''' - ufunc = numpy.subtract class ImageMultiply(_ImageArithmetic): @@ -238,7 +235,6 @@ class ImageMultiply(_ImageArithmetic): >> ImageMultiply[noise, ein] = -Image- ''' - ufunc = numpy.multiply class RandomImage(Builtin): From d8879685b2fbe02a755b52a578fe16179672267e Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Thu, 5 May 2016 08:15:06 +1000 Subject: [PATCH 28/47] only import image module when enabled --- mathics/builtin/image.py | 41 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index e520206a1e..ea6aa2273d 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -15,6 +15,7 @@ Builtin, Test, BoxConstruct, String) from mathics.core.expression import ( Atom, Expression, Integer, Rational, Real, Symbol, from_python) +from mathics.core.evaluation import Evaluation import six import base64 @@ -45,6 +46,7 @@ from io import BytesIO + if _enabled: _color_space_conversions = { 'RGB2Grayscale': skimage.color.rgb2gray, @@ -76,32 +78,22 @@ 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)) + 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): + if isinstance(expr, Image): skimage.io.imsave(path.get_string_value(), expr.pixels) return Symbol('Null') else: @@ -891,22 +883,15 @@ def apply(self, image, evaluation): 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') + 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: - 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) + return Expression('Image', array) class ImageBox(BoxConstruct): From 79937d30c94b0eec4dfdfb18434070b3c106fae9 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Thu, 5 May 2016 08:37:36 +1000 Subject: [PATCH 29/47] only test for the minimum import/export formats supported --- mathics/builtin/importexport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mathics/builtin/importexport.py b/mathics/builtin/importexport.py index d80ea8c7d6..74b0ceed61 100644 --- a/mathics/builtin/importexport.py +++ b/mathics/builtin/importexport.py @@ -34,7 +34,7 @@ class ImportFormats(Predefined): >> $ImportFormats - = {BMP, CSV, GIF, ICO, JPEG, JPEG2000, JSON, PBM, PCX, PGM, PNG, PPM, TGA, TIFF, Text} + = {...CSV,...JSON,...Text...} """ name = '$ImportFormats' @@ -51,7 +51,7 @@ class ExportFormats(Predefined): >> $ExportFormats - = {CSV, SVG, Text} + = {...CSV,...SVG,...Text...} """ name = '$ExportFormats' From c82d503ad2bfc80aacb78262d8024654f49206bf Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Thu, 5 May 2016 09:52:01 +1000 Subject: [PATCH 30/47] move image tests to image module --- mathics/builtin/image.py | 13 +++++++++++++ mathics/builtin/importexport.py | 12 ------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index ea6aa2273d..117afe8455 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -78,6 +78,19 @@ class ImageImport(Builtin): + """ + ## 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()) diff --git a/mathics/builtin/importexport.py b/mathics/builtin/importexport.py index 74b0ceed61..f08b061fe8 100644 --- a/mathics/builtin/importexport.py +++ b/mathics/builtin/importexport.py @@ -273,18 +273,6 @@ class Import(Builtin): ## JSON >> Import["ExampleData/colors.json"] = {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}}} - - ## 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- """ messages = { From 7844b2057b1b2283eac956103a92c40058d41ca0 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Thu, 5 May 2016 09:59:46 +1000 Subject: [PATCH 31/47] cleanp image arith --- mathics/builtin/image.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 117afe8455..6d39a168fd 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -622,8 +622,6 @@ class _MorphologyFilter(Builtin): '%(name)s[i_Image, r_?RealNumberQ]': '%(name)s[i, BoxMatrix[r]]' } - func = None # not implemented - def apply(self, image, k, evaluation): '%(name)s[image_Image, k_?MatrixQ]' if image.color_space != 'Grayscale': From 544223a863149fce562f47bfd336aa2dad63f5ce Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Thu, 5 May 2016 11:09:28 +1000 Subject: [PATCH 32/47] RandomImage RGB colorspace --- mathics/builtin/image.py | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 6d39a168fd..7b5041b846 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -156,7 +156,7 @@ def apply(self, image, args, evaluation): 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) + result = self._reduce(images, ufunc).clip(0, 1) return Image(result, image.color_space) @@ -186,6 +186,11 @@ class ImageAdd(_ImageArithmetic): >> 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- ''' @@ -264,8 +269,15 @@ class RandomImage(Builtin): = -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}]', @@ -275,16 +287,28 @@ class RandomImage(Builtin): 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): - 'RandomImage[{minval_?RealNumberQ, maxval_?RealNumberQ}, {w_Integer, h_Integer}]' + 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() - data = numpy.random.rand(size[1], size[0]) * (maxrange - minrange) + minrange - return Image(data, 'Grayscale') + + 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 @@ -741,7 +765,7 @@ def apply(self, image, evaluation, options): elif method_name == 'Mean': threshold = numpy.mean(pixels) else: - return evaluation.error('Threshold', 'illegalmethod', method) + return evaluation.message('Threshold', 'illegalmethod', method) return Real(threshold) @@ -799,7 +823,7 @@ def apply(self, a, evaluation): a = numpy.array(a.to_python()) n = int(numpy.max(a)) + 1 if n > 8192: - return evaluation.error('Colorize', 'toomany') + return evaluation.message('Colorize', 'toomany') cmap = matplotlib.cm.get_cmap('hot', n) p = numpy.transpose(numpy.array([cmap(i) for i in range(n)])[:, 0:3]) @@ -832,7 +856,7 @@ def apply(self, image, stype, evaluation): elif stype == 'Bit': pixels = pixels.as_dtype(numpy.bool) else: - return evaluation.error('ImageData', 'pixelfmt', stype) + return evaluation.message('ImageData', 'pixelfmt', stype) return from_python(pixels.tolist()) From 2da6e3a3e2659ee4657d11a13cfa2dbb7811d540 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Tue, 10 May 2016 20:47:04 +0200 Subject: [PATCH 33/47] a much more robust implementation for Colorize[] --- mathics/builtin/image.py | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 7b5041b846..99b3f7f698 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -19,6 +19,7 @@ import six import base64 +import functools try: import skimage @@ -812,18 +813,41 @@ def apply(self, image, evaluation): return Expression('List', *images) -class Colorize(Builtin): - messages = { - 'toomany': 'Too many levels.' - } +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(Builtin): 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.message('Colorize', 'toomany') + 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]) From 6441aa3d9929de1fdce6384d997d17900f03ec2f Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Tue, 10 May 2016 22:57:44 +0200 Subject: [PATCH 34/47] ImageCreate[] now checks if pixel dimensions are ok --- mathics/builtin/image.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 99b3f7f698..3cfde99291 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -941,16 +941,28 @@ def apply(self, image, evaluation): # 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 ImageCreate(Builtin): def apply(self, array, evaluation): - '''ImageCreate[array_?MatrixQ]''' - 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)): + '''ImageCreate[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) + return Symbol('$Aborted') class ImageBox(BoxConstruct): From 980317c85803561976f5c2559b2e0f891a768bdc Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Sun, 15 May 2016 21:33:34 +1000 Subject: [PATCH 35/47] implement ImageReflect full spec --- mathics/builtin/image.py | 77 +++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 3cfde99291..01cdc561d0 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -480,17 +480,76 @@ def apply_resize_width_height(self, image, width, height, evaluation, options): class ImageReflect(Builtin): - def apply(self, image, evaluation): - 'ImageReflect[image_Image]' - return Image(numpy.flipud(image.pixels), image.color_space) + ''' +
+
'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]', + } - def apply_ud(self, image, evaluation): - 'ImageReflect[image_Image, Top|Bottom]' - return Image(numpy.flipud(image.pixels), image.color_space) + messages = { + 'bdrfl2': '`1` is not a valid 2D reflection specification.', + } - def apply_lr(self, image, evaluation): - 'ImageReflect[image_Image, Left|Right]' - return Image(numpy.fliplr(image.pixels), image.color_space) + 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(Builtin): From cb8696c6423412b14f4375be35819a5be4526bd2 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Mon, 16 May 2016 01:08:15 +1000 Subject: [PATCH 36/47] ImageRotate docs --- mathics/builtin/image.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 01cdc561d0..21bddd32a0 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -20,6 +20,7 @@ import six import base64 import functools +import math try: import skimage @@ -553,13 +554,44 @@ def apply(self, image, orig, dest, evaluation): class ImageRotate(Builtin): + ''' +
+
'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]' + '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_?RealNumberQ]' - return Image(skimage.transform.rotate(image.pixels, angle.to_python(), resize=True), image.color_space) + '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(Builtin): From 5a8fc422c8d14b8aee496035f0067c3287abcc17 Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Mon, 16 May 2016 03:16:53 +1000 Subject: [PATCH 37/47] remove Image.to_sympy (see what breaks) --- mathics/builtin/image.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 21bddd32a0..678f299fc7 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -1147,9 +1147,6 @@ def get_sort_key(self, pattern_sort=False): def same(self, other): return isinstance(other, Image) and numpy.array_equal(self.pixels, other.pixels) - def to_sympy(self, **kwargs): - return '-Image-' - def to_python(self, *args, **kwargs): return self.pixels From aa4131421419d9f2266e6a2c7d0f565cdea7c18b Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Sat, 21 May 2016 23:51:31 +1000 Subject: [PATCH 38/47] ImagePartition docs and tweak --- mathics/builtin/image.py | 54 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 678f299fc7..55d9f5a837 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -595,20 +595,64 @@ def apply(self, image, angle, evaluation): class ImagePartition(Builtin): + ''' +
+
'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.to_python() - h = h.to_python() + 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 - 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) + # 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 From c73dfb5b97d42ed47aa6e622698bddb7d86377be Mon Sep 17 00:00:00 2001 From: Angus Griffith <16sn6uv@gmail.com> Date: Sun, 22 May 2016 06:41:47 +1000 Subject: [PATCH 39/47] ImageAdjust --- mathics/builtin/image.py | 72 ++++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 55d9f5a837..0ce4f500eb 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -658,23 +658,69 @@ def apply(self, image, w, h, evaluation): class ImageAdjust(Builtin): + ''' +
+
'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_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) + 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(self, image, c, b, evaluation): - 'ImageAdjust[image_Image, {c_?RealNumberQ, b_?RealNumberQ}]' + def apply_contrast_brightness_gamma(self, image, c, b, g, evaluation): + 'ImageAdjust[image_Image, {c_?RealNumberQ, b_?RealNumberQ, g_?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) + + # 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(Builtin): From a8c8ef4b0399bd8f39fad0ca00112f00b37d9077 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Tue, 24 May 2016 19:26:59 +0200 Subject: [PATCH 40/47] removed ImageCreate[] in favour of Image[] alone. adds width and height tags to . --- mathics/builtin/__init__.py | 15 --------------- mathics/builtin/image.py | 37 +++++++++++++++++++++---------------- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/mathics/builtin/__init__.py b/mathics/builtin/__init__.py index 6aac4f1d89..ae61814af6 100644 --- a/mathics/builtin/__init__.py +++ b/mathics/builtin/__init__.py @@ -121,18 +121,3 @@ 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 index 0ce4f500eb..47bfb805d5 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -12,7 +12,7 @@ from __future__ import division from mathics.builtin.base import ( - Builtin, Test, BoxConstruct, String) + Builtin, AtomBuiltin, Test, BoxConstruct, String) from mathics.core.expression import ( Atom, Expression, Integer, Rational, Real, Symbol, from_python) from mathics.core.evaluation import Evaluation @@ -1134,25 +1134,14 @@ def _image_pixels(matrix): return None -class ImageCreate(Builtin): - def apply(self, array, evaluation): - '''ImageCreate[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 Symbol('$Aborted') - - 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()) + img = '' % ( + leaves[0].get_string_value(), leaves[1].get_int_value(), leaves[2].get_int_value()) # see https://github.com/mathjax/MathJax/issues/896 xml = '%s' % img @@ -1198,12 +1187,16 @@ def make_boxes(self, form): 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) - pixels = skimage.transform.resize(pixels, (int(scale * height), int(scale * width)), order=0) + scaled_width = int(scale * width) + scaled_height = int(scale * height) + pixels = skimage.transform.resize(pixels, (scaled_height, scaled_width), order=0) stream = BytesIO() skimage.io.imsave(stream, pixels, 'pil', format_str='png') @@ -1215,7 +1208,7 @@ def make_boxes(self, form): if not six.PY2: encoded = encoded.decode('utf8') - return Expression('ImageBox', String(encoded), Integer(width), Integer(height)) + return Expression('ImageBox', String(encoded), Integer(scaled_width), Integer(scaled_height)) except: return Symbol("$Failed") @@ -1268,3 +1261,15 @@ def storage_type(self): return 'Bit' else: return str(dtype) + + +class ImageAtom(AtomBuiltin): + 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) From 61e16cf3a07a1fdddd2b3d37d8295be9094cc957 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Tue, 24 May 2016 19:30:41 +0200 Subject: [PATCH 41/47] adds support for classic Mathics branch --- mathics/builtin/image.py | 13 ++++++++++--- mathics/web/media/js/mathics.js | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 47bfb805d5..39544e99f8 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -1143,9 +1143,16 @@ def boxes_to_xml(self, leaves, **options): img = '' % ( leaves[0].get_string_value(), leaves[1].get_int_value(), leaves[2].get_int_value()) - # see https://github.com/mathjax/MathJax/issues/896 - xml = '%s' % img - return xml + # if we have Mathics JavaScript frontend processing that rewrites MathML tags using + # , we must not embed our tag in here. + uses_mathics_frontend_processing = False + + if not uses_mathics_frontend_processing: + # see https://github.com/mathjax/MathJax/issues/896 + xml = '%s' % img + return xml + else: + return img def boxes_to_tex(self, leaves, **options): return '-Image-' 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 { From 9decfeba974d7ae2637cc18aac93b5ebba5b7794 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Tue, 24 May 2016 19:36:17 +0200 Subject: [PATCH 42/47] adds ImageQ --- mathics/builtin/image.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 39544e99f8..95457c2b49 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -1134,6 +1134,16 @@ def _image_pixels(matrix): return None +class ImageQ(Builtin): + def apply_image(self, image, evaluation): + 'ImageQ[image_Image]' + return Symbol('True') + + def apply_no_image(self, array, evaluation): + 'ImageQ[Image[array_]]' + return Symbol('False') + + class ImageBox(BoxConstruct): def boxes_to_text(self, leaves, **options): return '-Image-' From 6e5c4b369a859847b4b30d97f2b47b91713b2b58 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 25 May 2016 07:22:23 +0200 Subject: [PATCH 43/47] changed ImageQ to Test, fixed BinaryImageQ (was Test, but did not define test()) --- mathics/builtin/image.py | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 95457c2b49..7a221e97e2 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -1114,9 +1114,15 @@ def apply(self, image, evaluation): class BinaryImageQ(Test): - def apply(self, image, evaluation): - 'BinaryImageQ[image_Image]' - return Symbol('True') if image.storage_type() == 'Bit' else Symbol('False') + ''' +
+
'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 @@ -1134,14 +1140,28 @@ def _image_pixels(matrix): return None -class ImageQ(Builtin): - def apply_image(self, image, evaluation): - 'ImageQ[image_Image]' - return Symbol('True') +class ImageQ(Test): + ''' +
+
'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 + ''' - def apply_no_image(self, array, evaluation): - 'ImageQ[Image[array_]]' - return Symbol('False') + def test(self, expr): + return isinstance(expr, Image) class ImageBox(BoxConstruct): From 0ce4784687f5054c2f3a23419b4b8d1b214df4f0 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 25 May 2016 07:29:06 +0200 Subject: [PATCH 44/47] one more test case for ImageQ --- mathics/builtin/image.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 7a221e97e2..b0ab9612bc 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -1158,6 +1158,9 @@ class ImageQ(Test): >> ImageQ[Image[{1, 0, 1}]] = False + + >> ImageQ["abc"] + = False ''' def test(self, expr): From 1616ce0755d23d33275977bb3927444ac25cf293 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 17 Aug 2016 08:20:44 +0200 Subject: [PATCH 45/47] image cleanup after rebase --- mathics/builtin/image.py | 131 +++++++++++++++++++++------------------ 1 file changed, 69 insertions(+), 62 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index b0ab9612bc..9ec2cca7de 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -1,12 +1,7 @@ ''' -A place for Image[] and related functions. +Image[] and image 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. +Note that you (currently) need scikit-image installed in order for this module to work. ''' from __future__ import division @@ -22,6 +17,14 @@ import functools import math +_image_requires = ( + 'skimage', + 'warnings', + 'numpy', + 'PIL', + 'matplotlib', +) + try: import skimage import skimage.io @@ -33,6 +36,8 @@ import skimage.morphology import skimage.measure + import warnings + import PIL import PIL.ImageEnhance import PIL.ImageOps @@ -76,10 +81,17 @@ } +class _ImageBuiltin(Builtin): + requires = _image_requires + + +class _ImageTest(Test): + requires = _image_requires + # import and export -class ImageImport(Builtin): +class ImageImport(_ImageBuiltin): """ ## Image >> Import["ExampleData/Einstein.jpg"] @@ -101,7 +113,7 @@ def apply(self, path, evaluation): return Expression('List', Expression('Rule', String('Image'), atom)) -class ImageExport(Builtin): +class ImageExport(_ImageBuiltin): messages = { 'noimage': 'only an Image[] can be exported into an image file' } @@ -118,7 +130,7 @@ def apply(self, path, expr, opts, evaluation): # image math -class _ImageArithmetic(Builtin): +class _ImageArithmetic(_ImageBuiltin): messages = { 'bddarg': 'Expecting a number, image, or graphics instead of `1`.', } @@ -249,7 +261,7 @@ class ImageMultiply(_ImageArithmetic): ''' -class RandomImage(Builtin): +class RandomImage(_ImageBuiltin): '''
'RandomImage[$max$]' @@ -316,7 +328,7 @@ def apply(self, minval, maxval, w, h, evaluation, options): # simple image manipulation -class ImageResize(Builtin): +class ImageResize(_ImageBuiltin): '''
'ImageResize[$image$, $width$]' @@ -480,7 +492,7 @@ def apply_resize_width_height(self, image, width, height, evaluation, options): return Image(pixels, image.color_space) -class ImageReflect(Builtin): +class ImageReflect(_ImageBuiltin): '''
'ImageReflect[$image$]' @@ -553,7 +565,7 @@ def apply(self, image, orig, dest, evaluation): return Image(method(image.pixels), image.color_space) -class ImageRotate(Builtin): +class ImageRotate(_ImageBuiltin): '''
'ImageRotate[$image$]' @@ -594,7 +606,7 @@ def apply(self, image, angle, evaluation): return Image(skimage.transform.rotate(image.pixels, 180 * py_angle / math.pi, resize=True), image.color_space) -class ImagePartition(Builtin): +class ImagePartition(_ImageBuiltin): '''
'ImagePartition[$image$, $s$]' @@ -657,7 +669,7 @@ def apply(self, image, w, h, evaluation): # simple image filters -class ImageAdjust(Builtin): +class ImageAdjust(_ImageBuiltin): '''
'ImageAdjust[$image$]' @@ -723,7 +735,7 @@ def apply_contrast_brightness_gamma(self, image, c, b, g, evaluation): return Image(numpy.array(im), image.color_space) -class Blur(Builtin): +class Blur(_ImageBuiltin): rules = { 'Blur[i_Image]': 'Blur[i, 2]' } @@ -734,7 +746,7 @@ def apply(self, image, r, evaluation): PIL.ImageFilter.GaussianBlur(r.to_python()))), image.color_space) -class Sharpen(Builtin): +class Sharpen(_ImageBuiltin): rules = { 'Sharpen[i_Image]': 'Sharpen[i, 2]' } @@ -745,7 +757,7 @@ def apply(self, image, r, evaluation): PIL.ImageFilter.UnsharpMask(r.to_python()))), image.color_space) -class GaussianFilter(Builtin): +class GaussianFilter(_ImageBuiltin): messages = { 'only3': 'GaussianFilter only supports up to three channels.' } @@ -763,7 +775,7 @@ def apply_radius(self, image, radius, evaluation): # morphological image filters -class PillowImageFilter(Builtin): +class PillowImageFilter(_ImageBuiltin): def compute(self, image, f): return Image(numpy.array(image.pil().filter(f)), image.color_space) @@ -786,7 +798,7 @@ def apply(self, image, r, evaluation): return self.compute(image, PIL.ImageFilter.MedianFilter(1 + 2 * r.to_python())) -class EdgeDetect(Builtin): +class EdgeDetect(_ImageBuiltin): rules = { 'EdgeDetect[i_Image]': 'EdgeDetect[i, 2, 0.2]', 'EdgeDetect[i_Image, r_?RealNumberQ]': 'EdgeDetect[i, r, 0.2]' @@ -800,26 +812,26 @@ def apply(self, image, r, t, evaluation): 'Grayscale') -class BoxMatrix(Builtin): +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(Builtin): +class DiskMatrix(_ImageBuiltin): def apply(self, r, evaluation): 'DiskMatrix[r_?RealNumberQ]' return from_python(skimage.morphology.disk(r).tolist()) -class DiamondMatrix(Builtin): +class DiamondMatrix(_ImageBuiltin): def apply(self, r, evaluation): 'DiamondMatrix[r_?RealNumberQ]' return from_python(skimage.morphology.diamond(r).tolist()) -class _MorphologyFilter(Builtin): +class _MorphologyFilter(_ImageBuiltin): messages = { 'grayscale': 'Your image has been converted to grayscale as color images are not supported yet.' } @@ -890,7 +902,7 @@ class Closing(_MorphologyFilter): ''' -class MorphologicalComponents(Builtin): +class MorphologicalComponents(_ImageBuiltin): rules = { 'MorphologicalComponents[i_Image]': 'MorphologicalComponents[i, 0]' } @@ -904,19 +916,19 @@ def apply(self, image, t, evaluation): # color space -class ImageColorSpace(Builtin): +class ImageColorSpace(_ImageBuiltin): def apply(self, image, evaluation): 'ImageColorSpace[image_Image]' return String(image.color_space) -class ColorConvert(Builtin): +class ColorConvert(_ImageBuiltin): def apply(self, image, colorspace, evaluation): 'ColorConvert[image_Image, colorspace_String]' return image.color_convert(colorspace.get_string_value()) -class ColorQuantize(Builtin): +class ColorQuantize(_ImageBuiltin): def apply(self, image, n, evaluation): 'ColorQuantize[image_Image, n_Integer]' pixels = skimage.img_as_ubyte(image.color_convert('RGB').pixels) @@ -925,7 +937,7 @@ def apply(self, image, n, evaluation): return Image(numpy.array(im), 'RGB') -class Threshold(Builtin): +class Threshold(_ImageBuiltin): options = { 'Method': '"Cluster"' } @@ -952,7 +964,7 @@ def apply(self, image, evaluation, options): return Real(threshold) -class Binarize(Builtin): +class Binarize(_ImageBuiltin): def apply(self, image, evaluation): 'Binarize[image_Image]' image = image.grayscale() @@ -972,7 +984,7 @@ def apply_t1_t2(self, image, t1, t2, evaluation): return Image(mask1 * mask2, 'Grayscale') -class ColorNegate(Builtin): +class ColorNegate(_ImageBuiltin): def apply(self, image, evaluation): 'ColorNegate[image_Image]' pixels = image.pixels @@ -981,7 +993,7 @@ def apply(self, image, evaluation): return Image(anchor - pixels, image.color_space) -class ColorSeparate(Builtin): +class ColorSeparate(_ImageBuiltin): def apply(self, image, evaluation): 'ColorSeparate[image_Image]' images = [] @@ -1023,7 +1035,7 @@ def _linearize(a): return numpy.where(a == h[lower], lower, upper).reshape(orig_shape), n -class Colorize(Builtin): +class Colorize(_ImageBuiltin): def apply(self, a, evaluation): 'Colorize[a_?MatrixQ]' @@ -1039,7 +1051,7 @@ def apply(self, a, evaluation): # pixel access -class ImageData(Builtin): +class ImageData(_ImageBuiltin): rules = { 'ImageData[image_Image]': 'ImageData[image, "Real"]' } @@ -1065,19 +1077,19 @@ def apply(self, image, stype, evaluation): return from_python(pixels.tolist()) -class ImageTake(Builtin): +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(Builtin): +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(Builtin): +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())) @@ -1088,32 +1100,32 @@ def apply(self, image, val, evaluation): # image attribute queries -class ImageDimensions(Builtin): +class ImageDimensions(_ImageBuiltin): def apply(self, image, evaluation): 'ImageDimensions[image_Image]' return Expression('List', *image.dimensions()) -class ImageAspectRatio(Builtin): +class ImageAspectRatio(_ImageBuiltin): def apply(self, image, evaluation): 'ImageAspectRatio[image_Image]' dim = image.dimensions() return Real(dim[1] / float(dim[0])) -class ImageChannels(Builtin): +class ImageChannels(_ImageBuiltin): def apply(self, image, evaluation): 'ImageChannels[image_Image]' return Integer(image.channels()) -class ImageType(Builtin): +class ImageType(_ImageBuiltin): def apply(self, image, evaluation): 'ImageType[image_Image]' return String(image.storage_type()) -class BinaryImageQ(Test): +class BinaryImageQ(_ImageTest): '''
'BinaryImageQ[$image]' @@ -1140,7 +1152,7 @@ def _image_pixels(matrix): return None -class ImageQ(Test): +class ImageQ(_ImageTest): '''
'ImageQ[Image[$pixels]]' @@ -1173,20 +1185,9 @@ def boxes_to_text(self, leaves, **options): def boxes_to_xml(self, leaves, **options): # see https://tools.ietf.org/html/rfc2397 - img = '' % ( + return '' % ( leaves[0].get_string_value(), leaves[1].get_int_value(), leaves[2].get_int_value()) - # if we have Mathics JavaScript frontend processing that rewrites MathML tags using - # , we must not embed our tag in here. - uses_mathics_frontend_processing = False - - if not uses_mathics_frontend_processing: - # see https://github.com/mathjax/MathJax/issues/896 - xml = '%s' % img - return xml - else: - return img - def boxes_to_tex(self, leaves, **options): return '-Image-' @@ -1213,7 +1214,7 @@ def color_convert(self, to_color_space): def grayscale(self): return self.color_convert('Grayscale') - def make_boxes(self, form): + def atom_to_boxes(self, f, evaluation): try: if self.color_space == 'Grayscale': pixels = self.pixels @@ -1238,15 +1239,19 @@ def make_boxes(self, form): scaled_height = int(scale * height) pixels = skimage.transform.resize(pixels, (scaled_height, scaled_width), order=0) - stream = BytesIO() - skimage.io.imsave(stream, pixels, 'pil', format_str='png') - stream.seek(0) - contents = stream.read() - stream.close() + 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: @@ -1304,6 +1309,8 @@ def storage_type(self): class ImageAtom(AtomBuiltin): + requires = _image_requires + def apply_create(self, array, evaluation): 'Image[array_]' pixels = _image_pixels(array.to_python()) From 7bdfc0cf83d2219c693de55ce5dda20bf7745bf2 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 17 Aug 2016 13:42:11 +0200 Subject: [PATCH 46/47] added scipy to requirements as it's currently needed; will be remedied in the PyPy rework to come --- mathics/builtin/image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index 9ec2cca7de..e3706d6ce7 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -21,6 +21,7 @@ 'skimage', 'warnings', 'numpy', + 'scipy', 'PIL', 'matplotlib', ) From b67c7ed92651fb623d2631bae4365d7c43a9ccfa Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 17 Aug 2016 16:56:48 +0200 Subject: [PATCH 47/47] changed output to mglyph, added simple WordCloud --- mathics/builtin/image.py | 103 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index e3706d6ce7..9a1cf8ee0e 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -10,7 +10,6 @@ Builtin, AtomBuiltin, Test, BoxConstruct, String) from mathics.core.expression import ( Atom, Expression, Integer, Rational, Real, Symbol, from_python) -from mathics.core.evaluation import Evaluation import six import base64 @@ -1186,7 +1185,7 @@ def boxes_to_text(self, leaves, **options): def boxes_to_xml(self, leaves, **options): # see https://tools.ietf.org/html/rfc2397 - return '' % ( + return '' % ( leaves[0].get_string_value(), leaves[1].get_int_value(), leaves[2].get_int_value()) def boxes_to_tex(self, leaves, **options): @@ -1321,3 +1320,103 @@ def apply_create(self, array, evaluation): 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')