From d76949da47b2852f4c2e75fb6a6d23b1a6582e39 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Wed, 27 Apr 2016 22:39:47 +0200 Subject: [PATCH 01/13] basic support for images (python 3 version) --- mathics/autoload/formats/JPEG/Import.m | 12 ++ mathics/builtin/image.py | 245 +++++++++++++++++++++++++ mathics/builtin/inout.py | 5 +- setup.py | 2 +- 4 files changed, 261 insertions(+), 3 deletions(-) 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())) diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index ed89984b61..55fa6530e5 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -19,6 +19,7 @@ from mathics.builtin.options import options_to_rules from mathics.core.expression import ( Expression, String, Symbol, Integer, Rational, Real, Complex, BoxError) +from mathics.builtin.image import ImageAtom MULTI_NEWLINE_RE = re.compile(r"\n{2,}") @@ -248,7 +249,7 @@ def apply_general(self, expr, f, evaluation): return String(evaluation.definitions.shorten_name(x.name)) elif isinstance(x, String): return String('"' + six.text_type(x.value) + '"') - elif isinstance(x, (Integer, Real)): + elif isinstance(x, (Integer, Real, ImageAtom)): return x.make_boxes(f.get_name()) elif isinstance(x, (Rational, Complex)): return x.format(evaluation, f.get_name()) @@ -294,7 +295,7 @@ def _apply_atom(self, x, f, evaluation): return String(evaluation.definitions.shorten_name(x.name)) elif isinstance(x, String): return String('"' + x.value + '"') - elif isinstance(x, (Integer, Real)): + elif isinstance(x, (Integer, Real, ImageAtom)): return x.make_boxes(f.get_name()) elif isinstance(x, (Rational, Complex)): return x.format(evaluation, f.get_name()) diff --git a/setup.py b/setup.py index bffbc29be2..a6bf548efc 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ # General Requirements INSTALL_REQUIRES += ['sympy==1.0', 'django >= 1.8, < 1.9', 'ply==3.8', - 'mpmath>=0.19', 'python-dateutil', 'colorama', 'six>=1.10'] + 'mpmath>=0.19', 'python-dateutil', 'colorama', 'six>=1.10', 'scikit-image'] def subdirs(root, file='*.*', depth=10): From 1f36f5b9a0613b8e2227dc5d59a76a70c9e6bc1d Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 28 Apr 2016 22:38:46 +0200 Subject: [PATCH 02/13] 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 d2c591f769..c03163ca22 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 adb5fd236d80352958e079fa38c342eb585df17f Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 28 Apr 2016 23:14:26 +0200 Subject: [PATCH 03/13] some morphology functions; inout.py fix --- mathics/builtin/image.py | 79 ++++++++++++++++++++++++++++++++++++++-- mathics/builtin/inout.py | 6 +-- 2 files changed, 78 insertions(+), 7 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}]' diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index 55fa6530e5..31a039db14 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -19,7 +19,7 @@ from mathics.builtin.options import options_to_rules from mathics.core.expression import ( Expression, String, Symbol, Integer, Rational, Real, Complex, BoxError) -from mathics.builtin.image import ImageAtom +from mathics.builtin.image import Image MULTI_NEWLINE_RE = re.compile(r"\n{2,}") @@ -249,7 +249,7 @@ def apply_general(self, expr, f, evaluation): return String(evaluation.definitions.shorten_name(x.name)) elif isinstance(x, String): return String('"' + six.text_type(x.value) + '"') - elif isinstance(x, (Integer, Real, ImageAtom)): + elif isinstance(x, (Integer, Real, Image)): return x.make_boxes(f.get_name()) elif isinstance(x, (Rational, Complex)): return x.format(evaluation, f.get_name()) @@ -295,7 +295,7 @@ def _apply_atom(self, x, f, evaluation): return String(evaluation.definitions.shorten_name(x.name)) elif isinstance(x, String): return String('"' + x.value + '"') - elif isinstance(x, (Integer, Real, ImageAtom)): + elif isinstance(x, (Integer, Real, Image)): return x.make_boxes(f.get_name()) elif isinstance(x, (Rational, Complex)): return x.format(evaluation, f.get_name()) From 24eef6c9e19954d22ec30efb14db5b283d69763e Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Thu, 28 Apr 2016 23:39:46 +0200 Subject: [PATCH 04/13] scikit-image is now optional --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a6bf548efc..bffbc29be2 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ # General Requirements INSTALL_REQUIRES += ['sympy==1.0', 'django >= 1.8, < 1.9', 'ply==3.8', - 'mpmath>=0.19', 'python-dateutil', 'colorama', 'six>=1.10', 'scikit-image'] + 'mpmath>=0.19', 'python-dateutil', 'colorama', 'six>=1.10'] def subdirs(root, file='*.*', depth=10): From 3302e9316c033db5c677527107c1e3709973f59c Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 00:11:37 +0200 Subject: [PATCH 05/13] 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 be6f40fc88529bbe4188bff2928d448d32fce865 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 07:07:27 +0200 Subject: [PATCH 06/13] 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 5bdeab9ce5c2b77a9a7d8807f934076e750fe629 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 07:15:24 +0200 Subject: [PATCH 07/13] 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 e556fda5e241aaf498e09f391aaa61050504a66d Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 07:36:22 +0200 Subject: [PATCH 08/13] 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 e4d733bc02f200dd72ef56dc99c06bbd601a7632 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 18:37:47 +0200 Subject: [PATCH 09/13] 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 c96c7d2cd2..c978d79955 100644 --- a/mathics/builtin/importexport.py +++ b/mathics/builtin/importexport.py @@ -503,7 +503,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 11aa7fee4153112558938ce74db571c4fa826635 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 18:54:00 +0200 Subject: [PATCH 10/13] 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 c03163ca22..7c5f313d97 100644 --- a/mathics/builtin/__init__.py +++ b/mathics/builtin/__init__.py @@ -7,7 +7,7 @@ from mathics.builtin import ( algebra, arithmetic, assignment, attributes, calculus, combinatorial, comparison, control, datentime, diffeqns, evaluation, exptrig, functional, - graphics, graphics3d, inout, integer, linalg, lists, logic, numbertheory, + graphics, graphics3d, image, inout, integer, linalg, lists, logic, numbertheory, numeric, options, patterns, plot, physchemdata, randomnumbers, recurrence, specialfunctions, scoping, strings, structure, system, tensors) @@ -19,7 +19,7 @@ modules = [ algebra, arithmetic, assignment, attributes, calculus, combinatorial, comparison, control, datentime, diffeqns, evaluation, exptrig, functional, - graphics, graphics3d, inout, integer, linalg, lists, logic, numbertheory, + graphics, graphics3d, image, inout, integer, linalg, lists, logic, numbertheory, numeric, options, patterns, plot, physchemdata, randomnumbers, recurrence, specialfunctions, scoping, strings, structure, system, tensors] From 0c7e9e9a4b4f1d50aac64094dbd972a0f63e961c Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 19:19:22 +0200 Subject: [PATCH 11/13] 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 84461d833d21eace833dc909e46bd941db0794ee Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 20:04:08 +0200 Subject: [PATCH 12/13] 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 2e16084ca8355b0f46b10eb7f59da767d938c4d9 Mon Sep 17 00:00:00 2001 From: Bernhard Liebl Date: Fri, 29 Apr 2016 20:16:49 +0200 Subject: [PATCH 13/13] 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")