Repository navigation
Expand file tree
/
Copy pathImageCore.py
More file actions
4378 lines (3371 loc) · 140 KB
/
Copy pathImageCore.py
File metadata and controls
4378 lines (3371 loc) · 140 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Core Image class providing pixel storage, arithmetic operators, and color-plane access.
"""
from __future__ import annotations
import os
import os.path
import urllib
import warnings
from collections.abc import Iterator, Sequence
from math import nan
from pathlib import Path
import cv2
import numpy as np
import spatialmath.base as smb
from spatialmath import Polygon2
from spatialmath.base import islistof, isscalar
# from numpy.lib.arraysetops import isin
from machinevisiontoolbox.base import (
DTYPE_ALIASES,
draw_box,
draw_circle,
draw_labelbox,
draw_line,
draw_point,
draw_text,
float_image,
int_image,
)
from machinevisiontoolbox.base.imageio import convert, idisp, iread, iwrite
from machinevisiontoolbox.ImageBlobs import ImageBlobsMixin
from machinevisiontoolbox.ImageColor import ImageColorMixin
from machinevisiontoolbox.ImageConstants import ImageConstantsMixin
from machinevisiontoolbox.ImageFiducials import ImageFiducialsMixin
from machinevisiontoolbox.ImageIO import ImageIOMixin
from machinevisiontoolbox.ImageLineFeatures import ImageLineFeaturesMixin
from machinevisiontoolbox.ImageMorph import ImageMorphMixin
from machinevisiontoolbox.ImageMultiview import ImageMultiviewMixin
from machinevisiontoolbox.ImagePointFeatures import ImagePointFeaturesMixin
from machinevisiontoolbox.ImageTensor import ImageTensorMixin
from machinevisiontoolbox.ImageProcessing import ImageProcessingMixin
from machinevisiontoolbox.ImageRegionFeatures import ImageRegionFeaturesMixin
from machinevisiontoolbox.ImageReshape import ImageReshapeMixin
from machinevisiontoolbox.ImageSpatial import ImageSpatialMixin
from machinevisiontoolbox.Kernel import Kernel
from machinevisiontoolbox.ImageWholeFeatures import ImageWholeFeaturesMixin
from machinevisiontoolbox.mvtb_types import *
# import spatialmath.base.argcheck as argcheck
"""
This class encapsulates a Numpy array containing the pixel values. The object
supports arithmetic using overloaded operators, as well a large number of methods.
"""
class Image(
ImageIOMixin,
ImageConstantsMixin,
ImageProcessingMixin,
ImageMorphMixin,
ImageSpatialMixin,
ImageColorMixin,
ImageReshapeMixin,
ImageBlobsMixin,
ImageWholeFeaturesMixin,
ImageRegionFeaturesMixin,
ImageFiducialsMixin,
ImageLineFeaturesMixin,
ImagePointFeaturesMixin,
ImageMultiviewMixin,
ImageTensorMixin,
):
def __init__(
self,
image: Image | np.ndarray | None = None,
colororder: str | dict | None = None,
copy: bool = False,
size: tuple | list | None = None,
dtype: Dtype | bool | None = None,
name: str | None = None,
id: int | None = None,
domain=None,
binary: bool = False,
**kwargs,
) -> None:
"""
Create an Image instance
:param image: image data
:type image: array_like(H,W), :class:`Image`
:param colororder: order of color channels
:type colororder: str, dict
:param copy: copy the image data, defaults to False
:type copy: bool, optional
:param size: new size for the image, defaults to None
:type size: tuple, optional
:param dtype: data type for image; ``None`` [default] auto-detects (any
floating input becomes ``float32``; integer input becomes the
smallest unsigned/signed integer type that holds all its values);
``True`` keeps ``image``'s own dtype as-is; ``False`` raises
``ValueError``; otherwise a NumPy dtype string (``"uint8"``,
``"float32"``, ...) or NumPy type. |dtype_aliases|
:type dtype: str, NumPy dtype, bool, or None, optional
:param name: name of image, defaults to None
:type name: str, optional
:param id: numeric id of image, typically a sequence number, defaults to None
:type id: int, optional
:param domain: domain of image, defaults to None
:type domain: array_like(W), array_like(H), optional
:param binary: create binary image, non-zero values are set to True, defaults to False
:type binary: bool, optional
:raises TypeError: unknown type passed to constructor
Create a new :class:`Image` instance which contains pixel values as well as
information about image size, datatype, color planes and domain. The pixel
data is stored in an immutable NumPy array, encapsulated by the object.
An image is considered to be a two-dimensional (width x height) grid of pixel
values, that lie within one or more "color" planes.
The image data can be specified by:
- a NumPy 2D or 3D array for a greyscale or color image respectively. For
the latter case, the last index represents the color plane::
Image(nd.zeros((100, 200)))
Image(nd.zeros((100, 200, 4)), colororder="WXYZ")
- a lists of lists of pixel values, each inner list must have the same
number of elements (columns)::
Image([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
- a range of class methods that serve as constructors for common image types such as
:meth:`Zeros`, :meth:`Constant`, :meth:`Random` and :meth:`String`, or
read an image from a file :meth:`Read`.
- an existing :class:`Image` instance.
**Pixel datatype**
If ``dtype`` is a string or a NumPy dtype, the pixel data type is set to that type.
If ``dtype`` is ``True`` the pixel data type is inherited from the input if it is a NumPy array.
If ``dtype`` is not given and:
* the input is a NumPy array,the image data type is determined by the dtype of the array:
- if floating point, then the Image inherits the dtype of the array
- if integer, then the Image is assigned the smallest integer dtype that can
contain the values in the array. If the minimum value is negative, a signed
integer type is used; otherwise an unsigned type is used.
* the input is a list of lists then the image data type is:
- float32 if the list contains any floating point values, otherwise
- the smallest signed or unsigned int that can represent its
value span.
An image can have boolean pixel values which are stored as ``uint8`` values.
When used in a numerical expression, its values will be cast to integer values
of 0 or 1 representing False and True respectively.
**Color planes**
Images can have multiple planes, typically three (representing the
primary colors red, green and blue) but *any number* is possible. In
the underlying Numpy array, these planes are identified by an integer
plane index (the last dimension of the 3D array).
Rather than rely on a limiting convention such as planes being in the order
RGB or BGR, the :class:`Image` contains a dictionary that maps the name of a color plane to
its index value. The color plane order can be specified as a dict or a string, eg::
Image(img, colororder="RGB")
Image(img, colororder="red:green:blue:alpha")
Image(img, colororder=dict("R"=2, "G"=1, "B"=0))
Image planes can be referenced by their index or by their name, eg.::
img.plane(0)
img.plane("alpha")
:seealso: :meth:`colororder` :meth:`colororder_str`
**Image domain**
An :class:`Image` has a width and height in units of pixels, but for
some applications it is useful to specify the pixel coordinates in other
units, perhaps metres, or latitude/longitude angles, or for a spherical image
as azimuth and colatitude angles. The domain is specified by two 1D arrays
that map the pixel coordinate to the domain variable.
**Binary images**
If ``binary`` is True, the image is converted to a binary image, where zero valued
pixels are set to False and all other values are set to True. To create an
image where pixels have integer values of 0 and 1 use the ``dtype`` option::
Image([[0, 3], [4, 0]]) # pixel values are 0, 3, 4, 0
Image([[0, 3], [4, 0]], binary=True) # pixel values are: False, True, True, False
Image([[0, 3], [4, 0]], binary=True, dtype="uint8") # pixel values are 0, 1, 1, 0
**Reshaping**
Frequently we need to create an image from 1D data, for example::
1: Y0 Y1 Y2 ... # (N,)
2: R0 G0 B0 R1 G1 B1 ... # (3N,)
Or a 2D array with one or more rows::
3: Y0 Y1 Y2 ... # (1, N)
4: R0 G0 B0 R1 G1 B1 ... # (1,3N)
5: R0 R1 R2 ... # (3,N)
G0 G1 G2 ...
B0 B1 B2 ...
Or a 2D array with one or more columns::
6: Y0 # (N,1)
Y1
Y2
.
.
7: R0 # (3N,1)
G0
B0
R1
.
.
8: R0 G0 B0 # (N,3)
R1 G1 B1
R2 G2 B2
.
.
The ``size`` option can be used to reshape the data to the specified size. The number
of planes is determined from any of: ``colororder``, the third element of
``size``, or the number of row/columns in the data (formats 5 or 8). For formats
2, 4 or 7 the number of planes must be given explicitly in ``colororder``, the
third element of ``size``.
:seealso: :meth:`view1d`
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> import numpy as np
>>> img = Image([[1, 2], [3, 4]])
>>> print(img)
>>> img.print()
>>> Image(np.array([[1, 2], [3, 4]]))
>>> Image([[1, 2], [3, 1000]])
>>> Image([[0.1, 0.2], [0.3, 0.4]])
>>> Image([[True, False], [False, True]])
.. warning:: If an image is constructed from an existing :class:`Image` instance or a Numpy array,
the encapsulated Numpy array is, by default, a *reference* to the passed image data.
Use the option ``copy=True`` if you want to copy the data.
"""
self._name = None
self._colororder = None
self.id = id
self.domain = domain
if isinstance(name, Path):
name = str(name)
if isinstance(image, np.ndarray):
pass
elif isinstance(image, self.__class__):
# Image instance
name = image.name
colororder = image.colororder
image = image._A
elif isinstance(image, list):
# list of lists
# attempt to convert it to an ndarray
try:
image = np.array(image)
except ValueError:
raise ValueError("bad list of lists, check all rows have same length")
else:
raise ValueError("bad argument passed to Image constructor")
if not isinstance(image, np.ndarray):
raise ValueError(
"bad argument passed to Image constructor: must be ndarray or list of lists"
)
# if dtype is not given, determine the appropriate type for the data
dtype_given = dtype is not None
dtype = self._infer_dtype(image, dtype)
if binary:
image = image > 0
# Change type of array to the determined dtype -- unless the caller
# also passed convert()-only options (eg. maxintval) alongside an
# explicit dtype=, in which case defer the cast to convert() below.
# convert()'s int_image()/float_image() know how to *scale* a value
# using maxintval; a bare .astype() here would instead silently
# truncate (eg. uint16 0..4095 -> uint8 keeps only the low byte,
# producing a corrupted "bottom byte" image instead of the intended
# rescaled 0..255).
defer_dtype_to_convert = dtype_given and bool(kwargs)
if dtype is not None and not defer_dtype_to_convert:
image = image.astype(dtype, copy=False)
self.name = name
color_dict = Image.colororder2dict(colororder)
image = self._reshape_to_size(image, size)
if image.ndim not in (2, 3):
raise ValueError(
"bad ndarray passed to Image constructor: must be 2D or 3D array"
)
if image.ndim == 3 and image.shape[2] == 1:
image = image[:, :, 0] # squeeze out singleton plane
if kwargs:
image = convert(image, dtype=dtype if defer_dtype_to_convert else None, **kwargs)
# assign the image to the object, copying if requested
if copy:
self._A = image.copy()
else:
self._A = image
# final check that colororder length matches number of planes
if colororder is not None:
if len(color_dict) != self.nplanes:
raise ValueError("colororder length does not match number of planes")
if colororder is None:
if self.nplanes == 3:
self.colororder = "RGB"
# warnings.warn("defaulting color to RGB")
else:
self.colororder = color_dict
self._stats = None
self.name = name
@staticmethod
def _infer_dtype(image: np.ndarray, dtype: Dtype | bool | None) -> np.dtype | None:
"""Determine the pixel dtype for the constructor when ``dtype`` isn't
given explicitly, or resolve/validate it when it is."""
if dtype is None:
# no type given, automatically choose it
if np.issubdtype(image.dtype, np.floating):
# list contained a float
dtype = np.float32
elif np.issubdtype(image.dtype, np.integer):
# list contained only ints, convert to int/uint8 of smallest
# size to contain all values
if image.min() < 0:
# value is signed
for type in ["int8", "int16", "int32"]:
if (image.max() <= np.iinfo(type).max) and (
image.min() >= np.iinfo(type).min
):
dtype = np.dtype(type)
break
else:
# value is unsigned
for type in ["uint8", "uint16", "uint32"]:
if image.max() <= np.iinfo(type).max:
dtype = np.dtype(type)
break
elif dtype is True:
dtype = image.dtype
elif dtype is False:
raise ValueError("bad dtype argument passed to Image constructor")
else:
# dtype is given, convert to a NumPy dtype
if isinstance(dtype, str):
dtype = DTYPE_ALIASES.get(dtype, dtype)
try:
dtype = np.dtype(dtype)
except TypeError:
raise ValueError("bad dtype argument passed to Image constructor")
return dtype
def _reshape_to_size(
self, image: np.ndarray, size: tuple | list | None
) -> np.ndarray:
"""Reshape 1D/2D pixel data per the constructor's ``size=`` argument."""
if isinstance(size, self.__class__):
# size is an Image instance, ignore the size/shape and use the image's shape
size = size.size
# reshape the image data to match the specified size or shape
if size is not None:
newsize = [size[1], size[0]]
if len(size) == 3:
newsize.append(size[2])
if image.ndim == 1:
# 1D array
# Y0 Y1 Y2 ...
# R0 G0 B0 R1 G1 B1 ...
image = image.reshape(*newsize)
elif image.ndim == 2:
if image.shape[1] > image.shape[0]:
# wide image, reshape to width x height x nplanes
# Y0 Y1 Y2 ...
# R0 G0 B0 R1 G1 B1 ...
# R0 R1 R2 ...
# G0 G1 G2 ...
# B0 B1 B2 ...
if len(newsize) == 3:
if image.shape[0] != newsize[2]:
raise ValueError(
"specified number of color plane (size, colororder) does not match number of rows in data"
)
else:
if image.shape[0] > 1:
newsize.append(image.shape[0])
image = image.T.reshape(*newsize)
else:
# tall image, reshape to height x width x nplanes
# Y0
# Y1
# Y2
# .
# .
# R0 G0 B0
# R1 G1 B1
# R2 G2 B2
# .
# .
if len(newsize) == 3:
if image.shape[1] != newsize[2]:
raise ValueError(
"specified number of color plane (size, colororder) does not match number of columns in data"
)
else:
if image.shape[1] > 1:
newsize.append(image.shape[1])
image = image.reshape(*newsize)
return image
@staticmethod
def _plane_stats(plane: np.ndarray) -> dict[str, float | int]:
return {
"min": float(np.nanmin(plane)),
"max": float(np.nanmax(plane)),
"mean": float(np.nanmean(plane)),
"sdev": float(np.nanstd(plane)),
"median": float(np.nanmedian(plane)),
"nnan": int(np.sum(np.isnan(plane))),
"ninf": int(np.sum(np.isinf(plane))),
}
def __iter__(self) -> Iterator["Image"]:
"""Iterate over image planes."""
return self.planes()
def __array__(self, dtype: Dtype | None = None, copy: bool | None = None) -> np.ndarray:
"""
Convert to a plain NumPy array
:param dtype: dtype of the returned array, defaults to the image's own dtype
:type dtype: numpy dtype, optional
:param copy: force a copy of the underlying data, defaults to None
:type copy: bool, optional
:return: image pixel data
:rtype: ndarray(H,W) or ndarray(H,W,3)
Implements the NumPy array protocol, used by ``np.asarray(img)`` and by
any NumPy/SciPy/Matplotlib function that isn't ufunc- or array-function-
aware (those instead go through :meth:`__array_ufunc__` /
:meth:`__array_function__`) and calls ``np.asarray()`` internally on
whatever it's given. Without this, such a call would silently coerce an
:class:`Image` into a useless 0-d object array instead of its pixel data.
Returns the same read-only view as the :attr:`array` property unless a
dtype conversion or an explicit copy is requested, either of which
produces a new, independent (writeable) array.
:seealso: :attr:`array`
"""
arr = self.array
if dtype is not None:
arr = arr.astype(dtype)
if copy:
arr = arr.copy()
return arr
def __array_ufunc__(self, ufunc, method: str, *inputs, **kwargs):
"""
Support NumPy ufunc calls on image data.
:param ufunc: NumPy universal function being invoked
:type ufunc: numpy.ufunc
:param method: ufunc dispatch method
:type method: str
:param inputs: positional arguments passed to the ufunc
:type inputs: tuple
:param kwargs: keyword arguments passed to the ufunc
:type kwargs: dict
:raises TypeError: if the ufunc is called with ``out=``
:return: ufunc result as :class:`Image`, tuple, ndarray, or scalar
:rtype: :class:`Image`, tuple, ndarray, scalar
This method handles ordinary ufunc calls such as ``np.ceil(img)`` and
``np.arctan2(img1, img2)``.
Any :class:`Image` input arguments are converted to their underlying
NumPy arrays, the ufunc is applied, then outputs are mapped as follows:
- 2D or 3D ndarrays are wrapped to :class:`Image`
- scalars and 1D ndarrays are returned as NumPy values
- tuple outputs are handled element-wise using the same rules
Only the ``"__call__"`` ufunc mode is supported. Reduction-style ufunc methods
such as ``reduce`` or ``accumulate`` are delegated back to NumPy.
.. important:: The NumPy ``out=`` argument is intentionally not supported. Images are
treated as immutable values, so in-place ufunc writes are rejected.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> import numpy as np
>>> img = Image([[1.2, 2.8], [3.1, 4.9]], dtype='float32')
>>> np.ceil(img).array
>>> a = Image([[0.0, 1.0], [2.0, 3.0]], dtype='float32')
>>> b = Image([[1.0, 1.0], [1.0, 1.0]], dtype='float32')
>>> np.arctan2(a, b).array
For two images ``a`` and ``b``:
- ``np.add(a, b)`` dispatches here via NumPy ufunc protocol
- ``a + b`` dispatches to the Toolbox ``__add__`` operator implementation
:seealso: :meth:`array`
"""
if method != "__call__":
return NotImplemented
if "out" in kwargs:
raise TypeError("NumPy ufunc out= is not supported for Image")
image = next(
(arg for arg in inputs if isinstance(arg, self.__class__)),
self,
)
def wrap_result(result):
if not isinstance(result, np.ndarray):
return result
if result.ndim < 2:
return result
colororder = None
if (
result.ndim == 3
and image.colororder is not None
and result.shape[2] == image.nplanes
):
colororder = image.colororder
return image.__class__(result, colororder=colororder)
coerced_inputs = [
arg._A if isinstance(arg, self.__class__) else arg for arg in inputs
]
result = ufunc(*coerced_inputs, **kwargs)
if isinstance(result, tuple):
return tuple(wrap_result(item) for item in result)
return wrap_result(result)
def __array_function__(self, func, types, args, kwargs):
"""
Support selected high-level NumPy functions on images.
:param func: NumPy function being invoked
:type func: callable
:param types: distinct argument types participating in dispatch
:type types: tuple
:param args: positional arguments passed to ``func``
:type args: tuple
:param kwargs: keyword arguments passed to ``func``
:type kwargs: dict
:raises TypeError: if ``out=`` is provided
:return: result of the NumPy function on the underlying arrays
:rtype: Any
This method coerces :class:`Image` arguments to NumPy arrays, calls the
requested NumPy function, then wraps image-shaped ndarray outputs back to
:class:`Image`.
Scalars and 1D arrays are returned as NumPy values, not wrapped.
The NumPy ``out=`` argument is intentionally not supported for image
immutability.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> import numpy as np
>>> img = Image([[1, 2], [3, 4]], dtype='uint8')
>>> np.max(img)
>>> np.sum(img)
>>> np.ravel(img).shape
:seealso: :meth:`__array_ufunc__`
"""
if "out" in kwargs:
raise TypeError("NumPy function out= is not supported for Image")
if not all(issubclass(t, (self.__class__, np.ndarray)) for t in types):
return NotImplemented
def coerce(value):
if isinstance(value, self.__class__):
return value._A
if isinstance(value, tuple):
return tuple(coerce(v) for v in value)
if isinstance(value, list):
return [coerce(v) for v in value]
return value
image = next(
(
arg
for arg in (*args, *kwargs.values())
if isinstance(arg, self.__class__)
),
self,
)
def wrap_result(value):
if isinstance(value, tuple):
return tuple(wrap_result(v) for v in value)
if isinstance(value, list):
return [wrap_result(v) for v in value]
if not isinstance(value, np.ndarray):
return value
if value.ndim < 2:
return value
colororder = None
if (
value.ndim == 3
and image.colororder is not None
and value.shape[2] == image.nplanes
):
colororder = image.colororder
return image.__class__(value, colororder=colororder)
coerced_args = tuple(coerce(arg) for arg in args)
coerced_kwargs = {k: coerce(v) for k, v in kwargs.items()}
result = func(*coerced_args, **coerced_kwargs)
return wrap_result(result)
def __str__(self) -> str:
"""
Summary of image parameters
:return: summary of image
:rtype: str
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img = Image.Read('street.png')
>>> print(img)
>>> img = Image.Read('flowers1.png')
>>> str(img)
The ``repr`` method provides a more compact summary of the image parameters,
whereas the ``str`` method provides a more detailed summary, including
statistics about the pixel values.
:seealso: :meth:`stats` :meth:`__repr__`
"""
# basic image parameters
s = f"Image: {self.width} x {self.height} ({self.dtype})"
# append color order if it exists, otherwise append number of planes
if self.colororder is not None:
co = self.colororder_str or ""
s += ", " + co
else:
s += f", {self.nplanes} anonymous plane{'' if self.nplanes == 1 else 's'}"
# append id, but only if it's not None
if self.id is not None:
s += f", id={self.id}"
# append name, but if it's a long name, take from rightmost / and add ellipsis
if self.name is not None:
name = self.name
# if it's a long name, take from rightmost / and add ellipsis
if len(name) > 20:
k = [i for i, c in enumerate(name) if c == "/"]
if len(k) >= 2:
name = name[k[-2] :]
else:
name = name[-20:]
name = "..." + name
s += f" [{name}]"
# append domain if it exists
if self.domain is not None:
s += f", u::{self.domain[0][0]:.3g}:{self.domain[0][-1]:.3g}, v::{self.domain[1][0]:.3g}:{self.domain[1][-1]:.3g}"
# compute simple statistics about the pixel values, and if there are any NaN or Inf values, print that too
nnan = np.sum(np.isnan(self._A))
ninf = np.sum(np.isinf(self._A))
if nnan + ninf > 0:
s += " (contains "
if nnan > 0:
s += f"{nnan}xNaN{'s' if nnan > 1 else ''}"
if ninf > 0:
s += f" {ninf}xInf{'s' if ninf > 1 else ''}"
s += ")"
# Add statistics summary on subsequent line(s).
stats = self.stats
if stats is not None:
# self._stats is None if the image is mutable, so skip stats in that case
if self.iscolor and self.colororder is not None:
colororder = self.colororder
for k in sorted(stats.keys(), key=lambda x: colororder[x]):
s += f"\n {k:s}: {self._format_stats(stats[k])}"
else:
s += f"\n {self._format_stats(stats)}"
return s
def __repr__(self) -> str:
"""
Readable representation of image parameters
:return: summary of image enclosed in angle brackets
:rtype: str
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img = Image.Read('flowers1.png')
>>> img
"""
# basic image parameters
s = f"Image(size=({self.width}, {self.height}), dtype={self.dtype}"
# append number of planes
if self.nplanes > 1:
s += f", nplanes={self.nplanes}"
# append colororder if it exists
if self.colororder is not None:
co = self.colororder_str or ""
s += f", colororder={co}"
# append id if it exists
if self.id is not None:
s += f", id={self.id}"
# append name if it exists, but if it's a long name, take from rightmost / and add ellipsis
if self.name is not None:
name = self.name
# if it's a long name, take from rightmost / and add ellipsis
if len(name) > 20:
k = [i for i, c in enumerate(name) if c == "/"]
if len(k) >= 2:
name = name[k[-2] :]
else:
name = name[-20:]
name = "..." + name
s += f", name='{name}'"
# append domain if it exists
if self.domain is not None:
s += f", u=({self.domain[0][0]:.3g},{self.domain[0][-1]:.3g}), v=({self.domain[1][0]:.3g},{self.domain[1][-1]:.3g})"
return s + ")"
def rprint(self, **kwargs) -> "Image":
"""
Print image pixels in compact format and return image
:param fmt: format string, defaults to None
:type fmt: str, optional
:param separator: value separator, defaults to single space
:type separator: str, optional
:param precision: precision for floating point pixel values, defaults to 2
:type precision: int, optional
:param header: print image summary header, defaults to True
:type header: bool, optional
:param file: file to print to, defaults to None
:type file: file, optional
:return: the printed image
:rtype: :class:`Image`
Very compact display of pixel numerical values in grid layout and return the image itself.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img = Image.Squares(1, size=10).rprint()
>>> print(img)
>>> img = Image.Squares(1, size=10, dtype='float').rprint(precision=1, header=True)
>>> print(img)
The function returns the image, which is why we see the image ``repr`` value
after the printed pixels in this python intrepreter.
The `rprint` method is particularly useful in a method chain, for example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img = Image.Random(size=3).rprint()
>>> print(img) # return result of print() is the image itself
.. note::
- For a boolean image True and False are displayed as 1 and 0
respectively.
- For a multiplane images the planes are printed sequentially, along
with the plane's name.
:seealso: :meth:`print` :meth:`Image.strhcat` :meth:`Image.showpixels`
"""
self.print(**kwargs)
return self
def print(
self,
fmt: str | None = None,
separator: str = " ",
precision: int = 2,
header: bool = False,
file=None,
) -> None:
"""
Print image pixels in compact format
:param fmt: format string, defaults to None
:type fmt: str, optional
:param separator: value separator, defaults to single space
:type separator: str, optional
:param precision: precision for floating point pixel values, defaults to 2
:type precision: int, optional
:param header: print image summary header, defaults to True
:type header: bool, optional
:param file: file to print to, defaults to None
:type file: file, optional
:return: the printed image
:rtype: :class:`Image`
Very compact display of pixel numerical values in grid layout.
Example:
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> img = Image.Squares(1, size=10)
>>> img.print()
>>> img = Image.Squares(1, size=10, dtype='float')
>>> img.print(precision=1, header=True)
.. note::
- For a boolean image True and False are displayed as 1 and 0
respectively.
- For a multiplane images the planes are printed sequentially, along
with the plane's name.
:seealso: :meth:`rprint` :meth:`Image.strhcat` :meth:`Image.showpixels`
"""
def format_plane(plane: Image, fmt: str, indent: str = " ") -> list[str]:
rows = []
for v in plane.vspan():
row = indent
for u in plane.uspan():
row += (fmt or "{}").format(plane._A[v, u])
rows.append(row)
return rows
if fmt is None:
if self.isint:
width = max(len(str(self.max())), len(str(self.min())))
fmt = f"{separator}{{:{width}d}}"
elif self.isbool:
width = 1
fmt = f"{separator}{{:{width}d}}"
elif self.isfloat:
ff = f"{{:.{precision}f}}"
width = max(len(ff.format(self.max())), len(ff.format(self.min())))
fmt = f"{separator}{{:{width}.{precision}f}}"
if header:
print(self, file=file)
assert fmt is not None
if self.iscolor:
plane_names = (self.colororder_str or "").split(":")
for i, plane in enumerate(self.planes()):
print(f" plane {plane_names[i]}:")
print("\n".join(format_plane(plane, fmt, indent=" ")), file=file)
else:
print("\n".join(format_plane(self, fmt)), file=file)
@classmethod
def strhcat(
cls,
*images: "Image",
widths: int | Sequence[int] = 1,
arraysep: str = " |",
labels: Sequence[str] | None = None,
) -> str:
"""Format several small images concatenated horizontally
:param images: one or more images to be formatted horizontally concatenated
:type images: :class:`Image` instances
:param widths: number of digits for the formatted array elements, defaults to 1.
If scalar applies to all images, if list applies to each image.
:type widths: int or list of ints, optional
:param arraysep: separator between arrays, defaults to ``" |"``
:type arraysep: str, optional
:param labels: list of labels for each array, defaults to None
:type labels: list of str, optional
:return: multiline string containing formatted arrays
:rtype: str
:raises ValueError: if the arrays have different numbers of rows
AUTO_EDIT
For image processing this is useful for displaying small test images.
The arrays are formatted and concatenated horizontally with a vertical separator.
Each array has a header row that indicates the column number. Each row has a
header column that indicates the row number.
.. runblock:: pycon
>>> from machinevisiontoolbox import Image
>>> A = Image.Random(size=(5,5), maxval=9)
>>> print(Image.strhcat(A))
>>> print(Image.strhcat(A, widths=2))
>>> B = Image.Random(size=(5,5), maxval=9)
>>> print(Image.strhcat(A, B))
>>> print(Image.strhcat(A, B, labels=("A:", "B:")))