machinevision-toolbox-python 2.0.1__py3-none-any.whl → 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {machinevision_toolbox_python-2.0.1.dist-info → machinevision_toolbox_python-2.1.0.dist-info}/METADATA +13 -5
- {machinevision_toolbox_python-2.0.1.dist-info → machinevision_toolbox_python-2.1.0.dist-info}/RECORD +21 -31
- {machinevision_toolbox_python-2.0.1.dist-info → machinevision_toolbox_python-2.1.0.dist-info}/WHEEL +1 -1
- machinevisiontoolbox/BundleAdjust.py +1 -1
- machinevisiontoolbox/Camera.py +95 -80
- machinevisiontoolbox/ImageBlobs.py +10 -4
- machinevisiontoolbox/ImageCore.py +105 -97
- machinevisiontoolbox/ImageIO.py +5 -1
- machinevisiontoolbox/ImagePointFeatures.py +8 -6
- machinevisiontoolbox/ImageProcessing.py +1 -1
- machinevisiontoolbox/ImageRegionFeatures.py +9 -6
- machinevisiontoolbox/ImageWholeFeatures.py +113 -72
- machinevisiontoolbox/Sources.py +1 -1
- machinevisiontoolbox/__init__.py +1 -1
- machinevisiontoolbox/base/imageio.py +68 -33
- machinevisiontoolbox/bin/_iminteractive.py +461 -0
- machinevisiontoolbox/bin/bagtool.py +120 -6
- machinevisiontoolbox/bin/imtool.py +34 -5
- machinevisiontoolbox/bin/mvtbtool.py +12 -5
- machinevisiontoolbox/ImageCore.docstring +0 -2877
- machinevisiontoolbox/aruco0.pdf +0 -0
- machinevisiontoolbox/aruco50.pdf +0 -0
- machinevisiontoolbox/bin/labeltool.py +0 -147
- machinevisiontoolbox/blocks/Icons/icons.sh +0 -4
- machinevisiontoolbox/cvfuncs.py +0 -40
- machinevisiontoolbox/docbugs.py +0 -47
- machinevisiontoolbox/newcameras.py +0 -55
- machinevisiontoolbox/test_skimage.py +0 -12
- machinevisiontoolbox/testblobplots.py +0 -17
- machinevisiontoolbox/x.json +0 -296
- {machinevision_toolbox_python-2.0.1.dist-info → machinevision_toolbox_python-2.1.0.dist-info}/entry_points.txt +0 -0
- {machinevision_toolbox_python-2.0.1.dist-info → machinevision_toolbox_python-2.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -301,6 +301,57 @@ class Image(
|
|
|
301
301
|
)
|
|
302
302
|
|
|
303
303
|
# if dtype is not given, determine the appropriate type for the data
|
|
304
|
+
dtype = self._infer_dtype(image, dtype)
|
|
305
|
+
|
|
306
|
+
if binary:
|
|
307
|
+
image = image > 0
|
|
308
|
+
|
|
309
|
+
# change type of array to the determined dtype
|
|
310
|
+
if dtype is not None:
|
|
311
|
+
image = image.astype(dtype, copy=False)
|
|
312
|
+
|
|
313
|
+
self.name = name
|
|
314
|
+
|
|
315
|
+
color_dict = Image.colororder2dict(colororder)
|
|
316
|
+
|
|
317
|
+
image = self._reshape_to_size(image, size)
|
|
318
|
+
|
|
319
|
+
if image.ndim not in (2, 3):
|
|
320
|
+
raise ValueError(
|
|
321
|
+
"bad ndarray passed to Image constructor: must be 2D or 3D array"
|
|
322
|
+
)
|
|
323
|
+
if image.ndim == 3 and image.shape[2] == 1:
|
|
324
|
+
image = image[:, :, 0] # squeeze out singleton plane
|
|
325
|
+
|
|
326
|
+
if kwargs:
|
|
327
|
+
image = convert(image, **kwargs)
|
|
328
|
+
|
|
329
|
+
# assign the image to the object, copying if requested
|
|
330
|
+
if copy:
|
|
331
|
+
self._A = image.copy()
|
|
332
|
+
else:
|
|
333
|
+
self._A = image
|
|
334
|
+
|
|
335
|
+
# final check that colororder length matches number of planes
|
|
336
|
+
if colororder is not None:
|
|
337
|
+
if len(color_dict) != self.nplanes:
|
|
338
|
+
raise ValueError("colororder length does not match number of planes")
|
|
339
|
+
|
|
340
|
+
if colororder is None:
|
|
341
|
+
if self.nplanes == 3:
|
|
342
|
+
self.colororder = "RGB"
|
|
343
|
+
# warnings.warn("defaulting color to RGB")
|
|
344
|
+
else:
|
|
345
|
+
self.colororder = color_dict
|
|
346
|
+
|
|
347
|
+
self._stats = None
|
|
348
|
+
|
|
349
|
+
self.name = name
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def _infer_dtype(image: np.ndarray, dtype: Dtype | bool | None) -> np.dtype | None:
|
|
353
|
+
"""Determine the pixel dtype for the constructor when ``dtype`` isn't
|
|
354
|
+
given explicitly, or resolve/validate it when it is."""
|
|
304
355
|
if dtype is None:
|
|
305
356
|
# no type given, automatically choose it
|
|
306
357
|
if np.issubdtype(image.dtype, np.floating):
|
|
@@ -335,17 +386,12 @@ class Image(
|
|
|
335
386
|
except TypeError:
|
|
336
387
|
raise ValueError("bad dtype argument passed to Image constructor")
|
|
337
388
|
|
|
338
|
-
|
|
339
|
-
image = image > 0
|
|
340
|
-
|
|
341
|
-
# change type of array to the determined dtype
|
|
342
|
-
if dtype is not None:
|
|
343
|
-
image = image.astype(dtype, copy=False)
|
|
344
|
-
|
|
345
|
-
self.name = name
|
|
346
|
-
|
|
347
|
-
color_dict = Image.colororder2dict(colororder)
|
|
389
|
+
return dtype
|
|
348
390
|
|
|
391
|
+
def _reshape_to_size(
|
|
392
|
+
self, image: np.ndarray, size: tuple | list | None
|
|
393
|
+
) -> np.ndarray:
|
|
394
|
+
"""Reshape 1D/2D pixel data per the constructor's ``size=`` argument."""
|
|
349
395
|
if isinstance(size, self.__class__):
|
|
350
396
|
# size is an Image instance, ignore the size/shape and use the image's shape
|
|
351
397
|
size = size.size
|
|
@@ -354,16 +400,7 @@ class Image(
|
|
|
354
400
|
if size is not None:
|
|
355
401
|
newsize = [size[1], size[0]]
|
|
356
402
|
|
|
357
|
-
if
|
|
358
|
-
nplanes = len(color_dict)
|
|
359
|
-
|
|
360
|
-
if len(size) == 3:
|
|
361
|
-
if nplanes != size[2]:
|
|
362
|
-
raise ValueError(
|
|
363
|
-
"colororder length does not match number of planes in size"
|
|
364
|
-
)
|
|
365
|
-
newsize.append(nplanes)
|
|
366
|
-
elif len(size) == 3:
|
|
403
|
+
if len(size) == 3:
|
|
367
404
|
newsize.append(size[2])
|
|
368
405
|
|
|
369
406
|
if image.ndim == 1:
|
|
@@ -420,37 +457,7 @@ class Image(
|
|
|
420
457
|
|
|
421
458
|
image = image.reshape(*newsize)
|
|
422
459
|
|
|
423
|
-
|
|
424
|
-
raise ValueError(
|
|
425
|
-
"bad ndarray passed to Image constructor: must be 2D or 3D array"
|
|
426
|
-
)
|
|
427
|
-
if image.ndim == 3 and image.shape[2] == 1:
|
|
428
|
-
image = image[:, :, 0] # squeeze out singleton plane
|
|
429
|
-
|
|
430
|
-
if kwargs:
|
|
431
|
-
image = convert(image, **kwargs)
|
|
432
|
-
|
|
433
|
-
# assign the image to the object, copying if requested
|
|
434
|
-
if copy:
|
|
435
|
-
self._A = image.copy()
|
|
436
|
-
else:
|
|
437
|
-
self._A = image
|
|
438
|
-
|
|
439
|
-
# final check that colororder length matches number of planes
|
|
440
|
-
if colororder is not None:
|
|
441
|
-
if len(color_dict) != self.nplanes:
|
|
442
|
-
raise ValueError("colororder length does not match number of planes")
|
|
443
|
-
|
|
444
|
-
if colororder is None:
|
|
445
|
-
if self.nplanes == 3:
|
|
446
|
-
self.colororder = "RGB"
|
|
447
|
-
# warnings.warn("defaulting color to RGB")
|
|
448
|
-
else:
|
|
449
|
-
self.colororder = color_dict
|
|
450
|
-
|
|
451
|
-
self._stats = None
|
|
452
|
-
|
|
453
|
-
self.name = name
|
|
460
|
+
return image
|
|
454
461
|
|
|
455
462
|
@staticmethod
|
|
456
463
|
def _plane_stats(plane: np.ndarray) -> dict[str, float | int]:
|
|
@@ -2711,50 +2718,6 @@ class Image(
|
|
|
2711
2718
|
:seealso: :meth:`red` :meth:`green` :meth:`blue` :meth:`plane` :meth:`roi` :meth:`pixel`
|
|
2712
2719
|
"""
|
|
2713
2720
|
|
|
2714
|
-
def fixdims(
|
|
2715
|
-
out: np.ndarray, shape: tuple[int, ...], keys: tuple[Any, ...]
|
|
2716
|
-
) -> np.ndarray:
|
|
2717
|
-
# deal with the fact that some of the keys may have reduced the
|
|
2718
|
-
# dimensionality of the array, eg. a slice of span 0 or 1, or an integer
|
|
2719
|
-
# key.
|
|
2720
|
-
|
|
2721
|
-
# shape: (nrows, ncols, nplanes) in NumPy order
|
|
2722
|
-
# key: (rowspec, colspec, planespec) in NumPy order
|
|
2723
|
-
|
|
2724
|
-
def lenkey(key: int | slice, max: int) -> int:
|
|
2725
|
-
# compute the span of a particular key
|
|
2726
|
-
if isinstance(key, int):
|
|
2727
|
-
# int key has a span on 1
|
|
2728
|
-
return 1
|
|
2729
|
-
elif isinstance(key, slice):
|
|
2730
|
-
# slice key has a span depending on the slice and the corresponding
|
|
2731
|
-
# array dimension. we have to essentially replicate the logic of
|
|
2732
|
-
# slice() here.
|
|
2733
|
-
start = key.start if key.start is not None else 0
|
|
2734
|
-
stop = key.stop if key.stop is not None else max
|
|
2735
|
-
step = key.step if key.step is not None else 1
|
|
2736
|
-
n = stop - start
|
|
2737
|
-
if n < step:
|
|
2738
|
-
return 1
|
|
2739
|
-
else:
|
|
2740
|
-
return n // step
|
|
2741
|
-
|
|
2742
|
-
dims = list(shape)
|
|
2743
|
-
|
|
2744
|
-
if len(shape) == 2:
|
|
2745
|
-
dims = [lenkey(keys[0], shape[0]), lenkey(keys[1], shape[1])]
|
|
2746
|
-
elif len(shape) == 3:
|
|
2747
|
-
dims = [
|
|
2748
|
-
lenkey(keys[0], shape[0]),
|
|
2749
|
-
lenkey(keys[1], shape[1]),
|
|
2750
|
-
lenkey(keys[2], shape[2]),
|
|
2751
|
-
]
|
|
2752
|
-
# ignore loss of color dimension
|
|
2753
|
-
if dims[2] == 1:
|
|
2754
|
-
dims = dims[:2]
|
|
2755
|
-
|
|
2756
|
-
return out.reshape(tuple(dims))
|
|
2757
|
-
|
|
2758
2721
|
if (
|
|
2759
2722
|
isinstance(keys, tuple)
|
|
2760
2723
|
and len(keys) == 2
|
|
@@ -2791,7 +2754,7 @@ class Image(
|
|
|
2791
2754
|
out = self._A[keys]
|
|
2792
2755
|
|
|
2793
2756
|
if out.ndim < 3:
|
|
2794
|
-
out =
|
|
2757
|
+
out = self._fixdims(out, self._A.shape, keys)
|
|
2795
2758
|
|
|
2796
2759
|
else:
|
|
2797
2760
|
# greyscale image
|
|
@@ -2804,7 +2767,7 @@ class Image(
|
|
|
2804
2767
|
out = self._A[keys]
|
|
2805
2768
|
|
|
2806
2769
|
if out.ndim < 2:
|
|
2807
|
-
out =
|
|
2770
|
+
out = self._fixdims(out, self._A.shape, keys)
|
|
2808
2771
|
|
|
2809
2772
|
# a singleton plane dimensions is a grey scale image
|
|
2810
2773
|
if out.ndim == 3 and out.shape[2] == 1:
|
|
@@ -2825,6 +2788,51 @@ class Image(
|
|
|
2825
2788
|
else:
|
|
2826
2789
|
raise ValueError("invalid slice")
|
|
2827
2790
|
|
|
2791
|
+
@staticmethod
|
|
2792
|
+
def _lenkey(key: int | slice, max: int) -> int:
|
|
2793
|
+
"""Span of a single ``__getitem__`` key component (int or slice)."""
|
|
2794
|
+
if isinstance(key, int):
|
|
2795
|
+
# int key has a span on 1
|
|
2796
|
+
return 1
|
|
2797
|
+
elif isinstance(key, slice):
|
|
2798
|
+
# slice key has a span depending on the slice and the corresponding
|
|
2799
|
+
# array dimension. we have to essentially replicate the logic of
|
|
2800
|
+
# slice() here.
|
|
2801
|
+
start = key.start if key.start is not None else 0
|
|
2802
|
+
stop = key.stop if key.stop is not None else max
|
|
2803
|
+
step = key.step if key.step is not None else 1
|
|
2804
|
+
n = stop - start
|
|
2805
|
+
if n < step:
|
|
2806
|
+
return 1
|
|
2807
|
+
else:
|
|
2808
|
+
return n // step
|
|
2809
|
+
|
|
2810
|
+
@classmethod
|
|
2811
|
+
def _fixdims(
|
|
2812
|
+
cls, out: np.ndarray, shape: tuple[int, ...], keys: tuple[Any, ...]
|
|
2813
|
+
) -> np.ndarray:
|
|
2814
|
+
"""Restore dimensionality lost by ``__getitem__`` keys that collapse
|
|
2815
|
+
an axis (eg. a slice of span 0 or 1, or an integer key).
|
|
2816
|
+
|
|
2817
|
+
``shape``: (nrows, ncols, nplanes) in NumPy order.
|
|
2818
|
+
``keys``: (rowspec, colspec, planespec) in NumPy order.
|
|
2819
|
+
"""
|
|
2820
|
+
dims = list(shape)
|
|
2821
|
+
|
|
2822
|
+
if len(shape) == 2:
|
|
2823
|
+
dims = [cls._lenkey(keys[0], shape[0]), cls._lenkey(keys[1], shape[1])]
|
|
2824
|
+
elif len(shape) == 3:
|
|
2825
|
+
dims = [
|
|
2826
|
+
cls._lenkey(keys[0], shape[0]),
|
|
2827
|
+
cls._lenkey(keys[1], shape[1]),
|
|
2828
|
+
cls._lenkey(keys[2], shape[2]),
|
|
2829
|
+
]
|
|
2830
|
+
# ignore loss of color dimension
|
|
2831
|
+
if dims[2] == 1:
|
|
2832
|
+
dims = dims[:2]
|
|
2833
|
+
|
|
2834
|
+
return out.reshape(tuple(dims))
|
|
2835
|
+
|
|
2828
2836
|
def pixel(self, u: int, v: int) -> int | float | np.ndarray:
|
|
2829
2837
|
"""
|
|
2830
2838
|
Return pixel value
|
machinevisiontoolbox/ImageIO.py
CHANGED
|
@@ -178,10 +178,14 @@ class ImageIOMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
178
178
|
self.domain[1][0],
|
|
179
179
|
]
|
|
180
180
|
|
|
181
|
+
if self.colororder_str is not None:
|
|
182
|
+
colororder = self.colororder_str.replace(":", "")
|
|
183
|
+
else:
|
|
184
|
+
colororder = None
|
|
181
185
|
return idisp(
|
|
182
186
|
self._A,
|
|
183
187
|
title=display_title,
|
|
184
|
-
colororder=
|
|
188
|
+
colororder=colororder,
|
|
185
189
|
**kwargs,
|
|
186
190
|
)
|
|
187
191
|
|
|
@@ -3,7 +3,7 @@ Detection and matching of keypoint and descriptor features (SIFT, ORB, etc.) in
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import math
|
|
6
|
-
from typing import Any, Iterator
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Iterator
|
|
7
7
|
|
|
8
8
|
import cv2
|
|
9
9
|
import matplotlib.lines as mlines
|
|
@@ -21,13 +21,15 @@ from machinevisiontoolbox.base import (
|
|
|
21
21
|
name2color,
|
|
22
22
|
safe_plt_show,
|
|
23
23
|
)
|
|
24
|
-
from machinevisiontoolbox._image_typing import _ImageBase
|
|
25
24
|
from machinevisiontoolbox.decorators import (
|
|
26
25
|
array_result,
|
|
27
26
|
array_result2,
|
|
28
27
|
scalar_result,
|
|
29
28
|
)
|
|
30
29
|
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from machinevisiontoolbox._image_typing import _ImageBase
|
|
32
|
+
|
|
31
33
|
# TODO, either subclass SIFTFeature(BaseFeature2D) or just use BaseFeature2D
|
|
32
34
|
# directly
|
|
33
35
|
|
|
@@ -280,7 +282,7 @@ class BaseFeature2D:
|
|
|
280
282
|
|
|
281
283
|
try: # Sort features into grid
|
|
282
284
|
nw, nh = nbins
|
|
283
|
-
except:
|
|
285
|
+
except (TypeError, ValueError):
|
|
284
286
|
nw = nbins
|
|
285
287
|
nh = nbins
|
|
286
288
|
|
|
@@ -294,8 +296,8 @@ class BaseFeature2D:
|
|
|
294
296
|
bins = np.zeros((nh, nw), dtype="int")
|
|
295
297
|
|
|
296
298
|
for f in self:
|
|
297
|
-
ix = f.p[0] // binwidth
|
|
298
|
-
iy = f.p[1] // binheight
|
|
299
|
+
ix = int(f.p[0].item() // binwidth)
|
|
300
|
+
iy = int(f.p[1].item() // binheight)
|
|
299
301
|
|
|
300
302
|
if bins[iy, ix] < nfeat:
|
|
301
303
|
keep.append(f)
|
|
@@ -2012,7 +2014,7 @@ class LUCIDFeature(BaseFeature2D):
|
|
|
2012
2014
|
pass
|
|
2013
2015
|
|
|
2014
2016
|
|
|
2015
|
-
class ImagePointFeaturesMixin(_ImageBase):
|
|
2017
|
+
class ImagePointFeaturesMixin(_ImageBase if TYPE_CHECKING else object):
|
|
2016
2018
|
def _image2feature(
|
|
2017
2019
|
self,
|
|
2018
2020
|
cls,
|
|
@@ -1146,7 +1146,7 @@ class ImageProcessingMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
1146
1146
|
else:
|
|
1147
1147
|
try:
|
|
1148
1148
|
color = argcheck.getvector(image2, 3)
|
|
1149
|
-
except:
|
|
1149
|
+
except Exception:
|
|
1150
1150
|
raise ValueError("expecting a scalar, string or 3-vector")
|
|
1151
1151
|
if self.isbgr:
|
|
1152
1152
|
color = color[::-1]
|
|
@@ -9,7 +9,7 @@ import matplotlib.pyplot as plt
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
from ansitable import ANSITable, Column
|
|
11
11
|
from spatialmath import SE3
|
|
12
|
-
from typing import Any
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
13
|
|
|
14
14
|
from machinevisiontoolbox.decorators import array_result
|
|
15
15
|
from machinevisiontoolbox.base import plot_labelbox
|
|
@@ -22,8 +22,11 @@ except ImportError:
|
|
|
22
22
|
_pytesseract = None
|
|
23
23
|
_pytesseract_available = False
|
|
24
24
|
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from machinevisiontoolbox._image_typing import _ImageBase
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
class ImageRegionFeaturesMixin(_ImageBase if TYPE_CHECKING else object):
|
|
27
30
|
def MSER(self, **kwargs: Any) -> "MSERFeature":
|
|
28
31
|
"""
|
|
29
32
|
Find MSER features in image
|
|
@@ -239,7 +242,7 @@ class MSERFeature:
|
|
|
239
242
|
|
|
240
243
|
return new
|
|
241
244
|
|
|
242
|
-
def __str__(self):
|
|
245
|
+
def __str__(self) -> str:
|
|
243
246
|
"""
|
|
244
247
|
String representation of MSER
|
|
245
248
|
|
|
@@ -262,7 +265,7 @@ class MSERFeature:
|
|
|
262
265
|
s = f"MSER feature: u: {self._bboxes[0,0]} - {self._bboxes[0,2]}, v: {self._bboxes[0,1]} - {self._bboxes[0,3]}"
|
|
263
266
|
return s
|
|
264
267
|
|
|
265
|
-
def __repr__(self):
|
|
268
|
+
def __repr__(self) -> str:
|
|
266
269
|
"""
|
|
267
270
|
Representation of MSER
|
|
268
271
|
|
|
@@ -370,7 +373,7 @@ class OCRWord:
|
|
|
370
373
|
for key in ocr.keys():
|
|
371
374
|
self.dict[key] = ocr[key][i]
|
|
372
375
|
|
|
373
|
-
def __str__(self):
|
|
376
|
+
def __str__(self) -> str:
|
|
374
377
|
"""
|
|
375
378
|
String representation of MSER
|
|
376
379
|
|
|
@@ -379,7 +382,7 @@ class OCRWord:
|
|
|
379
382
|
"""
|
|
380
383
|
return f"{self.dict['text']} ({self.dict['conf']}%)"
|
|
381
384
|
|
|
382
|
-
def __repr__(self):
|
|
385
|
+
def __repr__(self) -> str:
|
|
383
386
|
return str(self)
|
|
384
387
|
|
|
385
388
|
@property
|
|
@@ -11,6 +11,7 @@ import cv2
|
|
|
11
11
|
import matplotlib.pyplot as plt
|
|
12
12
|
import numpy as np
|
|
13
13
|
import scipy as sp
|
|
14
|
+
from matplotlib.axes import Axes
|
|
14
15
|
from matplotlib.patches import Polygon
|
|
15
16
|
from matplotlib.ticker import ScalarFormatter
|
|
16
17
|
from spatialmath import SE3, base
|
|
@@ -519,6 +520,11 @@ class ImageWholeFeaturesMixin(_ImageBase if TYPE_CHECKING else object):
|
|
|
519
520
|
.. deprecated:: 2.0.0
|
|
520
521
|
Use ``hist().cdf`` instead.
|
|
521
522
|
"""
|
|
523
|
+
warnings.warn(
|
|
524
|
+
"Deprecated in 2.0.0: use hist().cdf instead of ncdf.",
|
|
525
|
+
DeprecationWarning,
|
|
526
|
+
stacklevel=2,
|
|
527
|
+
)
|
|
522
528
|
hist = self._default_hist()
|
|
523
529
|
return hist.cdf
|
|
524
530
|
|
|
@@ -1193,26 +1199,31 @@ class Histogram:
|
|
|
1193
1199
|
:rtype: ndarray(N) or ndarray(N,P)
|
|
1194
1200
|
|
|
1195
1201
|
.. deprecated:: 2.0.0
|
|
1196
|
-
Use ``
|
|
1202
|
+
Use ``cdf`` instead.
|
|
1197
1203
|
"""
|
|
1204
|
+
warnings.warn(
|
|
1205
|
+
"Deprecated in 2.0.0: use .cdf instead of .ncdf.",
|
|
1206
|
+
DeprecationWarning,
|
|
1207
|
+
stacklevel=2,
|
|
1208
|
+
)
|
|
1198
1209
|
return self.cdf
|
|
1199
1210
|
|
|
1200
1211
|
def plot(
|
|
1201
1212
|
self,
|
|
1202
|
-
type="frequency",
|
|
1203
|
-
block=False,
|
|
1204
|
-
filled=None,
|
|
1205
|
-
stats=True,
|
|
1206
|
-
style="stack",
|
|
1207
|
-
cursor=False,
|
|
1208
|
-
alpha=0.5,
|
|
1209
|
-
title=None,
|
|
1210
|
-
log=False,
|
|
1211
|
-
samescale=False,
|
|
1212
|
-
ax=None,
|
|
1213
|
-
bar=None,
|
|
1214
|
-
**kwargs,
|
|
1215
|
-
):
|
|
1213
|
+
type: str = "frequency",
|
|
1214
|
+
block: bool = False,
|
|
1215
|
+
filled: bool | None = None,
|
|
1216
|
+
stats: bool = True,
|
|
1217
|
+
style: str = "stack",
|
|
1218
|
+
cursor: bool = False,
|
|
1219
|
+
alpha: float = 0.5,
|
|
1220
|
+
title: str | None = None,
|
|
1221
|
+
log: bool = False,
|
|
1222
|
+
samescale: bool = False,
|
|
1223
|
+
ax: Axes | None = None,
|
|
1224
|
+
bar: bool | None = None,
|
|
1225
|
+
**kwargs: Any,
|
|
1226
|
+
) -> None:
|
|
1216
1227
|
"""
|
|
1217
1228
|
Plot histogram
|
|
1218
1229
|
|
|
@@ -1343,38 +1354,9 @@ class Histogram:
|
|
|
1343
1354
|
samescale = True
|
|
1344
1355
|
|
|
1345
1356
|
# figure vertical axis labels and scaling based on histogram type
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
maxy = np.max(y)
|
|
1350
|
-
else:
|
|
1351
|
-
maxy = np.max(y, axis=0)
|
|
1352
|
-
ylabel1 = "frequency"
|
|
1353
|
-
ylabel2 = "frequency"
|
|
1354
|
-
if filled is None:
|
|
1355
|
-
filled = True
|
|
1356
|
-
elif type in ("pdf", "probability"):
|
|
1357
|
-
y = self.pdf
|
|
1358
|
-
if samescale:
|
|
1359
|
-
maxy = np.max(y)
|
|
1360
|
-
else:
|
|
1361
|
-
maxy = np.max(y, axis=0)
|
|
1362
|
-
ylabel1 = "PDF"
|
|
1363
|
-
ylabel2 = "probability density"
|
|
1364
|
-
elif type in ("cf", "cumulative"):
|
|
1365
|
-
y = self.cf
|
|
1366
|
-
maxy = y[
|
|
1367
|
-
-1, 0
|
|
1368
|
-
] # last row values are all the same, total number of pixels in plane
|
|
1369
|
-
ylabel1 = "cumulative frequency"
|
|
1370
|
-
ylabel2 = "cumulative frequency"
|
|
1371
|
-
elif type in ("cdf", "normalized"):
|
|
1372
|
-
y = self.cdf
|
|
1373
|
-
maxy = 1
|
|
1374
|
-
ylabel1 = "CDF"
|
|
1375
|
-
ylabel2 = "normalized cumulative frequency"
|
|
1376
|
-
else:
|
|
1377
|
-
raise ValueError("unknown type")
|
|
1357
|
+
y, maxy, ylabel1, ylabel2 = self._compute_plot_series(type, samescale)
|
|
1358
|
+
if type == "frequency" and filled is None:
|
|
1359
|
+
filled = True
|
|
1378
1360
|
|
|
1379
1361
|
if self.nplanes == 1:
|
|
1380
1362
|
y = y[..., np.newaxis]
|
|
@@ -1391,17 +1373,6 @@ class Histogram:
|
|
|
1391
1373
|
if self.nplanes == 1:
|
|
1392
1374
|
hist_counts = hist_counts[..., np.newaxis]
|
|
1393
1375
|
|
|
1394
|
-
def plane_stats(counts: np.ndarray) -> tuple[float | None, float | None]:
|
|
1395
|
-
total = float(np.sum(counts))
|
|
1396
|
-
if total <= 0:
|
|
1397
|
-
return None, None
|
|
1398
|
-
|
|
1399
|
-
mean = float(np.sum(self.x * counts) / total)
|
|
1400
|
-
cdf = np.cumsum(counts)
|
|
1401
|
-
median_idx = int(np.searchsorted(cdf, 0.5 * total, side="left"))
|
|
1402
|
-
median = float(self.x[min(median_idx, len(self.x) - 1)])
|
|
1403
|
-
return mean, median
|
|
1404
|
-
|
|
1405
1376
|
if self._sorted:
|
|
1406
1377
|
xrange = (self.x[0], self.x[-1])
|
|
1407
1378
|
xlabel = "bin rank"
|
|
@@ -1448,7 +1419,7 @@ class Histogram:
|
|
|
1448
1419
|
ScalarFormatter(useOffset=False, useMathText=True)
|
|
1449
1420
|
)
|
|
1450
1421
|
if stats:
|
|
1451
|
-
mean, median =
|
|
1422
|
+
mean, median = self._histogram_plane_stats(hist_counts[:, i])
|
|
1452
1423
|
if mean is not None and median is not None:
|
|
1453
1424
|
ax.axvline(
|
|
1454
1425
|
mean,
|
|
@@ -1549,18 +1520,7 @@ class Histogram:
|
|
|
1549
1520
|
x = np.r_[xrange[0], x, xrange[1]]
|
|
1550
1521
|
_, ax = plt.subplots(1, 1)
|
|
1551
1522
|
|
|
1552
|
-
patchcolor =
|
|
1553
|
-
goodcolors = [c for c in "rgbykcm"]
|
|
1554
|
-
if self.colordict is None:
|
|
1555
|
-
self.colordict = {c: i for i, c in enumerate(goodcolors[:n])}
|
|
1556
|
-
colors = list(self.colordict.keys())
|
|
1557
|
-
patchcolor = [c.lower() for c in colors]
|
|
1558
|
-
else:
|
|
1559
|
-
for color, i in self.colordict.items():
|
|
1560
|
-
if color.lower() in "rgbykcm":
|
|
1561
|
-
patchcolor.append(color.lower())
|
|
1562
|
-
else:
|
|
1563
|
-
patchcolor.append(goodcolors.pop(0))
|
|
1523
|
+
colors, patchcolor = self._resolve_overlay_colors(n, colors)
|
|
1564
1524
|
|
|
1565
1525
|
if filled:
|
|
1566
1526
|
for i in range(n):
|
|
@@ -1580,7 +1540,7 @@ class Histogram:
|
|
|
1580
1540
|
ax.plot(x, np.r_[0, y[:, i], 0], color=patchcolor[i], **kwargs)
|
|
1581
1541
|
|
|
1582
1542
|
if stats:
|
|
1583
|
-
mean, median =
|
|
1543
|
+
mean, median = self._histogram_plane_stats(np.sum(hist_counts, axis=1))
|
|
1584
1544
|
if mean is not None and median is not None:
|
|
1585
1545
|
ax.axvline(
|
|
1586
1546
|
mean,
|
|
@@ -1617,6 +1577,87 @@ class Histogram:
|
|
|
1617
1577
|
set_window_title(title)
|
|
1618
1578
|
safe_plt_show(block=block)
|
|
1619
1579
|
|
|
1580
|
+
def _compute_plot_series(
|
|
1581
|
+
self, type: str, samescale: bool
|
|
1582
|
+
) -> tuple[np.ndarray, np.ndarray | float, str, str]:
|
|
1583
|
+
"""Compute plot()'s y-values, y-axis max, and y-axis labels for a
|
|
1584
|
+
given ``type``. Pure computation over self.h/self.pdf/self.cf/
|
|
1585
|
+
self.cdf -- no mutation."""
|
|
1586
|
+
if type == "frequency":
|
|
1587
|
+
y = self.h
|
|
1588
|
+
if samescale:
|
|
1589
|
+
maxy = np.max(y)
|
|
1590
|
+
else:
|
|
1591
|
+
maxy = np.max(y, axis=0)
|
|
1592
|
+
ylabel1 = "frequency"
|
|
1593
|
+
ylabel2 = "frequency"
|
|
1594
|
+
elif type in ("pdf", "probability"):
|
|
1595
|
+
y = self.pdf
|
|
1596
|
+
if samescale:
|
|
1597
|
+
maxy = np.max(y)
|
|
1598
|
+
else:
|
|
1599
|
+
maxy = np.max(y, axis=0)
|
|
1600
|
+
ylabel1 = "PDF"
|
|
1601
|
+
ylabel2 = "probability density"
|
|
1602
|
+
elif type in ("cf", "cumulative"):
|
|
1603
|
+
y = self.cf
|
|
1604
|
+
maxy = y[
|
|
1605
|
+
-1, 0
|
|
1606
|
+
] # last row values are all the same, total number of pixels in plane
|
|
1607
|
+
ylabel1 = "cumulative frequency"
|
|
1608
|
+
ylabel2 = "cumulative frequency"
|
|
1609
|
+
elif type in ("cdf", "normalized"):
|
|
1610
|
+
y = self.cdf
|
|
1611
|
+
maxy = 1
|
|
1612
|
+
ylabel1 = "CDF"
|
|
1613
|
+
ylabel2 = "normalized cumulative frequency"
|
|
1614
|
+
else:
|
|
1615
|
+
raise ValueError("unknown type")
|
|
1616
|
+
|
|
1617
|
+
return y, maxy, ylabel1, ylabel2
|
|
1618
|
+
|
|
1619
|
+
def _histogram_plane_stats(
|
|
1620
|
+
self, counts: np.ndarray
|
|
1621
|
+
) -> tuple[float | None, float | None]:
|
|
1622
|
+
"""Mean and median of a single plane's histogram counts, or
|
|
1623
|
+
(None, None) if the plane is empty. Used by both plot()'s stack
|
|
1624
|
+
and overlay styles."""
|
|
1625
|
+
total = float(np.sum(counts))
|
|
1626
|
+
if total <= 0:
|
|
1627
|
+
return None, None
|
|
1628
|
+
|
|
1629
|
+
mean = float(np.sum(self.x * counts) / total)
|
|
1630
|
+
cdf = np.cumsum(counts)
|
|
1631
|
+
median_idx = int(np.searchsorted(cdf, 0.5 * total, side="left"))
|
|
1632
|
+
median = float(self.x[min(median_idx, len(self.x) - 1)])
|
|
1633
|
+
return mean, median
|
|
1634
|
+
|
|
1635
|
+
def _resolve_overlay_colors(
|
|
1636
|
+
self, n: int, colors: list[str]
|
|
1637
|
+
) -> tuple[list[str], list[str]]:
|
|
1638
|
+
"""Resolve per-plane display colors for plot()'s style="overlay".
|
|
1639
|
+
|
|
1640
|
+
If self.colordict is unset, assigns a default one as a side effect
|
|
1641
|
+
(preserved intentionally -- this is existing behavior, not
|
|
1642
|
+
introduced by extracting this into its own method) and returns the
|
|
1643
|
+
colors derived from it; otherwise maps the existing colordict's
|
|
1644
|
+
color names onto a fallback palette for any non-standard names.
|
|
1645
|
+
"""
|
|
1646
|
+
patchcolor = []
|
|
1647
|
+
goodcolors = [c for c in "rgbykcm"]
|
|
1648
|
+
if self.colordict is None:
|
|
1649
|
+
self.colordict = {c: i for i, c in enumerate(goodcolors[:n])}
|
|
1650
|
+
colors = list(self.colordict.keys())
|
|
1651
|
+
patchcolor = [c.lower() for c in colors]
|
|
1652
|
+
else:
|
|
1653
|
+
for color, i in self.colordict.items():
|
|
1654
|
+
if color.lower() in "rgbykcm":
|
|
1655
|
+
patchcolor.append(color.lower())
|
|
1656
|
+
else:
|
|
1657
|
+
patchcolor.append(goodcolors.pop(0))
|
|
1658
|
+
|
|
1659
|
+
return colors, patchcolor
|
|
1660
|
+
|
|
1620
1661
|
def peaks(self, **kwargs: Any) -> np.ndarray | list[np.ndarray]:
|
|
1621
1662
|
r"""
|
|
1622
1663
|
Histogram peaks
|
machinevisiontoolbox/Sources.py
CHANGED
machinevisiontoolbox/__init__.py
CHANGED