machinevision-toolbox-python 2.0.1__py3-none-any.whl → 2.2.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.
Files changed (34) hide show
  1. {machinevision_toolbox_python-2.0.1.dist-info → machinevision_toolbox_python-2.2.0.dist-info}/METADATA +13 -5
  2. {machinevision_toolbox_python-2.0.1.dist-info → machinevision_toolbox_python-2.2.0.dist-info}/RECORD +23 -33
  3. {machinevision_toolbox_python-2.0.1.dist-info → machinevision_toolbox_python-2.2.0.dist-info}/WHEEL +1 -1
  4. machinevisiontoolbox/BundleAdjust.py +1 -1
  5. machinevisiontoolbox/Camera.py +95 -80
  6. machinevisiontoolbox/ImageBlobs.py +10 -4
  7. machinevisiontoolbox/ImageCore.py +105 -97
  8. machinevisiontoolbox/ImageFiducials.py +62 -10
  9. machinevisiontoolbox/ImageIO.py +5 -1
  10. machinevisiontoolbox/ImageLineFeatures.py +9 -1
  11. machinevisiontoolbox/ImagePointFeatures.py +29 -8
  12. machinevisiontoolbox/ImageProcessing.py +1 -1
  13. machinevisiontoolbox/ImageRegionFeatures.py +25 -6
  14. machinevisiontoolbox/ImageWholeFeatures.py +113 -72
  15. machinevisiontoolbox/Sources.py +1 -1
  16. machinevisiontoolbox/__init__.py +1 -1
  17. machinevisiontoolbox/base/imageio.py +68 -33
  18. machinevisiontoolbox/bin/_iminteractive.py +461 -0
  19. machinevisiontoolbox/bin/bagtool.py +120 -6
  20. machinevisiontoolbox/bin/imtool.py +34 -5
  21. machinevisiontoolbox/bin/mvtbtool.py +93 -18
  22. machinevisiontoolbox/ImageCore.docstring +0 -2877
  23. machinevisiontoolbox/aruco0.pdf +0 -0
  24. machinevisiontoolbox/aruco50.pdf +0 -0
  25. machinevisiontoolbox/bin/labeltool.py +0 -147
  26. machinevisiontoolbox/blocks/Icons/icons.sh +0 -4
  27. machinevisiontoolbox/cvfuncs.py +0 -40
  28. machinevisiontoolbox/docbugs.py +0 -47
  29. machinevisiontoolbox/newcameras.py +0 -55
  30. machinevisiontoolbox/test_skimage.py +0 -12
  31. machinevisiontoolbox/testblobplots.py +0 -17
  32. machinevisiontoolbox/x.json +0 -296
  33. {machinevision_toolbox_python-2.0.1.dist-info → machinevision_toolbox_python-2.2.0.dist-info}/entry_points.txt +0 -0
  34. {machinevision_toolbox_python-2.0.1.dist-info → machinevision_toolbox_python-2.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -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 ``hist().cdf`` instead.
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
- if type == "frequency":
1347
- y = self.h
1348
- if samescale:
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 = plane_stats(hist_counts[:, i])
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 = plane_stats(np.sum(hist_counts, axis=1))
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
@@ -3113,7 +3113,7 @@ class ROSBag(ImageSource):
3113
3113
  reader.close()
3114
3114
  self.reader = None
3115
3115
 
3116
- def _open_reader(self):
3116
+ def _open_reader(self) -> Any:
3117
3117
  if self.reader is not None:
3118
3118
  return self.reader
3119
3119
 
@@ -188,5 +188,5 @@ try:
188
188
  import importlib.metadata
189
189
 
190
190
  __version__ = importlib.metadata.version("machinevision-toolbox-python")
191
- except:
191
+ except importlib.metadata.PackageNotFoundError:
192
192
  pass
@@ -707,13 +707,14 @@ def idisp(
707
707
 
708
708
  # display the image
709
709
  if len(im.shape) == 3:
710
- # reverse the color planes if it's color
711
- if colororder not in ("RGB", "BGR"):
712
- raise ValueError("unknown colororder ", colororder)
713
- if colororder == "BGR":
714
- im = im[:, :, ::-1]
715
- h = ax.imshow(im, norm=norm, cmap=cmap, **options)
710
+ # color image, display in RGB or RGBA order; imshow handles alpha
711
+ # natively for both uint8 (0-255) and float (0.0-1.0) arrays
712
+ target = "RGBA" if "A" in colororder else "RGB"
713
+ idx = [colororder.find(c) for c in target if colororder.find(c) >= 0]
714
+ im_rgb = im[:, :, idx] # reorder planes → RGB or RGBA
715
+ h = ax.imshow(im_rgb, norm=norm, cmap=cmap, **options)
716
716
  else:
717
+ # monochrome image, display in indexed mode, where pixel value is mapped through the colormap
717
718
  if norm is None:
718
719
  # exclude NaN values
719
720
  if vrange is None:
@@ -889,7 +890,19 @@ def idisp(
889
890
  # At this point title is guaranteed to be a string
890
891
  assert title is not None
891
892
  cv2.namedWindow(title, cv2.WINDOW_AUTOSIZE)
892
- cv2.imshow(title, im) # make sure BGR format image
893
+
894
+ if im.ndim == 3:
895
+ # color image, display in BGR order, but allow user to specify color order of input image
896
+ idx = []
897
+ for c in "BGR":
898
+ j = colororder.find(c)
899
+ if j >= 0:
900
+ idx.append(j)
901
+ cv2.imshow(title, im[:, :, idx]) # convert → BGR order and display
902
+ else:
903
+ # monochrome image, display in indexed mode, where pixel value is mapped through the colormap
904
+ cv2.imshow(title, im) # make sure BGR format image
905
+
893
906
  cv2.waitKey(1)
894
907
 
895
908
  if fps is not None:
@@ -1028,7 +1041,7 @@ def iread(
1028
1041
  headers={"User-Agent": "machinevisiontoolbox-python/1.0"},
1029
1042
  )
1030
1043
  try:
1031
- with urllib.request.urlopen(req, context=ctx) as resp:
1044
+ with urllib.request.urlopen(req, context=ctx, timeout=10) as resp:
1032
1045
  if resp.status != 200:
1033
1046
  raise ValueError(f"HTTP {resp.status} fetching {filename}")
1034
1047
  array = np.asarray(bytearray(resp.read()), dtype="uint8")
@@ -1126,7 +1139,9 @@ def iread_iter(
1126
1139
 
1127
1140
  # read a single file
1128
1141
  path = Path(mvtb_path_to_datafile("images", path))
1129
- image = cv2.imread(path.as_posix(), cv2.IMREAD_UNCHANGED)
1142
+ image = cv2.imread(
1143
+ path.as_posix(), cv2.IMREAD_UNCHANGED
1144
+ ) # read BGR or BGRA image, or greyscale if that's what the file contains
1130
1145
  if image is None:
1131
1146
  raise ValueError(f"Could not read {filename}")
1132
1147
  yield (convert(image, **kwargs), str(path))
@@ -1153,7 +1168,7 @@ def convert(
1153
1168
  """
1154
1169
  Convert image
1155
1170
 
1156
- :param image: input image
1171
+ :param image: input image in BGR or BGRA order, as read by OpenCV
1157
1172
  :type image: ndarray(H,W), ndarray(H,W,P)
1158
1173
  :param mono: convert to grey scale, synonym for ``grey``
1159
1174
  :type mono: bool, optional
@@ -1207,20 +1222,29 @@ def convert(
1207
1222
  )
1208
1223
  image_original = image
1209
1224
 
1225
+ # handle color plane order and alpha channel
1226
+ # note that Torch doesn't like views of arrays with non-standard strides, so we use fancy indexing which implicitly makes a copy
1227
+
1210
1228
  if image.ndim == 3 and image.shape[2] in (3, 4):
1211
- # is color image RGB, RGBA, BGR, BGRA
1212
- if not alpha:
1213
- # optionally remove the alpha plane
1214
- image = image[:, :, :3]
1229
+ # color image, convert to RGB order if requested, otherwise leave in BGR order
1215
1230
  if rgb:
1216
- # optionally invert the color planes
1217
- image = np.copy(image[:, :, ::-1]) # reverse the planes
1218
- # np.copy() is required to make torchvision happy
1219
- colororder = "RGB"
1231
+ # RGB order is more common in Python, but OpenCV reads in BGR order, so convert to RGB
1232
+ if alpha and image.shape[2] == 4:
1233
+ # BGRA→RGBA
1234
+ image = image[:, :, [2, 1, 0, 3]] # BGRA → RGBA
1235
+ colororder = "RGBA"
1236
+ else:
1237
+ # BGR→RGB
1238
+ image = image[:, :, [2, 1, 0]]
1239
+ colororder = "RGB"
1220
1240
  else:
1221
- colororder = "BGR"
1222
- if alpha:
1223
- colororder += "A"
1241
+ # leave in BGR order, which is more efficient for OpenCV processing, but less common in Python
1242
+ if alpha and image.shape[2] == 4:
1243
+ colororder = "BGRA"
1244
+ else:
1245
+ # BGRA→BGR
1246
+ image = image[:, :, :3] # BGRA → BGR
1247
+ colororder = "BGR"
1224
1248
 
1225
1249
  mono = mono or gray or grey
1226
1250
  if mono and len(image.shape) == 3:
@@ -1284,32 +1308,43 @@ def iwrite(
1284
1308
  :return: successful write
1285
1309
  :rtype: bool
1286
1310
 
1287
- Writes the image ``im`` to ``filename`` using cv2.imwrite(), passing any
1288
- keyword arguments as options. The file type is taken from the extension in
1289
- ``filename``.
1311
+ Writes the image ``im`` to ``filename`` using cv2.imwrite(), passing any keyword
1312
+ arguments as options. The file type is taken from the extension in ``filename``.
1290
1313
 
1291
1314
  Example:
1292
1315
 
1293
1316
  .. code-block:: python
1294
1317
 
1295
- from machinevisiontoolbox import iwrite
1296
- import numpy as np
1297
- image = np.zeros((20,20)) # 20x20 black image
1298
- iwrite(image, "black.png")
1318
+ from machinevisiontoolbox import iwrite import numpy as np image =
1319
+ np.zeros((20,20)) # 20x20 black image iwrite(image, "black.png")
1299
1320
 
1300
1321
 
1301
1322
  .. note::
1302
1323
  - supports 8-bit greyscale and color images
1303
1324
  - supports uint16 for PNG, JPEG 2000, and TIFF formats
1304
1325
  - supports float32
1305
- - image must be in BGR or RGB format
1326
+ - supports alpha channel for PNG and TIFF formats
1327
+ - color image planes, with planes specified by ``colororder``, are reordered to
1328
+ OpenCV order: BGR or BGRA. For example, if ``colororder`` is "GRBA", the planes
1329
+ are reordered to "BGRA" for writing. If ``colororder`` is "BGR" or "BGRA", no reordering is done.
1306
1330
 
1307
- :seealso: :func:`iread` `cv2.imwrite <https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#gabbc7ef1aa2edfaa87772f1202d67e0ce>`_
1331
+ :seealso: :func:`iread` `cv2.imwrite
1332
+ <https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#gabbc7ef1aa2edfaa87772f1202d67e0ce>`_
1308
1333
  """
1309
- if im.ndim > 2 and colororder == "RGB":
1310
- # put image into OpenCV BGR order for writing
1311
- return cv2.imwrite(filename, im[:, :, ::-1], **kwargs)
1334
+ if im.ndim > 2:
1335
+ # color image, convert to OpenCV BGR order for writing
1336
+ if im.shape[2] not in (3, 4):
1337
+ raise ValueError("color image must have 3 or 4 planes")
1338
+ if colororder is not None:
1339
+ idx = []
1340
+ for c in "BGRA":
1341
+ j = colororder.find(c)
1342
+ if j >= 0:
1343
+ idx.append(j)
1344
+ im_bgra = im[:, :, idx] # reorder color planes → BGRA
1345
+ return cv2.imwrite(filename, im_bgra, **kwargs)
1312
1346
  else:
1347
+ # monochrome image
1313
1348
  return cv2.imwrite(filename, im, **kwargs)
1314
1349
 
1315
1350