1# -*- coding: utf-8 -*- 2""" 3Image[] and image related functions. 4 5Note that you (currently) need scikit-image installed in order for this module to work. 6""" 7from mathics.version import __version__ # noqa used in loading to check consistency. 8from mathics.builtin.base import Builtin, AtomBuiltin, Test, BoxConstruct, String 9from mathics.core.expression import ( 10 Atom, 11 Expression, 12 Integer, 13 Integer0, 14 Integer1, 15 Rational, 16 Real, 17 MachineReal, 18 Symbol, 19 SymbolNull, 20 SymbolList, 21 SymbolRule, 22 from_python, 23) 24from mathics.builtin.drawing.colors import ( 25 convert as convert_color, 26 colorspaces as known_colorspaces, 27) 28 29import base64 30import functools 31import itertools 32import math 33from collections import defaultdict 34 35_image_requires = ("numpy", "PIL") 36 37_skimage_requires = _image_requires + ("skimage", "scipy", "matplotlib", "networkx") 38 39try: 40 import warnings 41 42 import PIL 43 import PIL.ImageEnhance 44 import PIL.ImageOps 45 import PIL.ImageFilter 46 from PIL.ExifTags import TAGS as ExifTags 47 48 import numpy 49 50 _enabled = True 51except ImportError: 52 _enabled = False 53 54from io import BytesIO 55 56 57class _ImageBuiltin(Builtin): 58 requires = _image_requires 59 60 61class _ImageTest(Test): 62 requires = _image_requires 63 64 65class _SkimageBuiltin(_ImageBuiltin): 66 requires = _skimage_requires 67 68 69# helpers 70 71 72def pixels_as_float(pixels): 73 dtype = pixels.dtype 74 if dtype in (numpy.float32, numpy.float64): 75 return pixels 76 elif dtype == numpy.uint8: 77 return pixels.astype(numpy.float32) / 255.0 78 elif dtype == numpy.uint16: 79 return pixels.astype(numpy.float32) / 65535.0 80 elif dtype == numpy.bool: 81 return pixels.astype(numpy.float32) 82 else: 83 raise NotImplementedError 84 85 86def pixels_as_ubyte(pixels): 87 dtype = pixels.dtype 88 if dtype in (numpy.float32, numpy.float64): 89 pixels = numpy.maximum(numpy.minimum(pixels, 1.0), 0.0) 90 return (pixels * 255.0).astype(numpy.uint8) 91 elif dtype == numpy.uint8: 92 return pixels 93 elif dtype == numpy.uint16: 94 return (pixels / 256).astype(numpy.uint8) 95 elif dtype == numpy.bool: 96 return pixels.astype(numpy.uint8) * 255 97 else: 98 raise NotImplementedError 99 100 101def pixels_as_uint(pixels): 102 dtype = pixels.dtype 103 if dtype in (numpy.float32, numpy.float64): 104 pixels = numpy.maximum(numpy.minimum(pixels, 1.0), 0.0) 105 return (pixels * 65535.0).astype(numpy.uint16) 106 elif dtype == numpy.uint8: 107 return pixels.astype(numpy.uint16) * 256 108 elif dtype == numpy.uint16: 109 return pixels 110 elif dtype == numpy.bool: 111 return pixels.astype(numpy.uint8) * 65535 112 else: 113 raise NotImplementedError 114 115 116def matrix_to_numpy(a): 117 def matrix(): 118 for y in a.leaves: 119 yield [x.round_to_float() for x in y.leaves] 120 121 return numpy.array(list(matrix())) 122 123 124def numpy_to_matrix(pixels): 125 channels = pixels.shape[2] 126 if channels == 1: 127 return pixels[:, :, 0].tolist() 128 else: 129 return pixels.tolist() 130 131 132def numpy_flip(pixels, axis): 133 f = (numpy.flipud, numpy.fliplr)[axis] 134 return f(pixels) 135 136 137def convolve(in1, in2, fixed=True): 138 # a very much boiled down version scipy.signal.signaltools.fftconvolve with added padding, see 139 # https://github.com/scipy/scipy/blob/master/scipy/signal/signaltools.py; please see the Scipy 140 # LICENSE in the accompanying files. 141 142 in1 = numpy.asarray(in1) 143 in2 = numpy.asarray(in2) 144 145 padding = numpy.array(in2.shape) // 2 146 if fixed: # add "Fixed" padding? 147 in1 = numpy.pad(in1, padding, "edge") 148 149 s1 = numpy.array(in1.shape) 150 s2 = numpy.array(in2.shape) 151 shape = s1 + s2 - 1 152 153 sp1 = numpy.fft.rfftn(in1, shape) 154 sp2 = numpy.fft.rfftn(in2, shape) 155 ret = numpy.fft.irfftn(sp1 * sp2, shape) 156 157 excess = (numpy.array(ret.shape) - s1) // 2 + padding 158 return ret[tuple(slice(p, -p) for p in excess)] 159 160 161# import and export 162 163 164class _Exif: 165 _names = { # names overriding the ones given by Pillow 166 37385: "FlashInfo", 167 40960: "FlashpixVersion", 168 40962: "PixelXDimension", 169 40963: "PixelYDimension", 170 } 171 172 @staticmethod 173 def extract(im, evaluation): 174 if hasattr(im, "_getexif"): 175 exif = im._getexif() 176 if not exif: 177 return 178 179 for k, v in sorted(exif.items(), key=lambda x: x[0]): 180 name = ExifTags.get(k) 181 if not name: 182 continue 183 184 # EXIF has the following types: Short, Long, Rational, Ascii, Byte 185 # (see http://www.exiv2.org/tags.html). we detect the type from the 186 # Python type Pillow gives us and do the appropiate MMA handling. 187 188 if isinstance(v, tuple) and len(v) == 2: # Rational 189 value = Rational(v[0], v[1]) 190 if name == "FocalLength": 191 value = value.round(2) 192 else: 193 value = Expression("Simplify", value).evaluate(evaluation) 194 elif isinstance(v, bytes): # Byte 195 value = String(" ".join(["%d" % x for x in v])) 196 elif isinstance(v, (int, str)): # Short, Long, Ascii 197 value = v 198 else: 199 continue 200 201 yield Expression(SymbolRule, String(_Exif._names.get(k, name)), value) 202 203 204class ImageImport(_ImageBuiltin): 205 """ 206 ## Image 207 >> Import["ExampleData/Einstein.jpg"] 208 = -Image- 209 #> Import["ExampleData/sunflowers.jpg"] 210 = -Image- 211 >> Import["ExampleData/MadTeaParty.gif"] 212 = -Image- 213 >> Import["ExampleData/moon.tif"] 214 = -Image- 215 #> Import["ExampleData/lena.tif"] 216 = -Image- 217 """ 218 219 def apply(self, path, evaluation): 220 """ImageImport[path_?StringQ]""" 221 pillow = PIL.Image.open(path.get_string_value()) 222 pixels = numpy.asarray(pillow) 223 is_rgb = len(pixels.shape) >= 3 and pixels.shape[2] >= 3 224 exif = Expression(SymbolList, *list(_Exif.extract(pillow, evaluation))) 225 226 image = Image(pixels, "RGB" if is_rgb else "Grayscale") 227 return Expression( 228 "List", 229 Expression(SymbolRule, String("Image"), image), 230 Expression(SymbolRule, String("ColorSpace"), String(image.color_space)), 231 Expression( 232 SymbolRule, String("ImageSize"), from_python(image.dimensions()) 233 ), 234 Expression(SymbolRule, String("RawExif"), exif), 235 ) 236 237 238class ImageExport(_ImageBuiltin): 239 messages = {"noimage": "only an Image[] can be exported into an image file"} 240 241 def apply(self, path, expr, opts, evaluation): 242 """ImageExport[path_?StringQ, expr_, opts___]""" 243 if isinstance(expr, Image): 244 expr.pil().save(path.get_string_value()) 245 return SymbolNull 246 else: 247 return evaluation.message("ImageExport", "noimage") 248 249 250# image math 251 252 253class _ImageArithmetic(_ImageBuiltin): 254 messages = {"bddarg": "Expecting a number, image, or graphics instead of `1`."} 255 256 @staticmethod 257 def convert_Image(image): 258 assert isinstance(image, Image) 259 return pixels_as_float(image.pixels) 260 261 @staticmethod 262 def convert_args(*args): 263 images = [] 264 for arg in args: 265 if isinstance(arg, Image): 266 images.append(_ImageArithmetic.convert_Image(arg)) 267 elif isinstance(arg, (Integer, Rational, Real)): 268 images.append(float(arg.to_python())) 269 else: 270 return None, arg 271 return images, None 272 273 @staticmethod 274 def _reduce(iterable, ufunc): 275 result = None 276 for i in iterable: 277 if result is None: 278 # ufunc is destructive so copy first 279 result = numpy.copy(i) 280 else: 281 # e.g. result *= i 282 ufunc(result, i, result) 283 return result 284 285 def apply(self, image, args, evaluation): 286 "%(name)s[image_Image, args__]" 287 images, arg = self.convert_args(image, *args.get_sequence()) 288 if images is None: 289 return evaluation.message(self.get_name(), "bddarg", arg) 290 ufunc = getattr(numpy, self.get_name(True)[5:].lower()) 291 result = self._reduce(images, ufunc).clip(0, 1) 292 return Image(result, image.color_space) 293 294 295class ImageAdd(_ImageArithmetic): 296 """ 297 <dl> 298 <dt>'ImageAdd[$image$, $expr_1$, $expr_2$, ...]' 299 <dd>adds all $expr_i$ to $image$ where each $expr_i$ must be an image or a real number. 300 </dl> 301 302 >> i = Image[{{0, 0.5, 0.2, 0.1, 0.9}, {1.0, 0.1, 0.3, 0.8, 0.6}}]; 303 304 >> ImageAdd[i, 0.5] 305 = -Image- 306 307 >> ImageAdd[i, i] 308 = -Image- 309 310 #> ImageAdd[i, 0.2, i, 0.1] 311 = -Image- 312 313 #> ImageAdd[i, x] 314 : Expecting a number, image, or graphics instead of x. 315 = ImageAdd[-Image-, x] 316 317 >> ein = Import["ExampleData/Einstein.jpg"]; 318 >> noise = RandomImage[{-0.1, 0.1}, ImageDimensions[ein]]; 319 >> ImageAdd[noise, ein] 320 = -Image- 321 322 >> lena = Import["ExampleData/lena.tif"]; 323 >> noise = RandomImage[{-0.2, 0.2}, ImageDimensions[lena], ColorSpace -> "RGB"]; 324 >> ImageAdd[noise, lena] 325 = -Image- 326 """ 327 328 329class ImageSubtract(_ImageArithmetic): 330 """ 331 <dl> 332 <dt>'ImageSubtract[$image$, $expr_1$, $expr_2$, ...]' 333 <dd>subtracts all $expr_i$ from $image$ where each $expr_i$ must be an image or a real number. 334 </dl> 335 336 >> i = Image[{{0, 0.5, 0.2, 0.1, 0.9}, {1.0, 0.1, 0.3, 0.8, 0.6}}]; 337 338 >> ImageSubtract[i, 0.2] 339 = -Image- 340 341 >> ImageSubtract[i, i] 342 = -Image- 343 344 #> ImageSubtract[i, 0.2, i, 0.1] 345 = -Image- 346 347 #> ImageSubtract[i, x] 348 : Expecting a number, image, or graphics instead of x. 349 = ImageSubtract[-Image-, x] 350 """ 351 352 353class ImageMultiply(_ImageArithmetic): 354 """ 355 <dl> 356 <dt>'ImageMultiply[$image$, $expr_1$, $expr_2$, ...]' 357 <dd>multiplies all $expr_i$ with $image$ where each $expr_i$ must be an image or a real number. 358 </dl> 359 360 >> i = Image[{{0, 0.5, 0.2, 0.1, 0.9}, {1.0, 0.1, 0.3, 0.8, 0.6}}]; 361 362 >> ImageMultiply[i, 0.2] 363 = -Image- 364 365 >> ImageMultiply[i, i] 366 = -Image- 367 368 #> ImageMultiply[i, 0.2, i, 0.1] 369 = -Image- 370 371 #> ImageMultiply[i, x] 372 : Expecting a number, image, or graphics instead of x. 373 = ImageMultiply[-Image-, x] 374 375 >> ein = Import["ExampleData/Einstein.jpg"]; 376 >> noise = RandomImage[{0.7, 1.3}, ImageDimensions[ein]]; 377 >> ImageMultiply[noise, ein] 378 = -Image- 379 """ 380 381 382class RandomImage(_ImageBuiltin): 383 """ 384 <dl> 385 <dt>'RandomImage[$max$]' 386 <dd>creates an image of random pixels with values 0 to $max$. 387 <dt>'RandomImage[{$min$, $max$}]' 388 <dd>creates an image of random pixels with values $min$ to $max$. 389 <dt>'RandomImage[..., $size$]' 390 <dd>creates an image of the given $size$. 391 </dl> 392 393 >> RandomImage[1, {100, 100}] 394 = -Image- 395 396 #> RandomImage[0.5] 397 = -Image- 398 #> RandomImage[{0.1, 0.9}] 399 = -Image- 400 #> RandomImage[0.9, {400, 600}] 401 = -Image- 402 #> RandomImage[{0.1, 0.5}, {400, 600}] 403 = -Image- 404 405 #> RandomImage[{0.1, 0.5}, {400, 600}, ColorSpace -> "RGB"] 406 = -Image- 407 """ 408 409 options = {"ColorSpace": "Automatic"} 410 411 rules = { 412 "RandomImage[]": "RandomImage[{0, 1}, {150, 150}]", 413 "RandomImage[max_?RealNumberQ]": "RandomImage[{0, max}, {150, 150}]", 414 "RandomImage[{minval_?RealNumberQ, maxval_?RealNumberQ}]": "RandomImage[{minval, maxval}, {150, 150}]", 415 "RandomImage[max_?RealNumberQ, {w_Integer, h_Integer}]": "RandomImage[{0, max}, {w, h}]", 416 } 417 418 messages = { 419 "bddim": "The specified dimension `1` should be a pair of positive integers.", 420 "imgcstype": "`1` is an invalid color space specification.", 421 } 422 423 def apply(self, minval, maxval, w, h, evaluation, options): 424 "RandomImage[{minval_?RealNumberQ, maxval_?RealNumberQ}, {w_Integer, h_Integer}, OptionsPattern[RandomImage]]" 425 color_space = self.get_option(options, "ColorSpace", evaluation) 426 if ( 427 isinstance(color_space, Symbol) 428 and color_space.get_name() == "System`Automatic" 429 ): 430 cs = "Grayscale" 431 else: 432 cs = color_space.get_string_value() 433 size = [w.get_int_value(), h.get_int_value()] 434 if size[0] <= 0 or size[1] <= 0: 435 return evaluation.message("RandomImage", "bddim", from_python(size)) 436 minrange, maxrange = minval.round_to_float(), maxval.round_to_float() 437 438 if cs == "Grayscale": 439 data = ( 440 numpy.random.rand(size[1], size[0]) * (maxrange - minrange) + minrange 441 ) 442 elif cs == "RGB": 443 data = ( 444 numpy.random.rand(size[1], size[0], 3) * (maxrange - minrange) 445 + minrange 446 ) 447 else: 448 return evaluation.message("RandomImage", "imgcstype", color_space) 449 return Image(data, cs) 450 451 452# simple image manipulation 453 454 455class ImageResize(_ImageBuiltin): 456 """ 457 <dl> 458 <dt>'ImageResize[$image$, $width$]' 459 <dd> 460 <dt>'ImageResize[$image$, {$width$, $height$}]' 461 <dd> 462 </dl> 463 464 >> ein = Import["ExampleData/Einstein.jpg"]; 465 >> ImageDimensions[ein] 466 = {615, 768} 467 >> ImageResize[ein, {400, 600}] 468 = -Image- 469 #> ImageDimensions[%] 470 = {400, 600} 471 472 >> ImageResize[ein, 256] 473 = -Image- 474 >> ImageDimensions[%] 475 = {256, 320} 476 477 The default sampling method is Bicubic 478 >> ImageResize[ein, 256, Resampling -> "Bicubic"] 479 = -Image- 480 #> ImageDimensions[%] 481 = {256, 320} 482 >> ImageResize[ein, 256, Resampling -> "Nearest"] 483 = -Image- 484 #> ImageDimensions[%] 485 = {256, 320} 486 >> ImageResize[ein, 256, Resampling -> "Gaussian"] 487 = -Image- 488 #> ImageDimensions[%] 489 = {256, 320} 490 #> ImageResize[ein, {256, 256}, Resampling -> "Gaussian"] 491 : Gaussian resampling needs to maintain aspect ratio. 492 = ImageResize[-Image-, {256, 256}, Resampling -> Gaussian] 493 #> ImageResize[ein, 256, Resampling -> "Invalid"] 494 : Invalid resampling method Invalid. 495 = ImageResize[-Image-, 256, Resampling -> Invalid] 496 497 #> ImageDimensions[ImageResize[ein, {256}]] 498 = {256, 256} 499 500 #> ImageResize[ein, {x}] 501 : The size {x} is not a valid image size specification. 502 = ImageResize[-Image-, {x}] 503 #> ImageResize[ein, x] 504 : The size x is not a valid image size specification. 505 = ImageResize[-Image-, x] 506 """ 507 508 options = {"Resampling": "Automatic"} 509 510 messages = { 511 "imgrssz": "The size `1` is not a valid image size specification.", 512 "imgrsm": "Invalid resampling method `1`.", 513 "gaussaspect": "Gaussian resampling needs to maintain aspect ratio.", 514 "skimage": "Please install scikit-image to use Resampling -> Gaussian.", 515 } 516 517 def _get_image_size_spec(self, old_size, new_size): 518 predefined_sizes = { 519 "System`Tiny": 75, 520 "System`Small": 150, 521 "System`Medium": 300, 522 "System`Large": 450, 523 "System`Automatic": 0, # placeholder 524 } 525 result = new_size.round_to_float() 526 if result is not None: 527 result = int(result) 528 if result <= 0: 529 return None 530 return result 531 532 if isinstance(new_size, Symbol): 533 name = new_size.get_name() 534 if name == "System`All": 535 return old_size 536 return predefined_sizes.get(name, None) 537 if new_size.has_form("Scaled", 1): 538 s = new_size.leaves[0].round_to_float() 539 if s is None: 540 return None 541 return max(1, old_size * s) # handle negative s values silently 542 return None 543 544 def apply_resize_width(self, image, s, evaluation, options): 545 "ImageResize[image_Image, s_, OptionsPattern[ImageResize]]" 546 old_w = image.pixels.shape[1] 547 if s.has_form("List", 1): 548 width = s.leaves[0] 549 else: 550 width = s 551 w = self._get_image_size_spec(old_w, width) 552 if w is None: 553 return evaluation.message("ImageResize", "imgrssz", s) 554 if s.has_form("List", 1): 555 height = width 556 else: 557 height = Symbol("Automatic") 558 return self.apply_resize_width_height(image, width, height, evaluation, options) 559 560 def apply_resize_width_height(self, image, width, height, evaluation, options): 561 "ImageResize[image_Image, {width_, height_}, OptionsPattern[ImageResize]]" 562 # resampling method 563 resampling = self.get_option(options, "Resampling", evaluation) 564 if ( 565 isinstance(resampling, Symbol) 566 and resampling.get_name() == "System`Automatic" 567 ): 568 resampling_name = "Bicubic" 569 else: 570 resampling_name = resampling.get_string_value() 571 572 # find new size 573 old_w, old_h = image.pixels.shape[1], image.pixels.shape[0] 574 w = self._get_image_size_spec(old_w, width) 575 h = self._get_image_size_spec(old_h, height) 576 if h is None or w is None: 577 return evaluation.message( 578 "ImageResize", "imgrssz", Expression(SymbolList, width, height) 579 ) 580 581 # handle Automatic 582 old_aspect_ratio = old_w / old_h 583 if w == 0 and h == 0: 584 # if both width and height are Automatic then use old values 585 w, h = old_w, old_h 586 elif w == 0: 587 w = max(1, h * old_aspect_ratio) 588 elif h == 0: 589 h = max(1, w / old_aspect_ratio) 590 591 if resampling_name != "Gaussian": 592 # Gaussian need to unrounded values to compute scaling ratios. 593 # round to closest pixel for other methods. 594 h, w = int(round(h)), int(round(w)) 595 596 # perform the resize 597 if resampling_name == "Nearest": 598 return image.filter( 599 lambda im: im.resize((w, h), resample=PIL.Image.NEAREST) 600 ) 601 elif resampling_name == "Bicubic": 602 return image.filter( 603 lambda im: im.resize((w, h), resample=PIL.Image.BICUBIC) 604 ) 605 elif resampling_name != "Gaussian": 606 return evaluation.message("ImageResize", "imgrsm", resampling) 607 608 try: 609 from skimage import transform 610 611 multichannel = image.pixels.ndim == 3 612 613 sy = h / old_h 614 sx = w / old_w 615 if sy > sx: 616 err = abs((sy * old_w) - (sx * old_w)) 617 s = sy 618 else: 619 err = abs((sy * old_h) - (sx * old_h)) 620 s = sx 621 if err > 1.5: 622 # TODO overcome this limitation 623 return evaluation.message("ImageResize", "gaussaspect") 624 elif s > 1: 625 pixels = transform.pyramid_expand( 626 image.pixels, upscale=s, multichannel=multichannel 627 ).clip(0, 1) 628 else: 629 pixels = transform.pyramid_reduce( 630 image.pixels, multichannel=multichannel, downscale=(1.0 / s) 631 ).clip(0, 1) 632 633 return Image(pixels, image.color_space) 634 except ImportError: 635 evaluation.message("ImageResize", "skimage") 636 637 638class ImageReflect(_ImageBuiltin): 639 """ 640 <dl> 641 <dt>'ImageReflect[$image$]' 642 <dd>Flips $image$ top to bottom. 643 <dt>'ImageReflect[$image$, $side$]' 644 <dd>Flips $image$ so that $side$ is interchanged with its opposite. 645 <dt>'ImageReflect[$image$, $side_1$ -> $side_2$]' 646 <dd>Flips $image$ so that $side_1$ is interchanged with $side_2$. 647 </dl> 648 649 >> ein = Import["ExampleData/Einstein.jpg"]; 650 >> ImageReflect[ein] 651 = -Image- 652 >> ImageReflect[ein, Left] 653 = -Image- 654 >> ImageReflect[ein, Left -> Top] 655 = -Image- 656 657 #> ein == ImageReflect[ein, Left -> Left] == ImageReflect[ein, Right -> Right] == ImageReflect[ein, Top -> Top] == ImageReflect[ein, Bottom -> Bottom] 658 = True 659 #> ImageReflect[ein, Left -> Right] == ImageReflect[ein, Right -> Left] == ImageReflect[ein, Left] == ImageReflect[ein, Right] 660 = True 661 #> ImageReflect[ein, Bottom -> Top] == ImageReflect[ein, Top -> Bottom] == ImageReflect[ein, Top] == ImageReflect[ein, Bottom] 662 = True 663 #> ImageReflect[ein, Left -> Top] == ImageReflect[ein, Right -> Bottom] (* Transpose *) 664 = True 665 #> ImageReflect[ein, Left -> Bottom] == ImageReflect[ein, Right -> Top] (* Anti-Transpose *) 666 = True 667 668 #> ImageReflect[ein, x -> Top] 669 : x -> Top is not a valid 2D reflection specification. 670 = ImageReflect[-Image-, x -> Top] 671 """ 672 673 rules = { 674 "ImageReflect[image_Image]": "ImageReflect[image, Top -> Bottom]", 675 "ImageReflect[image_Image, Top|Bottom]": "ImageReflect[image, Top -> Bottom]", 676 "ImageReflect[image_Image, Left|Right]": "ImageReflect[image, Left -> Right]", 677 } 678 679 messages = {"bdrfl2": "`1` is not a valid 2D reflection specification."} 680 681 def apply(self, image, orig, dest, evaluation): 682 "ImageReflect[image_Image, Rule[orig_, dest_]]" 683 if isinstance(orig, Symbol) and isinstance(dest, Symbol): 684 specs = [orig.get_name(), dest.get_name()] 685 specs.sort() # `Top -> Bottom` is the same as `Bottom -> Top` 686 687 anti_transpose = lambda i: numpy.flipud(numpy.transpose(numpy.flipud(i))) 688 no_op = lambda i: i 689 690 method = { 691 ("System`Bottom", "System`Top"): numpy.flipud, 692 ("System`Left", "System`Right"): numpy.fliplr, 693 ("System`Left", "System`Top"): numpy.transpose, 694 ("System`Right", "System`Top"): anti_transpose, 695 ("System`Bottom", "System`Left"): anti_transpose, 696 ("System`Bottom", "System`Right"): numpy.transpose, 697 ("System`Bottom", "System`Bottom"): no_op, 698 ("System`Top", "System`Top"): no_op, 699 ("System`Left", "System`Left"): no_op, 700 ("System`Right", "System`Right"): no_op, 701 }.get(tuple(specs), None) 702 703 if method is None: 704 return evaluation.message( 705 "ImageReflect", "bdrfl2", Expression(SymbolRule, orig, dest) 706 ) 707 708 return Image(method(image.pixels), image.color_space) 709 710 711class ImageRotate(_ImageBuiltin): 712 """ 713 <dl> 714 <dt>'ImageRotate[$image$]' 715 <dd>Rotates $image$ 90 degrees counterclockwise. 716 <dt>'ImageRotate[$image$, $theta$]' 717 <dd>Rotates $image$ by a given angle $theta$ 718 </dl> 719 720 >> ein = Import["ExampleData/Einstein.jpg"]; 721 722 >> ImageRotate[ein] 723 = -Image- 724 725 >> ImageRotate[ein, 45 Degree] 726 = -Image- 727 728 >> ImageRotate[ein, Pi / 2] 729 = -Image- 730 731 #> ImageRotate[ein, ein] 732 : Angle -Image- should be a real number, one of Top, Bottom, Left, Right, or a rule from one to another. 733 = ImageRotate[-Image-, -Image-] 734 """ 735 736 rules = {"ImageRotate[i_Image]": "ImageRotate[i, 90 Degree]"} 737 738 messages = { 739 "imgang": "Angle `1` should be a real number, one of Top, Bottom, Left, Right, or a rule from one to another." 740 } 741 742 def apply(self, image, angle, evaluation): 743 "ImageRotate[image_Image, angle_]" 744 py_angle = angle.round_to_float(evaluation) 745 if py_angle is None: 746 return evaluation.message("ImageRotate", "imgang", angle) 747 748 def rotate(im): 749 return im.rotate( 750 180 * py_angle / math.pi, resample=PIL.Image.BICUBIC, expand=True 751 ) 752 753 return image.filter(rotate) 754 755 756class ImagePartition(_ImageBuiltin): 757 """ 758 <dl> 759 <dt>'ImagePartition[$image$, $s$]' 760 <dd>Partitions an image into an array of $s$ x $s$ pixel subimages. 761 <dt>'ImagePartition[$image$, {$w$, $h$}]' 762 <dd>Partitions an image into an array of $w$ x $h$ pixel subimages. 763 </dl> 764 765 >> lena = Import["ExampleData/lena.tif"]; 766 >> ImageDimensions[lena] 767 = {512, 512} 768 >> ImagePartition[lena, 256] 769 = {{-Image-, -Image-}, {-Image-, -Image-}} 770 771 >> ImagePartition[lena, {512, 128}] 772 = {{-Image-}, {-Image-}, {-Image-}, {-Image-}} 773 774 #> ImagePartition[lena, 257] 775 = {{-Image-}} 776 #> ImagePartition[lena, 512] 777 = {{-Image-}} 778 #> ImagePartition[lena, 513] 779 = {} 780 #> ImagePartition[lena, {256, 300}] 781 = {{-Image-, -Image-}} 782 783 #> ImagePartition[lena, {0, 300}] 784 : {0, 300} is not a valid size specification for image partitions. 785 = ImagePartition[-Image-, {0, 300}] 786 """ 787 788 rules = {"ImagePartition[i_Image, s_Integer]": "ImagePartition[i, {s, s}]"} 789 790 messages = {"arg2": "`1` is not a valid size specification for image partitions."} 791 792 def apply(self, image, w, h, evaluation): 793 "ImagePartition[image_Image, {w_Integer, h_Integer}]" 794 w = w.get_int_value() 795 h = h.get_int_value() 796 if w <= 0 or h <= 0: 797 return evaluation.message("ImagePartition", "arg2", from_python([w, h])) 798 pixels = image.pixels 799 shape = pixels.shape 800 801 # drop blocks less than w x h 802 parts = [] 803 for yi in range(shape[0] // h): 804 row = [] 805 for xi in range(shape[1] // w): 806 p = pixels[yi * h : (yi + 1) * h, xi * w : (xi + 1) * w] 807 row.append(Image(p, image.color_space)) 808 if row: 809 parts.append(row) 810 return from_python(parts) 811 812 813# simple image filters 814 815 816class ImageAdjust(_ImageBuiltin): 817 """ 818 <dl> 819 <dt>'ImageAdjust[$image$]' 820 <dd>adjusts the levels in $image$. 821 <dt>'ImageAdjust[$image$, $c$]' 822 <dd>adjusts the contrast in $image$ by $c$. 823 <dt>'ImageAdjust[$image$, {$c$, $b$}]' 824 <dd>adjusts the contrast $c$, and brightness $b$ in $image$. 825 <dt>'ImageAdjust[$image$, {$c$, $b$, $g$}]' 826 <dd>adjusts the contrast $c$, brightness $b$, and gamma $g$ in $image$. 827 </dl> 828 829 >> lena = Import["ExampleData/lena.tif"]; 830 >> ImageAdjust[lena] 831 = -Image- 832 """ 833 834 rules = { 835 "ImageAdjust[image_Image, c_?RealNumberQ]": "ImageAdjust[image, {c, 0, 1}]", 836 "ImageAdjust[image_Image, {c_?RealNumberQ, b_?RealNumberQ}]": "ImageAdjust[image, {c, b, 1}]", 837 } 838 839 def apply_auto(self, image, evaluation): 840 "ImageAdjust[image_Image]" 841 pixels = pixels_as_float(image.pixels) 842 843 # channel limits 844 axis = (0, 1) 845 cmaxs, cmins = pixels.max(axis=axis), pixels.min(axis=axis) 846 847 # normalise channels 848 scales = cmaxs - cmins 849 if not scales.shape: 850 scales = numpy.array([scales]) 851 scales[scales == 0.0] = 1 852 pixels -= cmins 853 pixels /= scales 854 return Image(pixels, image.color_space) 855 856 def apply_contrast_brightness_gamma(self, image, c, b, g, evaluation): 857 "ImageAdjust[image_Image, {c_?RealNumberQ, b_?RealNumberQ, g_?RealNumberQ}]" 858 859 im = image.pil() 860 861 # gamma 862 g = g.round_to_float() 863 if g != 1: 864 im = PIL.ImageEnhance.Color(im).enhance(g) 865 866 # brightness 867 b = b.round_to_float() 868 if b != 0: 869 im = PIL.ImageEnhance.Brightness(im).enhance(b + 1) 870 871 # contrast 872 c = c.round_to_float() 873 if c != 0: 874 im = PIL.ImageEnhance.Contrast(im).enhance(c + 1) 875 876 return Image(numpy.array(im), image.color_space) 877 878 879class Blur(_ImageBuiltin): 880 """ 881 <dl> 882 <dt>'Blur[$image$]' 883 <dd>gives a blurred version of $image$. 884 <dt>'Blur[$image$, $r$]' 885 <dd>blurs $image$ with a kernel of size $r$. 886 </dl> 887 888 >> lena = Import["ExampleData/lena.tif"]; 889 >> Blur[lena] 890 = -Image- 891 >> Blur[lena, 5] 892 = -Image- 893 """ 894 895 rules = { 896 "Blur[image_Image]": "Blur[image, 2]", 897 "Blur[image_Image, r_?RealNumberQ]": "ImageConvolve[image, BoxMatrix[r] / Total[Flatten[BoxMatrix[r]]]]", 898 } 899 900 901class Sharpen(_ImageBuiltin): 902 """ 903 <dl> 904 <dt>'Sharpen[$image$]' 905 <dd>gives a sharpened version of $image$. 906 <dt>'Sharpen[$image$, $r$]' 907 <dd>sharpens $image$ with a kernel of size $r$. 908 </dl> 909 910 >> lena = Import["ExampleData/lena.tif"]; 911 >> Sharpen[lena] 912 = -Image- 913 >> Sharpen[lena, 5] 914 = -Image- 915 """ 916 917 rules = {"Sharpen[i_Image]": "Sharpen[i, 2]"} 918 919 def apply(self, image, r, evaluation): 920 "Sharpen[image_Image, r_?RealNumberQ]" 921 f = PIL.ImageFilter.UnsharpMask(r.round_to_float()) 922 return image.filter(lambda im: im.filter(f)) 923 924 925class GaussianFilter(_ImageBuiltin): 926 """ 927 <dl> 928 <dt>'GaussianFilter[$image$, $r$]' 929 <dd>blurs $image$ using a Gaussian blur filter of radius $r$. 930 </dl> 931 932 >> lena = Import["ExampleData/lena.tif"]; 933 >> GaussianFilter[lena, 2.5] 934 = -Image- 935 """ 936 937 messages = {"only3": "GaussianFilter only supports up to three channels."} 938 939 def apply_radius(self, image, radius, evaluation): 940 "GaussianFilter[image_Image, radius_?RealNumberQ]" 941 if len(image.pixels.shape) > 2 and image.pixels.shape[2] > 3: 942 return evaluation.message("GaussianFilter", "only3") 943 else: 944 f = PIL.ImageFilter.GaussianBlur(radius.round_to_float()) 945 return image.filter(lambda im: im.filter(f)) 946 947 948# morphological image filters 949 950 951class PillowImageFilter(_ImageBuiltin): 952 def compute(self, image, f): 953 return image.filter(lambda im: im.filter(f)) 954 955 956class MinFilter(PillowImageFilter): 957 """ 958 <dl> 959 <dt>'MinFilter[$image$, $r$]' 960 <dd>gives $image$ with a minimum filter of radius $r$ applied on it. This always 961 picks the smallest value in the filter's area. 962 </dl> 963 964 >> lena = Import["ExampleData/lena.tif"]; 965 >> MinFilter[lena, 5] 966 = -Image- 967 """ 968 969 def apply(self, image, r, evaluation): 970 "MinFilter[image_Image, r_Integer]" 971 return self.compute(image, PIL.ImageFilter.MinFilter(1 + 2 * r.get_int_value())) 972 973 974class MaxFilter(PillowImageFilter): 975 """ 976 <dl> 977 <dt>'MaxFilter[$image$, $r$]' 978 <dd>gives $image$ with a maximum filter of radius $r$ applied on it. This always 979 picks the largest value in the filter's area. 980 </dl> 981 982 >> lena = Import["ExampleData/lena.tif"]; 983 >> MaxFilter[lena, 5] 984 = -Image- 985 """ 986 987 def apply(self, image, r, evaluation): 988 "MaxFilter[image_Image, r_Integer]" 989 return self.compute(image, PIL.ImageFilter.MaxFilter(1 + 2 * r.get_int_value())) 990 991 992class MedianFilter(PillowImageFilter): 993 """ 994 <dl> 995 <dt>'MedianFilter[$image$, $r$]' 996 <dd>gives $image$ with a median filter of radius $r$ applied on it. This always 997 picks the median value in the filter's area. 998 </dl> 999 1000 >> lena = Import["ExampleData/lena.tif"]; 1001 >> MedianFilter[lena, 5] 1002 = -Image- 1003 """ 1004 1005 def apply(self, image, r, evaluation): 1006 "MedianFilter[image_Image, r_Integer]" 1007 return self.compute( 1008 image, PIL.ImageFilter.MedianFilter(1 + 2 * r.get_int_value()) 1009 ) 1010 1011 1012class EdgeDetect(_SkimageBuiltin): 1013 """ 1014 <dl> 1015 <dt>'EdgeDetect[$image$]' 1016 <dd>returns an image showing the edges in $image$. 1017 </dl> 1018 1019 >> lena = Import["ExampleData/lena.tif"]; 1020 >> EdgeDetect[lena] 1021 = -Image- 1022 >> EdgeDetect[lena, 5] 1023 = -Image- 1024 >> EdgeDetect[lena, 4, 0.5] 1025 = -Image- 1026 """ 1027 1028 rules = { 1029 "EdgeDetect[i_Image]": "EdgeDetect[i, 2, 0.2]", 1030 "EdgeDetect[i_Image, r_?RealNumberQ]": "EdgeDetect[i, r, 0.2]", 1031 } 1032 1033 def apply(self, image, r, t, evaluation): 1034 "EdgeDetect[image_Image, r_?RealNumberQ, t_?RealNumberQ]" 1035 import skimage.feature 1036 1037 pixels = image.grayscale().pixels 1038 return Image( 1039 skimage.feature.canny( 1040 pixels.reshape(pixels.shape[:2]), 1041 sigma=r.round_to_float() / 2, 1042 low_threshold=0.5 * t.round_to_float(), 1043 high_threshold=t.round_to_float(), 1044 ), 1045 "Grayscale", 1046 ) 1047 1048 1049def _matrix(rows): 1050 return Expression(SymbolList, *[Expression(SymbolList, *r) for r in rows]) 1051 1052 1053class BoxMatrix(_ImageBuiltin): 1054 """ 1055 <dl> 1056 <dt>'BoxMatrix[$s]' 1057 <dd>Gives a box shaped kernel of size 2 $s$ + 1. 1058 </dl> 1059 1060 >> BoxMatrix[3] 1061 = {{1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}} 1062 """ 1063 1064 def apply(self, r, evaluation): 1065 "BoxMatrix[r_?RealNumberQ]" 1066 py_r = abs(r.round_to_float()) 1067 s = int(math.floor(1 + 2 * py_r)) 1068 return _matrix([[Integer1] * s] * s) 1069 1070 1071class DiskMatrix(_ImageBuiltin): 1072 """ 1073 <dl> 1074 <dt>'DiskMatrix[$s]' 1075 <dd>Gives a disk shaped kernel of size 2 $s$ + 1. 1076 </dl> 1077 1078 >> DiskMatrix[3] 1079 = {{0, 0, 1, 1, 1, 0, 0}, {0, 1, 1, 1, 1, 1, 0}, {1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1}, {0, 1, 1, 1, 1, 1, 0}, {0, 0, 1, 1, 1, 0, 0}} 1080 """ 1081 1082 def apply(self, r, evaluation): 1083 "DiskMatrix[r_?RealNumberQ]" 1084 py_r = abs(r.round_to_float()) 1085 s = int(math.floor(0.5 + py_r)) 1086 1087 m = (Integer0, Integer1) 1088 r_sqr = (py_r + 0.5) * (py_r + 0.5) 1089 1090 def rows(): 1091 for y in range(-s, s + 1): 1092 yield [m[int((x) * (x) + (y) * (y) <= r_sqr)] for x in range(-s, s + 1)] 1093 1094 return _matrix(rows()) 1095 1096 1097class DiamondMatrix(_ImageBuiltin): 1098 """ 1099 <dl> 1100 <dt>'DiamondMatrix[$s]' 1101 <dd>Gives a diamond shaped kernel of size 2 $s$ + 1. 1102 </dl> 1103 1104 >> DiamondMatrix[3] 1105 = {{0, 0, 0, 1, 0, 0, 0}, {0, 0, 1, 1, 1, 0, 0}, {0, 1, 1, 1, 1, 1, 0}, {1, 1, 1, 1, 1, 1, 1}, {0, 1, 1, 1, 1, 1, 0}, {0, 0, 1, 1, 1, 0, 0}, {0, 0, 0, 1, 0, 0, 0}} 1106 """ 1107 1108 def apply(self, r, evaluation): 1109 "DiamondMatrix[r_?RealNumberQ]" 1110 py_r = abs(r.round_to_float()) 1111 t = int(math.floor(0.5 + py_r)) 1112 1113 zero = Integer0 1114 one = Integer1 1115 1116 def rows(): 1117 for d in range(0, t): 1118 p = [zero] * (t - d) 1119 yield p + ([one] * (1 + d * 2)) + p 1120 1121 yield [one] * (2 * t + 1) 1122 1123 for d in reversed(range(0, t)): 1124 p = [zero] * (t - d) 1125 yield p + ([one] * (1 + d * 2)) + p 1126 1127 return _matrix(rows()) 1128 1129 1130class ImageConvolve(_ImageBuiltin): 1131 """ 1132 <dl> 1133 <dt>'ImageConvolve[$image$, $kernel$]' 1134 <dd>Computes the convolution of $image$ using $kernel$. 1135 </dl> 1136 1137 >> img = Import["ExampleData/lena.tif"]; 1138 >> ImageConvolve[img, DiamondMatrix[5] / 61] 1139 = -Image- 1140 >> ImageConvolve[img, DiskMatrix[5] / 97] 1141 = -Image- 1142 >> ImageConvolve[img, BoxMatrix[5] / 121] 1143 = -Image- 1144 """ 1145 1146 def apply(self, image, kernel, evaluation): 1147 "%(name)s[image_Image, kernel_?MatrixQ]" 1148 numpy_kernel = matrix_to_numpy(kernel) 1149 pixels = pixels_as_float(image.pixels) 1150 shape = pixels.shape[:2] 1151 channels = [] 1152 for c in (pixels[:, :, i] for i in range(pixels.shape[2])): 1153 channels.append(convolve(c.reshape(shape), numpy_kernel, fixed=True)) 1154 return Image(numpy.dstack(channels), image.color_space) 1155 1156 1157class _MorphologyFilter(_SkimageBuiltin): 1158 1159 messages = { 1160 "grayscale": "Your image has been converted to grayscale as color images are not supported yet." 1161 } 1162 1163 rules = {"%(name)s[i_Image, r_?RealNumberQ]": "%(name)s[i, BoxMatrix[r]]"} 1164 1165 def apply(self, image, k, evaluation): 1166 "%(name)s[image_Image, k_?MatrixQ]" 1167 if image.color_space != "Grayscale": 1168 image = image.grayscale() 1169 evaluation.message(self.get_name(), "grayscale") 1170 import skimage.morphology 1171 1172 f = getattr(skimage.morphology, self.get_name(True).lower()) 1173 shape = image.pixels.shape[:2] 1174 img = f(image.pixels.reshape(shape), matrix_to_numpy(k)) 1175 return Image(img, "Grayscale") 1176 1177 1178class Dilation(_MorphologyFilter): 1179 """ 1180 <dl> 1181 <dt>'Dilation[$image$, $ker$]' 1182 <dd>Gives the morphological dilation of $image$ with respect to structuring element $ker$. 1183 </dl> 1184 1185 >> ein = Import["ExampleData/Einstein.jpg"]; 1186 >> Dilation[ein, 2.5] 1187 = -Image- 1188 """ 1189 1190 1191class Erosion(_MorphologyFilter): 1192 """ 1193 <dl> 1194 <dt>'Erosion[$image$, $ker$]' 1195 <dd>Gives the morphological erosion of $image$ with respect to structuring element $ker$. 1196 </dl> 1197 1198 >> ein = Import["ExampleData/Einstein.jpg"]; 1199 >> Erosion[ein, 2.5] 1200 = -Image- 1201 """ 1202 1203 1204class Opening(_MorphologyFilter): 1205 """ 1206 <dl> 1207 <dt>'Opening[$image$, $ker$]' 1208 <dd>Gives the morphological opening of $image$ with respect to structuring element $ker$. 1209 </dl> 1210 1211 >> ein = Import["ExampleData/Einstein.jpg"]; 1212 >> Opening[ein, 2.5] 1213 = -Image- 1214 """ 1215 1216 1217class Closing(_MorphologyFilter): 1218 """ 1219 <dl> 1220 <dt>'Closing[$image$, $ker$]' 1221 <dd>Gives the morphological closing of $image$ with respect to structuring element $ker$. 1222 </dl> 1223 1224 >> ein = Import["ExampleData/Einstein.jpg"]; 1225 >> Closing[ein, 2.5] 1226 = -Image- 1227 """ 1228 1229 1230class MorphologicalComponents(_SkimageBuiltin): 1231 1232 rules = {"MorphologicalComponents[i_Image]": "MorphologicalComponents[i, 0]"} 1233 1234 def apply(self, image, t, evaluation): 1235 "MorphologicalComponents[image_Image, t_?RealNumberQ]" 1236 pixels = pixels_as_ubyte( 1237 pixels_as_float(image.grayscale().pixels) > t.round_to_float() 1238 ) 1239 import skimage.measure 1240 1241 return from_python( 1242 skimage.measure.label(pixels, background=0, connectivity=2).tolist() 1243 ) 1244 1245 1246# color space 1247 1248 1249class ImageColorSpace(_ImageBuiltin): 1250 """ 1251 <dl> 1252 <dt>'ImageColorSpace[$image$]' 1253 <dd>gives $image$'s color space, e.g. "RGB" or "CMYK". 1254 </dl> 1255 1256 >> img = Import["ExampleData/lena.tif"]; 1257 >> ImageColorSpace[img] 1258 = RGB 1259 """ 1260 1261 def apply(self, image, evaluation): 1262 "ImageColorSpace[image_Image]" 1263 return String(image.color_space) 1264 1265 1266class ColorConvert(Builtin): 1267 """ 1268 <dl> 1269 <dt>'ColorConvert[$c$, $colspace$]' 1270 <dd>returns the representation of $c$ in the color space $colspace$. $c$ 1271 may be a color or an image. 1272 </dl> 1273 1274 Valid values for $colspace$ are: 1275 1276 CMYK: convert to CMYKColor 1277 Grayscale: convert to GrayLevel 1278 HSB: convert to Hue 1279 LAB: concert to LABColor 1280 LCH: convert to LCHColor 1281 LUV: convert to LUVColor 1282 RGB: convert to RGBColor 1283 XYZ: convert to XYZColor 1284 """ 1285 1286 messages = { 1287 "ccvinput": "`` should be a color.", 1288 "imgcstype": "`` is not a valid color space.", 1289 } 1290 1291 def apply(self, input, colorspace, evaluation): 1292 "ColorConvert[input_, colorspace_String]" 1293 1294 if isinstance(input, Image): 1295 return input.color_convert(colorspace.get_string_value()) 1296 else: 1297 from mathics.builtin.graphics import ( 1298 expression_to_color, 1299 color_to_expression, 1300 ) 1301 1302 py_color = expression_to_color(input) 1303 if py_color is None: 1304 evaluation.message("ColorConvert", "ccvinput", input) 1305 return 1306 1307 py_colorspace = colorspace.get_string_value() 1308 converted_components = convert_color( 1309 py_color.components, py_color.color_space, py_colorspace 1310 ) 1311 1312 if converted_components is None: 1313 evaluation.message("ColorConvert", "imgcstype", colorspace) 1314 return 1315 1316 return color_to_expression(converted_components, py_colorspace) 1317 1318 1319class ColorQuantize(_ImageBuiltin): 1320 """ 1321 <dl> 1322 <dt>'ColorQuantize[$image$, $n$]' 1323 <dd>gives a version of $image$ using only $n$ colors. 1324 </dl> 1325 1326 >> img = Import["ExampleData/lena.tif"]; 1327 >> ColorQuantize[img, 6] 1328 = -Image- 1329 1330 #> ColorQuantize[img, 0] 1331 : Positive integer expected at position 2 in ColorQuantize[-Image-, 0]. 1332 = ColorQuantize[-Image-, 0] 1333 #> ColorQuantize[img, -1] 1334 : Positive integer expected at position 2 in ColorQuantize[-Image-, -1]. 1335 = ColorQuantize[-Image-, -1] 1336 """ 1337 1338 messages = {"intp": "Positive integer expected at position `2` in `1`."} 1339 1340 def apply(self, image, n, evaluation): 1341 "ColorQuantize[image_Image, n_Integer]" 1342 if n.get_int_value() <= 0: 1343 return evaluation.message( 1344 "ColorQuantize", "intp", Expression("ColorQuantize", image, n), 2 1345 ) 1346 converted = image.color_convert("RGB") 1347 if converted is None: 1348 return 1349 pixels = pixels_as_ubyte(converted.pixels) 1350 im = PIL.Image.fromarray(pixels).quantize(n.get_int_value()) 1351 im = im.convert("RGB") 1352 return Image(numpy.array(im), "RGB") 1353 1354 1355class Threshold(_SkimageBuiltin): 1356 """ 1357 <dl> 1358 <dt>'Threshold[$image$]' 1359 <dd>gives a value suitable for binarizing $image$. 1360 </dl> 1361 1362 The option "Method" may be "Cluster" (use Otsu's threshold), "Median", or "Mean". 1363 1364 >> img = Import["ExampleData/lena.tif"]; 1365 >> Threshold[img] 1366 = 0.456739 1367 >> Binarize[img, %] 1368 = -Image- 1369 >> Threshold[img, Method -> "Mean"] 1370 = 0.486458 1371 >> Threshold[img, Method -> "Median"] 1372 = 0.504726 1373 """ 1374 1375 options = {"Method": '"Cluster"'} 1376 1377 messages = { 1378 "illegalmethod": "Method `` is not supported.", 1379 "skimage": "Please install scikit-image to use Method -> Cluster.", 1380 } 1381 1382 def apply(self, image, evaluation, options): 1383 "Threshold[image_Image, OptionsPattern[Threshold]]" 1384 pixels = image.grayscale().pixels 1385 1386 method = self.get_option(options, "Method", evaluation) 1387 method_name = ( 1388 method.get_string_value() 1389 if isinstance(method, String) 1390 else method.to_python() 1391 ) 1392 if method_name == "Cluster": 1393 import skimage.filters 1394 1395 threshold = skimage.filters.threshold_otsu(pixels) 1396 elif method_name == "Median": 1397 threshold = numpy.median(pixels) 1398 elif method_name == "Mean": 1399 threshold = numpy.mean(pixels) 1400 else: 1401 return evaluation.message("Threshold", "illegalmethod", method) 1402 1403 return MachineReal(float(threshold)) 1404 1405 1406class Binarize(_SkimageBuiltin): 1407 """ 1408 <dl> 1409 <dt>'Binarize[$image$]' 1410 <dd>gives a binarized version of $image$, in which each pixel is either 0 or 1. 1411 <dt>'Binarize[$image$, $t$]' 1412 <dd>map values $x$ > $t$ to 1, and values $x$ <= $t$ to 0. 1413 <dt>'Binarize[$image$, {$t1$, $t2$}]' 1414 <dd>map $t1$ < $x$ < $t2$ to 1, and all other values to 0. 1415 </dl> 1416 1417 >> img = Import["ExampleData/lena.tif"]; 1418 >> Binarize[img] 1419 = -Image- 1420 >> Binarize[img, 0.7] 1421 = -Image- 1422 >> Binarize[img, {0.2, 0.6}] 1423 = -Image- 1424 """ 1425 1426 def apply(self, image, evaluation): 1427 "Binarize[image_Image]" 1428 image = image.grayscale() 1429 thresh = Expression("Threshold", image).evaluate(evaluation).round_to_float() 1430 if thresh is not None: 1431 return Image(image.pixels > thresh, "Grayscale") 1432 1433 def apply_t(self, image, t, evaluation): 1434 "Binarize[image_Image, t_?RealNumberQ]" 1435 pixels = image.grayscale().pixels 1436 return Image(pixels > t.round_to_float(), "Grayscale") 1437 1438 def apply_t1_t2(self, image, t1, t2, evaluation): 1439 "Binarize[image_Image, {t1_?RealNumberQ, t2_?RealNumberQ}]" 1440 pixels = image.grayscale().pixels 1441 mask1 = pixels > t1.round_to_float() 1442 mask2 = pixels < t2.round_to_float() 1443 return Image(mask1 * mask2, "Grayscale") 1444 1445 1446class ColorNegate(_ImageBuiltin): 1447 """ 1448 <dl> 1449 <dt>'ColorNegate[$image$]' 1450 <dd>returns the negative of $image$ in which colors have been negated. 1451 1452 <dt>'ColorNegate[$color$]' 1453 <dd>returns the negative of a color. 1454 1455 Yellow is RGBColor[1.0, 1.0, 0.0] 1456 >> ColorNegate[Yellow] 1457 = RGBColor[0., 0., 1.] 1458 </dl> 1459 """ 1460 1461 def apply_for_image(self, image, evaluation): 1462 "ColorNegate[image_Image]" 1463 return image.filter(lambda im: PIL.ImageOps.invert(im)) 1464 1465 def apply_for_color(self, color, evaluation): 1466 "ColorNegate[color_RGBColor]" 1467 # Get components 1468 r, g, b = [leaf.to_python() for leaf in color.leaves] 1469 # Invert 1470 r, g, b = (1.0 - r, 1.0 - g, 1.0 - b) 1471 # Reconstitute 1472 return Expression("RGBColor", Real(r), Real(g), Real(b)) 1473 1474 1475class ColorSeparate(_ImageBuiltin): 1476 """ 1477 <dl> 1478 <dt>'ColorSeparate[$image$]' 1479 <dd>Gives each channel of $image$ as a separate grayscale image. 1480 </dl> 1481 """ 1482 1483 def apply(self, image, evaluation): 1484 "ColorSeparate[image_Image]" 1485 images = [] 1486 pixels = image.pixels 1487 if len(pixels.shape) < 3: 1488 images.append(pixels) 1489 else: 1490 for i in range(pixels.shape[2]): 1491 images.append(Image(pixels[:, :, i], "Grayscale")) 1492 return Expression(SymbolList, *images) 1493 1494 1495class ColorCombine(_ImageBuiltin): 1496 """ 1497 <dl> 1498 <dt>'ColorCombine[$channels$, $colorspace$]' 1499 <dd>Gives an image with $colorspace$ and the respective components described by the given channels. 1500 </dl> 1501 1502 >> ColorCombine[{{{1, 0}, {0, 0.75}}, {{0, 1}, {0, 0.25}}, {{0, 0}, {1, 0.5}}}, "RGB"] 1503 = -Image- 1504 """ 1505 1506 def apply(self, channels, colorspace, evaluation): 1507 "ColorCombine[channels_List, colorspace_String]" 1508 1509 py_colorspace = colorspace.get_string_value() 1510 if py_colorspace not in known_colorspaces: 1511 return 1512 1513 numpy_channels = [] 1514 for channel in channels.leaves: 1515 if not Expression("MatrixQ", channel).evaluate(evaluation).is_true(): 1516 return 1517 numpy_channels.append(matrix_to_numpy(channel)) 1518 1519 if not numpy_channels: 1520 return 1521 1522 if not all(x.shape == numpy_channels[0].shape for x in numpy_channels[1:]): 1523 return 1524 1525 return Image(numpy.dstack(numpy_channels), py_colorspace) 1526 1527 1528def _linearize(a): 1529 # this uses a vectorized binary search to compute 1530 # strictly sequential indices for all values in a. 1531 1532 orig_shape = a.shape 1533 a = a.reshape((functools.reduce(lambda x, y: x * y, a.shape),)) # 1 dimension 1534 1535 u = numpy.unique(a) 1536 n = len(u) 1537 1538 lower = numpy.ndarray(a.shape, dtype=numpy.int) 1539 lower.fill(0) 1540 upper = numpy.ndarray(a.shape, dtype=numpy.int) 1541 upper.fill(n - 1) 1542 1543 h = numpy.sort(u) 1544 q = n # worst case partition size 1545 1546 while q > 2: 1547 m = numpy.right_shift(lower + upper, 1) 1548 f = a <= h[m] 1549 # (lower, m) vs (m + 1, upper) 1550 lower = numpy.where(f, lower, m + 1) 1551 upper = numpy.where(f, m, upper) 1552 q = (q + 1) // 2 1553 1554 return numpy.where(a == h[lower], lower, upper).reshape(orig_shape), n 1555 1556 1557class Colorize(_ImageBuiltin): 1558 """ 1559 <dl> 1560 <dt>'Colorize[$values$]' 1561 <dd>returns an image where each number in the rectangular matrix $values$ is a pixel and each 1562 occurence of the same number is displayed in the same unique color, which is different from the 1563 colors of all non-identical numbers. 1564 <dt>'Colorize[$image$]' 1565 <dd>gives a colorized version of $image$. 1566 </dl> 1567 1568 >> Colorize[{{1.3, 2.1, 1.5}, {1.3, 1.3, 2.1}, {1.3, 2.1, 1.5}}] 1569 = -Image- 1570 1571 >> Colorize[{{1, 2}, {2, 2}, {2, 3}}, ColorFunction -> (Blend[{White, Blue}, #]&)] 1572 = -Image- 1573 """ 1574 1575 options = {"ColorFunction": "Automatic"} 1576 1577 messages = { 1578 "cfun": "`1` is neither a gradient ColorData nor a pure function suitable as ColorFunction." 1579 } 1580 1581 def apply(self, values, evaluation, options): 1582 "Colorize[values_, OptionsPattern[%(name)s]]" 1583 1584 if isinstance(values, Image): 1585 pixels = values.grayscale().pixels 1586 matrix = pixels_as_ubyte(pixels.reshape(pixels.shape[:2])) 1587 else: 1588 if not Expression("MatrixQ", values).evaluate(evaluation).is_true(): 1589 return 1590 matrix = matrix_to_numpy(values) 1591 1592 a, n = _linearize(matrix) 1593 # the maximum value for n is the number of pixels in a, which is acceptable and never too large. 1594 1595 color_function = self.get_option(options, "ColorFunction", evaluation) 1596 if ( 1597 isinstance(color_function, Symbol) 1598 and color_function.get_name() == "System`Automatic" 1599 ): 1600 color_function = String("LakeColors") 1601 1602 from mathics.builtin.drawing.plot import gradient_palette 1603 1604 cmap = gradient_palette(color_function, n, evaluation) 1605 if not cmap: 1606 evaluation.message("Colorize", "cfun", color_function) 1607 return 1608 1609 s = (a.shape[0], a.shape[1], 1) 1610 p = numpy.transpose(numpy.array([cmap[i] for i in range(n)])[:, 0:3]) 1611 return Image( 1612 numpy.concatenate([p[i][a].reshape(s) for i in range(3)], axis=2), 1613 color_space="RGB", 1614 ) 1615 1616 1617class DominantColors(_ImageBuiltin): 1618 """ 1619 <dl> 1620 <dt>'DominantColors[$image$]' 1621 <dd>gives a list of colors which are dominant in the given image. 1622 <dt>'DominantColors[$image$, $n$]' 1623 <dd>returns at most $n$ colors. 1624 <dt>'DominantColors[$image$, $n$, $prop$]' 1625 <dd>returns the given property $prop$, which may be "Color" (return RGB colors), "LABColor" (return 1626 LAB colors), "Count" (return the number of pixels a dominant color covers), "Coverage" (return the 1627 fraction of the image a dominant color covers), or "CoverageImage" (return a black and white image 1628 indicating with white the parts that are covered by a dominant color). 1629 </dl> 1630 1631 The option "ColorCoverage" specifies the minimum amount of coverage needed to include a dominant color 1632 in the result. 1633 1634 The option "MinColorDistance" specifies the distance (in LAB color space) up to which colors are merged 1635 and thus regarded as belonging to the same dominant color. 1636 1637 >> img = Import["ExampleData/lena.tif"] 1638 = -Image- 1639 1640 >> DominantColors[img] 1641 = {RGBColor[0.827451, 0.537255, 0.486275], RGBColor[0.87451, 0.439216, 0.45098], RGBColor[0.341176, 0.0705882, 0.254902], RGBColor[0.690196, 0.266667, 0.309804], RGBColor[0.533333, 0.192157, 0.298039], RGBColor[0.878431, 0.760784, 0.721569]} 1642 1643 >> DominantColors[img, 3] 1644 = {RGBColor[0.827451, 0.537255, 0.486275], RGBColor[0.87451, 0.439216, 0.45098], RGBColor[0.341176, 0.0705882, 0.254902]} 1645 1646 >> DominantColors[img, 3, "Coverage"] 1647 = {28579 / 131072, 751 / 4096, 23841 / 131072} 1648 1649 >> DominantColors[img, 3, "CoverageImage"] 1650 = {-Image-, -Image-, -Image-} 1651 1652 >> DominantColors[img, 3, "Count"] 1653 = {57158, 48064, 47682} 1654 1655 >> DominantColors[img, 2, "LABColor"] 1656 = {LABColor[0.646831, 0.279785, 0.193184], LABColor[0.608465, 0.443559, 0.195911]} 1657 1658 >> DominantColors[img, MinColorDistance -> 0.5] 1659 = {RGBColor[0.87451, 0.439216, 0.45098], RGBColor[0.341176, 0.0705882, 0.254902]} 1660 1661 >> DominantColors[img, ColorCoverage -> 0.15] 1662 = {RGBColor[0.827451, 0.537255, 0.486275], RGBColor[0.87451, 0.439216, 0.45098], RGBColor[0.341176, 0.0705882, 0.254902]} 1663 """ 1664 1665 rules = { 1666 "DominantColors[image_Image, n_Integer, options___]": 'DominantColors[image, n, "Color", options]', 1667 "DominantColors[image_Image, options___]": 'DominantColors[image, 256, "Color", options]', 1668 } 1669 1670 options = {"ColorCoverage": "Automatic", "MinColorDistance": "Automatic"} 1671 1672 def apply(self, image, n, prop, evaluation, options): 1673 "DominantColors[image_Image, n_Integer, prop_String, OptionsPattern[%(name)s]]" 1674 1675 py_prop = prop.get_string_value() 1676 if py_prop not in ("Color", "LABColor", "Count", "Coverage", "CoverageImage"): 1677 return 1678 1679 color_coverage = self.get_option(options, "ColorCoverage", evaluation) 1680 min_color_distance = self.get_option(options, "MinColorDistance", evaluation) 1681 1682 if ( 1683 isinstance(min_color_distance, Symbol) 1684 and min_color_distance.get_name() == "System`Automatic" 1685 ): 1686 py_min_color_distance = 0.15 1687 else: 1688 py_min_color_distance = min_color_distance.round_to_float() 1689 if py_min_color_distance is None: 1690 return 1691 1692 if ( 1693 isinstance(color_coverage, Symbol) 1694 and color_coverage.get_name() == "System`Automatic" 1695 ): 1696 py_min_color_coverage = 0.05 1697 py_max_color_coverage = 1.0 1698 elif color_coverage.has_form("List", 2): 1699 py_min_color_coverage = color_coverage.leaves[0].round_to_float() 1700 py_max_color_coverage = color_coverage.leaves[1].round_to_float() 1701 else: 1702 py_min_color_coverage = color_coverage.round_to_float() 1703 py_max_color_coverage = 1.0 1704 1705 if py_min_color_coverage is None or py_max_color_coverage is None: 1706 return 1707 1708 at_most = n.get_int_value() 1709 1710 if at_most > 256: 1711 return 1712 1713 # reduce complexity by reducing to 256 colors. this is not uncommon; see Kiranyaz et al., 1714 # "Perceptual Dominant Color Extraction by Multidimensional Particle Swarm Optimization": 1715 # "to reduce the computational complexity [...] a preprocessing step, which creates a 1716 # limited color palette in RGB color domain, is first performed." 1717 1718 im = ( 1719 image.color_convert("RGB") 1720 .pil() 1721 .convert("P", palette=PIL.Image.ADAPTIVE, colors=256) 1722 ) 1723 pixels = numpy.array(list(im.getdata())) 1724 1725 flat = numpy.array(list(im.getpalette())) / 255.0 # float values now 1726 rgb_palette = [flat[i : i + 3] for i in range(0, len(flat), 3)] # group by 3 1727 lab_palette = [ 1728 numpy.array(x) for x in convert_color(rgb_palette, "RGB", "LAB", False) 1729 ] 1730 1731 bins = numpy.bincount(pixels, minlength=len(rgb_palette)) 1732 num_pixels = im.size[0] * im.size[1] 1733 1734 from mathics.algorithm.clusters import ( 1735 agglomerate, 1736 PrecomputedDistances, 1737 FixedDistanceCriterion, 1738 ) 1739 1740 norm = numpy.linalg.norm 1741 1742 def df(i, j): 1743 return norm(lab_palette[i] - lab_palette[j]) 1744 1745 lab_distances = [df(i, j) for i in range(len(lab_palette)) for j in range(i)] 1746 1747 if py_prop == "LABColor": 1748 out_palette = lab_palette 1749 out_palette_head = "LABColor" 1750 else: 1751 out_palette = rgb_palette 1752 out_palette_head = "RGBColor" 1753 1754 dominant = agglomerate( 1755 (out_palette, bins), 1756 (FixedDistanceCriterion, {"merge_limit": py_min_color_distance}), 1757 PrecomputedDistances(lab_distances), 1758 mode="dominant", 1759 ) 1760 1761 def result(): 1762 min_count = max(0, int(num_pixels * py_min_color_coverage)) 1763 max_count = min(num_pixels, int(num_pixels * py_max_color_coverage)) 1764 1765 for prototype, count, members in dominant: 1766 if max_count >= count > min_count: 1767 if py_prop == "Count": 1768 yield Integer(count) 1769 elif py_prop == "Coverage": 1770 yield Rational(int(count), num_pixels) 1771 elif py_prop == "CoverageImage": 1772 mask = numpy.ndarray(shape=pixels.shape, dtype=numpy.bool) 1773 mask.fill(0) 1774 for i in members: 1775 mask = mask | (pixels == i) 1776 yield Image(mask.reshape(tuple(reversed(im.size))), "Grayscale") 1777 else: 1778 yield Expression(out_palette_head, *prototype) 1779 1780 return Expression(SymbolList, *itertools.islice(result(), 0, at_most)) 1781 1782 1783# pixel access 1784 1785 1786class ImageData(_ImageBuiltin): 1787 """ 1788 <dl> 1789 <dt>'ImageData[$image$]' 1790 <dd>gives a list of all color values of $image$ as a matrix. 1791 <dt>'ImageData[$image$, $stype$]' 1792 <dd>gives a list of color values in type $stype$. 1793 </dl> 1794 1795 >> img = Image[{{0.2, 0.4}, {0.9, 0.6}, {0.5, 0.8}}]; 1796 >> ImageData[img] 1797 = {{0.2, 0.4}, {0.9, 0.6}, {0.5, 0.8}} 1798 1799 >> ImageData[img, "Byte"] 1800 = {{51, 102}, {229, 153}, {127, 204}} 1801 1802 >> ImageData[Image[{{0, 1}, {1, 0}, {1, 1}}], "Bit"] 1803 = {{0, 1}, {1, 0}, {1, 1}} 1804 1805 #> ImageData[img, "Bytf"] 1806 : Unsupported pixel format "Bytf". 1807 = ImageData[-Image-, Bytf] 1808 """ 1809 1810 rules = {"ImageData[image_Image]": 'ImageData[image, "Real"]'} 1811 1812 messages = {"pixelfmt": 'Unsupported pixel format "``".'} 1813 1814 def apply(self, image, stype, evaluation): 1815 "ImageData[image_Image, stype_String]" 1816 pixels = image.pixels 1817 stype = stype.get_string_value() 1818 if stype == "Real": 1819 pixels = pixels_as_float(pixels) 1820 elif stype == "Byte": 1821 pixels = pixels_as_ubyte(pixels) 1822 elif stype == "Bit16": 1823 pixels = pixels_as_uint(pixels) 1824 elif stype == "Bit": 1825 pixels = pixels.astype(numpy.int) 1826 else: 1827 return evaluation.message("ImageData", "pixelfmt", stype) 1828 return from_python(numpy_to_matrix(pixels)) 1829 1830 1831class ImageTake(_ImageBuiltin): 1832 """ 1833 <dl> 1834 <dt>'ImageTake[$image$, $n$]' 1835 <dd>gives the first $n$ rows of $image$. 1836 <dt>'ImageTake[$image$, -$n$]' 1837 <dd>gives the last $n$ rows of $image$. 1838 <dt>'ImageTake[$image$, {$r1$, $r2$}]' 1839 <dd>gives rows $r1$, ..., $r2$ of $image$. 1840 <dt>'ImageTake[$image$, {$r1$, $r2$}, {$c1$, $c2$}]' 1841 <dd>gives a cropped version of $image$. 1842 </dl> 1843 """ 1844 1845 def apply(self, image, n, evaluation): 1846 "ImageTake[image_Image, n_Integer]" 1847 py_n = n.get_int_value() 1848 if py_n >= 0: 1849 pixels = image.pixels[:py_n] 1850 elif py_n < 0: 1851 pixels = image.pixels[py_n:] 1852 return Image(pixels, image.color_space) 1853 1854 def _slice(self, image, i1, i2, axis): 1855 n = image.pixels.shape[axis] 1856 py_i1 = min(max(i1.get_int_value() - 1, 0), n - 1) 1857 py_i2 = min(max(i2.get_int_value() - 1, 0), n - 1) 1858 1859 def flip(pixels): 1860 if py_i1 > py_i2: 1861 return numpy_flip(pixels, axis) 1862 else: 1863 return pixels 1864 1865 return slice(min(py_i1, py_i2), 1 + max(py_i1, py_i2)), flip 1866 1867 def apply_rows(self, image, r1, r2, evaluation): 1868 "ImageTake[image_Image, {r1_Integer, r2_Integer}]" 1869 s, f = self._slice(image, r1, r2, 0) 1870 return Image(f(image.pixels[s]), image.color_space) 1871 1872 def apply_rows_cols(self, image, r1, r2, c1, c2, evaluation): 1873 "ImageTake[image_Image, {r1_Integer, r2_Integer}, {c1_Integer, c2_Integer}]" 1874 sr, fr = self._slice(image, r1, r2, 0) 1875 sc, fc = self._slice(image, c1, c2, 1) 1876 return Image(fc(fr(image.pixels[sr, sc])), image.color_space) 1877 1878 1879class PixelValue(_ImageBuiltin): 1880 """ 1881 <dl> 1882 <dt>'PixelValue[$image$, {$x$, $y$}]' 1883 <dd>gives the value of the pixel at position {$x$, $y$} in $image$. 1884 </dl> 1885 1886 >> lena = Import["ExampleData/lena.tif"]; 1887 >> PixelValue[lena, {1, 1}] 1888 = {0.321569, 0.0862745, 0.223529} 1889 #> {82 / 255, 22 / 255, 57 / 255} // N (* pixel byte values from bottom left corner *) 1890 = {0.321569, 0.0862745, 0.223529} 1891 1892 #> PixelValue[lena, {0, 1}]; 1893 : Padding not implemented for PixelValue. 1894 #> PixelValue[lena, {512, 1}] 1895 = {0.72549, 0.290196, 0.317647} 1896 #> PixelValue[lena, {513, 1}]; 1897 : Padding not implemented for PixelValue. 1898 #> PixelValue[lena, {1, 0}]; 1899 : Padding not implemented for PixelValue. 1900 #> PixelValue[lena, {1, 512}] 1901 = {0.886275, 0.537255, 0.490196} 1902 #> PixelValue[lena, {1, 513}]; 1903 : Padding not implemented for PixelValue. 1904 """ 1905 1906 messages = {"nopad": "Padding not implemented for PixelValue."} 1907 1908 def apply(self, image, x, y, evaluation): 1909 "PixelValue[image_Image, {x_?RealNumberQ, y_?RealNumberQ}]" 1910 x = int(x.round_to_float()) 1911 y = int(y.round_to_float()) 1912 height = image.pixels.shape[0] 1913 width = image.pixels.shape[1] 1914 if not (1 <= x <= width and 1 <= y <= height): 1915 return evaluation.message("PixelValue", "nopad") 1916 pixel = pixels_as_float(image.pixels)[height - y, x - 1] 1917 if isinstance(pixel, (numpy.ndarray, numpy.generic, list)): 1918 return Expression(SymbolList, *[MachineReal(float(x)) for x in list(pixel)]) 1919 else: 1920 return MachineReal(float(pixel)) 1921 1922 1923class PixelValuePositions(_ImageBuiltin): 1924 """ 1925 <dl> 1926 <dt>'PixelValuePositions[$image$, $val$]' 1927 <dd>gives the positions of all pixels in $image$ that have value $val$. 1928 </dl> 1929 1930 >> PixelValuePositions[Image[{{0, 1}, {1, 0}, {1, 1}}], 1] 1931 = {{1, 1}, {1, 2}, {2, 1}, {2, 3}} 1932 1933 >> PixelValuePositions[Image[{{0.2, 0.4}, {0.9, 0.6}, {0.3, 0.8}}], 0.5, 0.15] 1934 = {{2, 2}, {2, 3}} 1935 1936 >> img = Import["ExampleData/lena.tif"]; 1937 >> PixelValuePositions[img, 3 / 255, 0.5 / 255] 1938 = {{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}} 1939 >> PixelValue[img, {180, 192}] 1940 = {0.25098, 0.0117647, 0.215686} 1941 """ 1942 1943 rules = { 1944 "PixelValuePositions[image_Image, val_?RealNumberQ]": "PixelValuePositions[image, val, 0]" 1945 } 1946 1947 def apply(self, image, val, d, evaluation): 1948 "PixelValuePositions[image_Image, val_?RealNumberQ, d_?RealNumberQ]" 1949 val = val.round_to_float() 1950 d = d.round_to_float() 1951 1952 positions = numpy.argwhere( 1953 numpy.isclose(pixels_as_float(image.pixels), val, atol=d, rtol=0) 1954 ) 1955 1956 # python indexes from 0 at top left -> indices from 1 starting at bottom left 1957 # if single channel then ommit channel indices 1958 height = image.pixels.shape[0] 1959 if image.pixels.shape[2] == 1: 1960 result = sorted((j + 1, height - i) for i, j, k in positions.tolist()) 1961 else: 1962 result = sorted( 1963 (j + 1, height - i, k + 1) for i, j, k in positions.tolist() 1964 ) 1965 return Expression(SymbolList, *(Expression(SymbolList, *arg) for arg in result)) 1966 1967 1968# image attribute queries 1969 1970 1971class ImageDimensions(_ImageBuiltin): 1972 """ 1973 <dl> 1974 <dt>'ImageDimensions[$image$]' 1975 <dd>Returns the dimensions of $image$ in pixels. 1976 </dl> 1977 1978 >> lena = Import["ExampleData/lena.tif"]; 1979 >> ImageDimensions[lena] 1980 = {512, 512} 1981 1982 >> ImageDimensions[RandomImage[1, {50, 70}]] 1983 = {50, 70} 1984 1985 #> Image[{{0, 1}, {1, 0}, {1, 1}}] // ImageDimensions 1986 = {2, 3} 1987 #> Image[{{0.2, 0.4}, {0.9, 0.6}, {0.3, 0.8}}] // ImageDimensions 1988 = {2, 3} 1989 """ 1990 1991 def apply(self, image, evaluation): 1992 "ImageDimensions[image_Image]" 1993 return Expression(SymbolList, *image.dimensions()) 1994 1995 1996class ImageAspectRatio(_ImageBuiltin): 1997 """ 1998 <dl> 1999 <dt>'ImageAspectRatio[$image$]' 2000 <dd>gives the aspect ratio of $image$. 2001 </dl> 2002 2003 >> img = Import["ExampleData/lena.tif"]; 2004 >> ImageAspectRatio[img] 2005 = 1 2006 2007 >> ImageAspectRatio[Image[{{0, 1}, {1, 0}, {1, 1}}]] 2008 = 3 / 2 2009 """ 2010 2011 def apply(self, image, evaluation): 2012 "ImageAspectRatio[image_Image]" 2013 dim = image.dimensions() 2014 return Expression("Divide", Integer(dim[1]), Integer(dim[0])) 2015 2016 2017class ImageChannels(_ImageBuiltin): 2018 """ 2019 <dl> 2020 <dt>'ImageChannels[$image$]' 2021 <dd>gives the number of channels in $image$. 2022 </dl> 2023 2024 >> ImageChannels[Image[{{0, 1}, {1, 0}}]] 2025 = 1 2026 2027 >> img = Import["ExampleData/lena.tif"]; 2028 >> ImageChannels[img] 2029 = 3 2030 """ 2031 2032 def apply(self, image, evaluation): 2033 "ImageChannels[image_Image]" 2034 return Integer(image.channels()) 2035 2036 2037class ImageType(_ImageBuiltin): 2038 """ 2039 <dl> 2040 <dt>'ImageType[$image$]' 2041 <dd>gives the interval storage type of $image$, e.g. "Real", "Bit32", or "Bit". 2042 </dl> 2043 2044 >> img = Import["ExampleData/lena.tif"]; 2045 >> ImageType[img] 2046 = Byte 2047 2048 >> ImageType[Image[{{0, 1}, {1, 0}}]] 2049 = Real 2050 2051 >> ImageType[Binarize[img]] 2052 = Bit 2053 2054 """ 2055 2056 def apply(self, image, evaluation): 2057 "ImageType[image_Image]" 2058 return String(image.storage_type()) 2059 2060 2061class BinaryImageQ(_ImageTest): 2062 """ 2063 <dl> 2064 <dt>'BinaryImageQ[$image]' 2065 <dd>returns True if the pixels of $image are binary bit values, and False otherwise. 2066 </dl> 2067 2068 >> img = Import["ExampleData/lena.tif"]; 2069 S> BinaryImageQ[img] 2070 = False 2071 2072 S> BinaryImageQ[Binarize[img]] 2073 = True 2074 """ 2075 2076 def test(self, expr): 2077 return isinstance(expr, Image) and expr.storage_type() == "Bit" 2078 2079 2080# Image core classes 2081 2082 2083def _image_pixels(matrix): 2084 try: 2085 pixels = numpy.array(matrix, dtype="float64") 2086 except ValueError: # irregular array, e.g. {{0, 1}, {0, 1, 1}} 2087 return None 2088 shape = pixels.shape 2089 if len(shape) == 2 or (len(shape) == 3 and shape[2] in (1, 3, 4)): 2090 return pixels 2091 else: 2092 return None 2093 2094 2095class ImageQ(_ImageTest): 2096 """ 2097 <dl> 2098 <dt>'ImageQ[Image[$pixels]]' 2099 <dd>returns True if $pixels has dimensions from which an Image can be constructed, and False otherwise. 2100 </dl> 2101 2102 >> ImageQ[Image[{{0, 1}, {1, 0}}]] 2103 = True 2104 2105 >> ImageQ[Image[{{{0, 0, 0}, {0, 1, 0}}, {{0, 1, 0}, {0, 1, 1}}}]] 2106 = True 2107 2108 >> ImageQ[Image[{{{0, 0, 0}, {0, 1}}, {{0, 1, 0}, {0, 1, 1}}}]] 2109 = False 2110 2111 >> ImageQ[Image[{1, 0, 1}]] 2112 = False 2113 2114 >> ImageQ["abc"] 2115 = False 2116 """ 2117 2118 def test(self, expr): 2119 return isinstance(expr, Image) 2120 2121 2122class ImageBox(BoxConstruct): 2123 def boxes_to_text(self, leaves=None, **options): 2124 return "-Image-" 2125 2126 def boxes_to_mathml(self, leaves=None, **options): 2127 if leaves is None: 2128 leaves = self._leaves 2129 # see https://tools.ietf.org/html/rfc2397 2130 return '<mglyph src="%s" width="%dpx" height="%dpx" />' % ( 2131 leaves[0].get_string_value(), 2132 leaves[1].get_int_value(), 2133 leaves[2].get_int_value(), 2134 ) 2135 2136 def boxes_to_tex(self, leaves=None, **options): 2137 return "-Image-" 2138 2139 2140class Image(Atom): 2141 def __init__(self, pixels, color_space, metadata={}, **kwargs): 2142 super(Image, self).__init__(**kwargs) 2143 if len(pixels.shape) == 2: 2144 pixels = pixels.reshape(list(pixels.shape) + [1]) 2145 self.pixels = pixels 2146 self.color_space = color_space 2147 self.metadata = metadata 2148 2149 def filter(self, f): # apply PIL filters component-wise 2150 pixels = self.pixels 2151 n = pixels.shape[2] 2152 channels = [ 2153 f(PIL.Image.fromarray(c, "L")) for c in (pixels[:, :, i] for i in range(n)) 2154 ] 2155 return Image(numpy.dstack(channels), self.color_space) 2156 2157 def pil(self): 2158 # see http://pillow.readthedocs.io/en/3.1.x/handbook/concepts.html#concept-modes 2159 n = self.channels() 2160 2161 if n == 1: 2162 dtype = self.pixels.dtype 2163 2164 if dtype in (numpy.float32, numpy.float64): 2165 pixels = self.pixels.astype(numpy.float32) 2166 mode = "F" 2167 elif dtype == numpy.uint32: 2168 pixels = self.pixels 2169 mode = "I" 2170 else: 2171 pixels = pixels_as_ubyte(self.pixels) 2172 mode = "L" 2173 2174 pixels = pixels.reshape(pixels.shape[:2]) 2175 elif n == 3: 2176 if self.color_space == "LAB": 2177 mode = "LAB" 2178 pixels = self.pixels 2179 elif self.color_space == "HSB": 2180 mode = "HSV" 2181 pixels = self.pixels 2182 elif self.color_space == "RGB": 2183 mode = "RGB" 2184 pixels = self.pixels 2185 else: 2186 mode = "RGB" 2187 pixels = self.color_convert("RGB").pixels 2188 2189 pixels = pixels_as_ubyte(pixels) 2190 elif n == 4: 2191 if self.color_space == "CMYK": 2192 mode = "CMYK" 2193 pixels = self.pixels 2194 elif self.color_space == "RGB": 2195 mode = "RGBA" 2196 pixels = self.pixels 2197 else: 2198 mode = "RGBA" 2199 pixels = self.color_convert("RGB").pixels 2200 2201 pixels = pixels_as_ubyte(pixels) 2202 else: 2203 raise NotImplementedError 2204 2205 return PIL.Image.fromarray(pixels, mode) 2206 2207 def color_convert(self, to_color_space, preserve_alpha=True): 2208 if to_color_space == self.color_space and preserve_alpha: 2209 return self 2210 else: 2211 pixels = pixels_as_float(self.pixels) 2212 converted = convert_color( 2213 pixels, self.color_space, to_color_space, preserve_alpha 2214 ) 2215 if converted is None: 2216 return None 2217 return Image(converted, to_color_space) 2218 2219 def grayscale(self): 2220 return self.color_convert("Grayscale") 2221 2222 def atom_to_boxes(self, f, evaluation): 2223 pixels = pixels_as_ubyte(self.color_convert("RGB", True).pixels) 2224 shape = pixels.shape 2225 2226 width = shape[1] 2227 height = shape[0] 2228 scaled_width = width 2229 scaled_height = height 2230 2231 if len(shape) >= 3 and shape[2] == 4: 2232 pixels_format = "RGBA" 2233 else: 2234 pixels_format = "RGB" 2235 2236 pillow = PIL.Image.fromarray(pixels, pixels_format) 2237 2238 # if the image is very small, scale it up using nearest neighbour. 2239 min_size = 128 2240 if width < min_size and height < min_size: 2241 scale = min_size / max(width, height) 2242 scaled_width = int(scale * width) 2243 scaled_height = int(scale * height) 2244 pillow = pillow.resize( 2245 (scaled_height, scaled_width), resample=PIL.Image.NEAREST 2246 ) 2247 2248 with warnings.catch_warnings(): 2249 warnings.simplefilter("ignore") 2250 2251 stream = BytesIO() 2252 pillow.save(stream, format="png") 2253 stream.seek(0) 2254 contents = stream.read() 2255 stream.close() 2256 2257 encoded = base64.b64encode(contents) 2258 encoded = b"data:image/png;base64," + encoded 2259 2260 return ImageBox(String(encoded), Integer(scaled_width), Integer(scaled_height)) 2261 2262 def __str__(self): 2263 return "-Image-" 2264 2265 def do_copy(self): 2266 return Image(self.pixels, self.color_space, self.metadata) 2267 2268 def default_format(self, evaluation, form): 2269 return "-Image-" 2270 2271 def get_sort_key(self, pattern_sort=False): 2272 if pattern_sort: 2273 return super(Image, self).get_sort_key(True) 2274 else: 2275 return hash(self) 2276 2277 def sameQ(self, other) -> bool: 2278 """Mathics SameQ""" 2279 if not isinstance(other, Image): 2280 return False 2281 if self.color_space != other.color_space or self.metadata != other.metadata: 2282 return False 2283 return numpy.array_equal(self.pixels, other.pixels) 2284 2285 def to_python(self, *args, **kwargs): 2286 return self.pixels 2287 2288 def __hash__(self): 2289 return hash( 2290 ( 2291 "Image", 2292 self.pixels.tobytes(), 2293 self.color_space, 2294 frozenset(self.metadata.items()), 2295 ) 2296 ) 2297 2298 def dimensions(self): 2299 shape = self.pixels.shape 2300 return shape[1], shape[0] 2301 2302 def channels(self): 2303 return self.pixels.shape[2] 2304 2305 def storage_type(self): 2306 dtype = self.pixels.dtype 2307 if dtype in (numpy.float32, numpy.float64): 2308 return "Real" 2309 elif dtype == numpy.uint32: 2310 return "Bit32" 2311 elif dtype == numpy.uint16: 2312 return "Bit16" 2313 elif dtype == numpy.uint8: 2314 return "Byte" 2315 elif dtype == numpy.bool: 2316 return "Bit" 2317 else: 2318 return str(dtype) 2319 2320 def options(self): 2321 return Expression( 2322 "List", 2323 Expression(SymbolRule, String("ColorSpace"), String(self.color_space)), 2324 Expression(SymbolRule, String("MetaInformation"), self.metadata), 2325 ) 2326 2327 2328class ImageAtom(AtomBuiltin): 2329 """ 2330 #> Image[{{{1,1,0},{0,1,1}}, {{1,0,1},{1,1,0}}}] 2331 = -Image- 2332 2333 #> Image[{{{0,0,0,0.25},{0,0,0,0.5}}, {{0,0,0,0.5},{0,0,0,0.75}}}] 2334 = -Image- 2335 """ 2336 2337 requires = _image_requires 2338 2339 def apply_create(self, array, evaluation): 2340 "Image[array_]" 2341 pixels = _image_pixels(array.to_python()) 2342 if pixels is not None: 2343 shape = pixels.shape 2344 is_rgb = len(shape) == 3 and shape[2] in (3, 4) 2345 return Image(pixels.clip(0, 1), "RGB" if is_rgb else "Grayscale") 2346 else: 2347 return Expression("Image", array) 2348 2349 2350# complex operations 2351 2352 2353class TextRecognize(Builtin): 2354 """ 2355 <dl> 2356 <dt>'TextRecognize[{$image$}]' 2357 <dd>Recognizes text in $image$ and returns it as string. 2358 </dl> 2359 """ 2360 2361 requires = _image_requires + ("pyocr",) 2362 2363 messages = { 2364 "tool": "No text recognition tools were found in the paths available to the Mathics kernel.", 2365 "langinv": "No language data for `1` is available.", 2366 "lang": "Language `1` is not supported in your installation of `2`. Please install it.", 2367 } 2368 2369 options = {"Language": '"English"'} 2370 2371 def apply(self, image, evaluation, options): 2372 "TextRecognize[image_Image, OptionsPattern[%(name)s]]" 2373 import pyocr 2374 from mathics.builtin.codetables import iso639_3 2375 2376 language = self.get_option(options, "Language", evaluation) 2377 if not isinstance(language, String): 2378 return 2379 py_language = language.get_string_value() 2380 py_language_code = iso639_3.get(py_language) 2381 2382 if py_language_code is None: 2383 evaluation.message("TextRecognize", "langcode", py_language) 2384 return 2385 2386 tools = pyocr.get_available_tools() 2387 if not tools: 2388 evaluation.message("TextRecognize", "tool") 2389 return 2390 best_tool = tools[0] 2391 2392 langs = best_tool.get_available_languages() 2393 if py_language_code not in langs: 2394 # if we use Tesseract, then this means copying the necessary language files from 2395 # https://github.com/tesseract-ocr/tessdatainstalling to tessdata, which is 2396 # usually located at /usr/share/tessdata or similar, but there's no API to query 2397 # the exact location, so we cannot, for now, give a better message. 2398 2399 evaluation.message( 2400 "TextRecognize", "lang", py_language, best_tool.get_name() 2401 ) 2402 return 2403 2404 import pyocr.builders 2405 2406 text = best_tool.image_to_string( 2407 image.pil(), lang=py_language_code, builder=pyocr.builders.TextBuilder() 2408 ) 2409 2410 if isinstance(text, (list, tuple)): 2411 text = "\n".join(text) 2412 2413 return String(text) 2414 2415 2416class WordCloud(Builtin): 2417 """ 2418 <dl> 2419 <dt>'WordCloud[{$word1$, $word2$, ...}]' 2420 <dd>Gives a word cloud with the given list of words. 2421 <dt>'WordCloud[{$weight1$ -> $word1$, $weight2$ -> $word2$, ...}]' 2422 <dd>Gives a word cloud with the words weighted using the given weights. 2423 <dt>'WordCloud[{$weight1$, $weight2$, ...} -> {$word1$, $word2$, ...}]' 2424 <dd>Also gives a word cloud with the words weighted using the given weights. 2425 <dt>'WordCloud[{{$word1$, $weight1$}, {$word2$, $weight2$}, ...}]' 2426 <dd>Gives a word cloud with the words weighted using the given weights. 2427 </dl> 2428 2429 >> WordCloud[StringSplit[Import["ExampleData/EinsteinSzilLetter.txt"]]] 2430 = -Image- 2431 2432 >> WordCloud[Range[50] -> ToString /@ Range[50]] 2433 = -Image- 2434 """ 2435 2436 requires = _image_requires + ("wordcloud",) 2437 2438 options = {"IgnoreCase": "True", "ImageSize": "Automatic", "MaxItems": "Automatic"} 2439 2440 # this is the palettable.colorbrewer.qualitative.Dark2_8 palette 2441 default_colors = ( 2442 (27, 158, 119), 2443 (217, 95, 2), 2444 (117, 112, 179), 2445 (231, 41, 138), 2446 (102, 166, 30), 2447 (230, 171, 2), 2448 (166, 118, 29), 2449 (102, 102, 102), 2450 ) 2451 2452 def apply_words_weights(self, weights, words, evaluation, options): 2453 "WordCloud[weights_List -> words_List, OptionsPattern[%(name)s]]" 2454 if len(weights.leaves) != len(words.leaves): 2455 return 2456 2457 def weights_and_words(): 2458 for weight, word in zip(weights.leaves, words.leaves): 2459 yield weight.round_to_float(), word.get_string_value() 2460 2461 return self._word_cloud(weights_and_words(), evaluation, options) 2462 2463 def apply_words(self, words, evaluation, options): 2464 "WordCloud[words_List, OptionsPattern[%(name)s]]" 2465 2466 if not words: 2467 return 2468 elif isinstance(words.leaves[0], String): 2469 2470 def weights_and_words(): 2471 for word in words.leaves: 2472 yield 1, word.get_string_value() 2473 2474 else: 2475 2476 def weights_and_words(): 2477 for word in words.leaves: 2478 if len(word.leaves) != 2: 2479 raise ValueError 2480 2481 head_name = word.get_head_name() 2482 if head_name == "System`Rule": 2483 weight, s = word.leaves 2484 elif head_name == "System`List": 2485 s, weight = word.leaves 2486 else: 2487 raise ValueError 2488 2489 yield weight.round_to_float(), s.get_string_value() 2490 2491 try: 2492 return self._word_cloud(weights_and_words(), evaluation, options) 2493 except ValueError: 2494 return 2495 2496 def _word_cloud(self, words, evaluation, options): 2497 ignore_case = self.get_option(options, "IgnoreCase", evaluation) == Symbol( 2498 "True" 2499 ) 2500 2501 freq = defaultdict(int) 2502 for py_weight, py_word in words: 2503 if py_word is None or py_weight is None: 2504 return 2505 key = py_word.lower() if ignore_case else py_word 2506 freq[key] += py_weight 2507 2508 max_items = self.get_option(options, "MaxItems", evaluation) 2509 if isinstance(max_items, Integer): 2510 py_max_items = max_items.get_int_value() 2511 else: 2512 py_max_items = 200 2513 2514 image_size = self.get_option(options, "ImageSize", evaluation) 2515 if image_size == Symbol("Automatic"): 2516 py_image_size = (800, 600) 2517 elif ( 2518 image_size.get_head_name() == "System`List" and len(image_size.leaves) == 2 2519 ): 2520 py_image_size = [] 2521 for leaf in image_size.leaves: 2522 if not isinstance(leaf, Integer): 2523 return 2524 py_image_size.append(leaf.get_int_value()) 2525 elif isinstance(image_size, Integer): 2526 size = image_size.get_int_value() 2527 py_image_size = (size, size) 2528 else: 2529 return 2530 2531 # inspired by http://minimaxir.com/2016/05/wordclouds/ 2532 import random 2533 import os 2534 2535 def color_func( 2536 word, font_size, position, orientation, random_state=None, **kwargs 2537 ): 2538 return self.default_colors[random.randint(0, 7)] 2539 2540 font_base_path = os.path.dirname(os.path.abspath(__file__)) + "/../fonts/" 2541 2542 font_path = os.path.realpath(font_base_path + "AmaticSC-Bold.ttf") 2543 if not os.path.exists(font_path): 2544 font_path = None 2545 2546 from wordcloud import WordCloud 2547 2548 wc = WordCloud( 2549 width=py_image_size[0], 2550 height=py_image_size[1], 2551 font_path=font_path, 2552 max_font_size=300, 2553 mode="RGB", 2554 background_color="white", 2555 max_words=py_max_items, 2556 color_func=color_func, 2557 random_state=42, 2558 stopwords=set(), 2559 ) 2560 wc.generate_from_frequencies(freq) 2561 2562 image = wc.to_image() 2563 return Image(numpy.array(image), "RGB") 2564