diff --git a/mathics/builtin/image.py b/mathics/builtin/image.py index ef7368722d..a372288835 100644 --- a/mathics/builtin/image.py +++ b/mathics/builtin/image.py @@ -102,6 +102,14 @@ def matrix(): return numpy.array(list(matrix())) +def numpy_to_matrix(pixels): + channels = pixels.shape[2] + if channels == 1: + return pixels[:,:,0].tolist() + else: + return pixels.tolist() + + def numpy_flip(pixels, axis): f = (numpy.flipud, numpy.fliplr)[axis] return f(pixels) @@ -842,6 +850,12 @@ class Blur(_ImageBuiltin):
'Blur[$image$, $r$]'
blurs $image$ with a kernel of size $r$. + + >> lena = Import["ExampleData/lena.tif"]; + >> Blur[lena] + = -Image- + >> Blur[lena, 5] + = -Image- ''' rules = { @@ -858,6 +872,12 @@ class Sharpen(_ImageBuiltin):
'Sharpen[$image$, $r$]'
sharpens $image$ with a kernel of size $r$. + + >> lena = Import["ExampleData/lena.tif"]; + >> Sharpen[lena] + = -Image- + >> Sharpen[lena, 5] + = -Image- ''' rules = { @@ -876,6 +896,10 @@ class GaussianFilter(_ImageBuiltin):
'GaussianFilter[$image$, $r$]'
blurs $image$ using a Gaussian blur filter of radius $r$. + + >> lena = Import["ExampleData/lena.tif"]; + >> GaussianFilter[lena, 2.5] + = -Image- ''' messages = { @@ -906,6 +930,10 @@ class MinFilter(PillowImageFilter):
gives $image$ with a minimum filter of radius $r$ applied on it. This always picks the smallest value in the filter's area. + + >> lena = Import["ExampleData/lena.tif"]; + >> MinFilter[lena, 5] + = -Image- ''' def apply(self, image, r, evaluation): @@ -920,6 +948,10 @@ class MaxFilter(PillowImageFilter):
gives $image$ with a maximum filter of radius $r$ applied on it. This always picks the largest value in the filter's area. + + >> lena = Import["ExampleData/lena.tif"]; + >> MaxFilter[lena, 5] + = -Image- ''' def apply(self, image, r, evaluation): @@ -934,6 +966,10 @@ class MedianFilter(PillowImageFilter):
gives $image$ with a median filter of radius $r$ applied on it. This always picks the median value in the filter's area. + + >> lena = Import["ExampleData/lena.tif"]; + >> MedianFilter[lena, 5] + = -Image- ''' def apply(self, image, r, evaluation): @@ -947,6 +983,14 @@ class EdgeDetect(_ImageBuiltin):
'EdgeDetect[$image$]'
returns an image showing the edges in $image$. + + >> lena = Import["ExampleData/lena.tif"]; + >> EdgeDetect[lena] + = -Image- + >> EdgeDetect[lena, 5] + = -Image- + >> EdgeDetect[lena, 4, 0.5] + = -Image- ''' requires = _image_requires + ( @@ -1055,6 +1099,14 @@ class ImageConvolve(_ImageBuiltin):
'ImageConvolve[$image$, $kernel$]'
Computes the convolution of $image$ using $kernel$. + + >> img = Import["ExampleData/lena.tif"]; + >> ImageConvolve[img, DiamondMatrix[5] / 61] + = -Image- + >> ImageConvolve[img, DiskMatrix[5] / 97] + = -Image- + >> ImageConvolve[img, BoxMatrix[5] / 121] + = -Image- ''' def apply(self, image, kernel, evaluation): @@ -1170,6 +1222,10 @@ class ImageColorSpace(_ImageBuiltin):
'ImageColorSpace[$image$]'
gives $image$'s color space, e.g. "RGB" or "CMYK". + + >> img = Import["ExampleData/lena.tif"]; + >> ImageColorSpace[img] + = RGB """ def apply(self, image, evaluation): @@ -1232,10 +1288,27 @@ class ColorQuantize(_ImageBuiltin):
'ColorQuantize[$image$, $n$]'
gives a version of $image$ using only $n$ colors. + + >> img = Import["ExampleData/lena.tif"]; + >> ColorQuantize[img, 6] + = -Image- + + #> ColorQuantize[img, 0] + : Positive integer expected at position 2 in ColorQuantize[-Image-, 0]. + = ColorQuantize[-Image-, 0] + #> ColorQuantize[img, -1] + : Positive integer expected at position 2 in ColorQuantize[-Image-, -1]. + = ColorQuantize[-Image-, -1] ''' + messages = { + 'intp': 'Positive integer expected at position `2` in `1`.', + } + def apply(self, image, n, evaluation): 'ColorQuantize[image_Image, n_Integer]' + if n.get_int_value() <= 0: + return evaluation.message('ColorQuantize', 'intp', Expression('ColorQuantize', image, n), 2) converted = image.color_convert('RGB') if converted is None: return @@ -1253,6 +1326,16 @@ class Threshold(_ImageBuiltin): The option "Method" may be "Cluster" (use Otsu's threshold), "Median", or "Mean". + + >> img = Import["ExampleData/lena.tif"]; + >> Threshold[img] + = 0.456739 + >> Binarize[img, %] + = -Image- + >> Threshold[img, Method -> "Mean"] + = 0.486458 + >> Threshold[img, Method -> "Median"] + = 0.504726 ''' options = { @@ -1284,7 +1367,7 @@ def apply(self, image, evaluation, options): else: return evaluation.message('Threshold', 'illegalmethod', method) - return Real(threshold) + return MachineReal(float(threshold)) class Binarize(_ImageBuiltin): @@ -1294,9 +1377,17 @@ class Binarize(_ImageBuiltin):
gives a binarized version of $image$, in which each pixel is either 0 or 1.
'Binarize[$image$, $t$]'
map values $x$ > $t$ to 1, and values $x$ <= $t$ to 0. -
'Binarize[$image$, $t1$, $t2$]' +
'Binarize[$image$, {$t1$, $t2$}]'
map $t1$ < $x$ < $t2$ to 1, and all other values to 0. + + >> img = Import["ExampleData/lena.tif"]; + >> Binarize[img] + = -Image- + >> Binarize[img, 0.7] + = -Image- + >> Binarize[img, {0.2, 0.6}] + = -Image- ''' def apply(self, image, evaluation): @@ -1468,7 +1559,7 @@ def apply(self, values, evaluation, options): return Image(numpy.concatenate([p[i][a].reshape(s) for i in range(3)], axis=2), color_space='RGB') -class DominantColors(Builtin): +class DominantColors(_ImageBuiltin): '''
'DominantColors[$image$]' @@ -1487,6 +1578,33 @@ class DominantColors(Builtin): The option "MinColorDistance" specifies the distance (in LAB color space) up to which colors are merged and thus regarded as belonging to the same dominant color. + + >> img = Import["ExampleData/sunflowers.jpg"] + = -Image- + + >> DominantColors[img] + = {RGBColor[0.0235294, 0.00392157, 0.], RGBColor[1., 0.835294, 0.027451], RGBColor[0.0352941, 0.168627, 0.], RGBColor[0.0941176, 0.294118, 0.00392157], RGBColor[0.12549, 0.415686, 0.0196078], RGBColor[0.752941, 0.835294, 0.996078], RGBColor[0.952941, 0.705882, 0.]} + + >> DominantColors[img, 3] + = {RGBColor[0.0235294, 0.00392157, 0.], RGBColor[1., 0.835294, 0.027451], RGBColor[0.0352941, 0.168627, 0.]} + + >> DominantColors[img, 3, "Coverage"] + = {311 / 1584, 5419 / 31680, 1081 / 7680} + + >> DominantColors[img, 3, "CoverageImage"] + = {-Image-, -Image-, -Image-} + + >> DominantColors[img, 3, "Count"] + = {49760, 43352, 35673} + + >> DominantColors[img, 2, "LABColor"] + = {LABColor[0.00562582, 0.0125387, 0.00866458], LABColor[0.86979, 0.0391206, 0.856497]} + + >> DominantColors[img, MinColorDistance -> 0.5] + = {RGBColor[0.0941176, 0.294118, 0.00392157], RGBColor[1., 0.835294, 0.027451], RGBColor[0.0235294, 0.00392157, 0.], RGBColor[0.752941, 0.835294, 0.996078], RGBColor[0.490196, 0.258824, 0.0196078]} + + >> DominantColors[img, ColorCoverage -> 0.15] + = {RGBColor[0.0235294, 0.00392157, 0.], RGBColor[1., 0.835294, 0.027451]} ''' rules = { @@ -1581,7 +1699,7 @@ def result(): if py_prop == 'Count': yield Integer(count) elif py_prop == 'Coverage': - yield Rational(count, num_pixels) + yield Rational(int(count), num_pixels) elif py_prop == 'CoverageImage': mask = numpy.ndarray(shape=pixels.shape, dtype=numpy.bool) mask.fill(0) @@ -1602,7 +1720,23 @@ class ImageData(_ImageBuiltin):
'ImageData[$image$]'
gives a list of all color values of $image$ as a matrix. +
'ImageData[$image$, $stype$]' +
gives a list of color values in type $stype$.
+ + >> img = Image[{{0.2, 0.4}, {0.9, 0.6}, {0.5, 0.8}}]; + >> ImageData[img] + = {{0.2, 0.4}, {0.9, 0.6}, {0.5, 0.8}} + + >> ImageData[img, "Byte"] + = {{51, 102}, {229, 153}, {127, 204}} + + >> ImageData[Image[{{0, 1}, {1, 0}, {1, 1}}], "Bit"] + = {{0, 1}, {1, 0}, {1, 1}} + + #> ImageData[img, "Bytf"] + : Unsupported pixel format "Bytf". + = ImageData[-Image-, Bytf] ''' rules = { @@ -1610,7 +1744,7 @@ class ImageData(_ImageBuiltin): } messages = { - 'pixelfmt': 'unsupported pixel format "``"' + 'pixelfmt': 'Unsupported pixel format "``".' } def apply(self, image, stype, evaluation): @@ -1627,7 +1761,7 @@ def apply(self, image, stype, evaluation): pixels = pixels.astype(numpy.bool) else: return evaluation.message('ImageData', 'pixelfmt', stype) - return from_python(pixels.tolist()) + return from_python(numpy_to_matrix(pixels)) class ImageTake(_ImageBuiltin): @@ -1684,11 +1818,40 @@ class PixelValue(_ImageBuiltin):
'PixelValue[$image$, {$x$, $y$}]'
gives the value of the pixel at position {$x$, $y$} in $image$.
+ + >> lena = Import["ExampleData/lena.tif"]; + >> PixelValue[lena, {1, 1}] + = {0.321569, 0.0862745, 0.223529} + #> {82 / 255, 22 / 255, 57 / 255} // N (* pixel byte values from bottom left corner *) + = {0.321569, 0.0862745, 0.223529} + + #> PixelValue[lena, {0, 1}]; + : Padding not implemented for PixelValue. + #> PixelValue[lena, {512, 1}] + = {0.72549, 0.290196, 0.317647} + #> PixelValue[lena, {513, 1}]; + : Padding not implemented for PixelValue. + #> PixelValue[lena, {1, 0}]; + : Padding not implemented for PixelValue. + #> PixelValue[lena, {1, 512}] + = {0.886275, 0.537255, 0.490196} + #> PixelValue[lena, {1, 513}]; + : Padding not implemented for PixelValue. ''' + messages = { + 'nopad': 'Padding not implemented for PixelValue.', + } + def apply(self, image, x, y, evaluation): 'PixelValue[image_Image, {x_?RealNumberQ, y_?RealNumberQ}]' - pixel = image.pixels[int(y.round_to_float() - 1), int(x.round_to_float() - 1)] + x = int(x.round_to_float()) + y = int(y.round_to_float()) + height = image.pixels.shape[0] + width = image.pixels.shape[1] + if not (1 <= x <= width and 1 <= y <= height): + return evaluation.message('PixelValue', 'nopad') + pixel = pixels_as_float(image.pixels)[height - y, x - 1] if isinstance(pixel, (numpy.ndarray, numpy.generic, list)): return Expression('List', *[MachineReal(float(x)) for x in list(pixel)]) else: @@ -1701,13 +1864,39 @@ class PixelValuePositions(_ImageBuiltin):
'PixelValuePositions[$image$, $val$]'
gives the positions of all pixels in $image$ that have value $val$. + + >> PixelValuePositions[Image[{{0, 1}, {1, 0}, {1, 1}}], 1] + = {{1, 1}, {1, 2}, {2, 1}, {2, 3}} + + >> PixelValuePositions[Image[{{0.2, 0.4}, {0.9, 0.6}, {0.3, 0.8}}], 0.5, 0.15] + = {{2, 2}, {2, 3}} + + >> img = Import["ExampleData/lena.tif"]; + >> PixelValuePositions[img, 3 / 255, 0.5 / 255] + = {{180, 192, 2}, {181, 192, 2}, {181, 193, 2}, {188, 204, 2}, {265, 314, 2}, {364, 77, 2}, {365, 72, 2}, {365, 73, 2}, {365, 77, 2}, {366, 70, 2}, {367, 65, 2}} + >> PixelValue[img, {180, 192}] + = {0.25098, 0.0117647, 0.215686} ''' - def apply(self, image, val, evaluation): - 'PixelValuePositions[image_Image, val_?RealNumberQ]' - rows, cols = numpy.where(pixels_as_float(image.pixels) == float(val.round_to_float())) - p = numpy.dstack((cols, rows)) + numpy.array([1, 1]) - return from_python(p.tolist()) + rules = { + 'PixelValuePositions[image_Image, val_?RealNumberQ]': 'PixelValuePositions[image, val, 0]', + } + + def apply(self, image, val, d, evaluation): + 'PixelValuePositions[image_Image, val_?RealNumberQ, d_?RealNumberQ]' + val = val.round_to_float() + d = d.round_to_float() + + positions = numpy.argwhere(numpy.isclose(pixels_as_float(image.pixels), val, atol=d, rtol=0)) + + # python indexes from 0 at top left -> indices from 1 starting at bottom left + # if single channel then ommit channel indices + height = image.pixels.shape[0] + if image.pixels.shape[2] == 1: + result = sorted((j + 1, height - i) for i, j, k in positions.tolist()) + else: + result = sorted((j + 1, height - i, k + 1) for i, j, k in positions.tolist()) + return Expression('List', *(Expression('List', *arg) for arg in result)) # image attribute queries @@ -1726,6 +1915,11 @@ class ImageDimensions(_ImageBuiltin): >> ImageDimensions[RandomImage[1, {50, 70}]] = {50, 70} + + #> Image[{{0, 1}, {1, 0}, {1, 1}}] // ImageDimensions + = {2, 3} + #> Image[{{0.2, 0.4}, {0.9, 0.6}, {0.3, 0.8}}] // ImageDimensions + = {2, 3} ''' def apply(self, image, evaluation): 'ImageDimensions[image_Image]' @@ -1738,12 +1932,19 @@ class ImageAspectRatio(_ImageBuiltin):
'ImageAspectRatio[$image$]'
gives the aspect ratio of $image$. + + >> img = Import["ExampleData/lena.tif"]; + >> ImageAspectRatio[img] + = 1 + + >> ImageAspectRatio[Image[{{0, 1}, {1, 0}, {1, 1}}]] + = 3 / 2 ''' def apply(self, image, evaluation): 'ImageAspectRatio[image_Image]' dim = image.dimensions() - return Real(dim[1] / float(dim[0])) + return Expression('Divide', Integer(dim[1]), Integer(dim[0])) class ImageChannels(_ImageBuiltin): @@ -1752,6 +1953,13 @@ class ImageChannels(_ImageBuiltin):
'ImageChannels[$image$]'
gives the number of channels in $image$. + + >> ImageChannels[Image[{{0, 1}, {1, 0}}]] + = 1 + + >> img = Import["ExampleData/lena.tif"]; + >> ImageChannels[img] + = 3 ''' def apply(self, image, evaluation): @@ -1765,6 +1973,17 @@ class ImageType(_ImageBuiltin):
'ImageType[$image$]'
gives the interval storage type of $image$, e.g. "Real", "Bit32", or "Bit". + + >> img = Import["ExampleData/lena.tif"]; + >> ImageType[img] + = Byte + + >> ImageType[Image[{{0, 1}, {1, 0}}]] + = Real + + >> ImageType[Binarize[img]] + = Bit + ''' def apply(self, image, evaluation): @@ -1778,6 +1997,13 @@ class BinaryImageQ(_ImageTest):
'BinaryImageQ[$image]'
returns True if the pixels of $image are binary bit values, and False otherwise. + + >> img = Import["ExampleData/lena.tif"]; + >> BinaryImageQ[img] + = False + + >> BinaryImageQ[Binarize[img]] + = True ''' def test(self, expr):