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):