machinevision-toolbox-python 0.9.7__py3-none-any.whl → 1.0.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 (27) hide show
  1. {machinevision_toolbox_python-0.9.7.dist-info → machinevision_toolbox_python-1.0.0.dist-info}/METADATA +19 -18
  2. machinevision_toolbox_python-1.0.0.dist-info/RECORD +44 -0
  3. {machinevision_toolbox_python-0.9.7.dist-info → machinevision_toolbox_python-1.0.0.dist-info}/WHEEL +1 -1
  4. machinevision_toolbox_python-1.0.0.dist-info/entry_points.txt +2 -0
  5. machinevisiontoolbox/Camera.py +50 -18
  6. machinevisiontoolbox/ImageBlobs.py +116 -11
  7. machinevisiontoolbox/ImageConstants.py +89 -13
  8. machinevisiontoolbox/ImageCore.py +479 -153
  9. machinevisiontoolbox/ImageIO.py +43 -7
  10. machinevisiontoolbox/ImageMorph.py +1 -1
  11. machinevisiontoolbox/ImageProcessing.py +8 -4
  12. machinevisiontoolbox/ImageRegionFeatures.py +181 -25
  13. machinevisiontoolbox/ImageReshape.py +1 -0
  14. machinevisiontoolbox/ImageSpatial.py +262 -119
  15. machinevisiontoolbox/ImageWholeFeatures.py +9 -6
  16. machinevisiontoolbox/PointCloud.py +10 -3
  17. machinevisiontoolbox/Sources.py +1 -1
  18. machinevisiontoolbox/__init__.py +3 -2
  19. machinevisiontoolbox/base/__init__.py +1 -0
  20. machinevisiontoolbox/base/data.py +29 -13
  21. machinevisiontoolbox/base/imageio.py +23 -22
  22. machinevisiontoolbox/blocks/__init__.py +4 -1
  23. machinevisiontoolbox/blocks/test_blocks.py +7 -1
  24. machinevisiontoolbox/mvtb_types.py +15 -0
  25. machinevision_toolbox_python-0.9.7.dist-info/RECORD +0 -42
  26. {machinevision_toolbox_python-0.9.7.dist-info → machinevision_toolbox_python-1.0.0.dist-info}/LICENSE +0 -0
  27. {machinevision_toolbox_python-0.9.7.dist-info → machinevision_toolbox_python-1.0.0.dist-info}/top_level.txt +0 -0
@@ -1,27 +1,27 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: machinevision-toolbox-python
3
- Version: 0.9.7
3
+ Version: 1.0.0
4
4
  Summary: Python tools for machine vision - education and research
5
5
  Author: Dorian Tsai
6
6
  Author-email: Peter Corke <rvc@petercorke.com>
7
7
  Project-URL: Homepage, https://github.com/petercorke/machinevision-toolbox-python
8
8
  Project-URL: Bug Tracker, https://github.com/pypa/sampleproject/issues
9
9
  Project-URL: Documentation, https://petercorke.github.io/machinevision-toolbox-python
10
- Project-URL: Source, https://github.com/petercorke/machinevision-toolbox-python
10
+ Project-URL: GitHub Source, https://github.com/petercorke/machinevision-toolbox-python
11
+ Project-URL: Changelog, https://github.com/petercorke/machinevision-toolbox-python/blob/master/CHANGELOG.md
11
12
  Keywords: machine vision,computer vision,multiview geometry,stereo vision,bundle adjustment,visual servoing,image features,color,blobs,morphology,image segmentation,opencv,open3d
12
13
  Classifier: Development Status :: 5 - Production/Stable
13
14
  Classifier: Intended Audience :: Developers
14
- Classifier: Programming Language :: Python :: 3.7
15
- Classifier: Programming Language :: Python :: 3.8
16
15
  Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: License :: OSI Approved :: MIT License
20
20
  Classifier: Operating System :: OS Independent
21
21
  Requires-Python: >=3.7
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
- Requires-Dist: numpy >=1.17.4
24
+ Requires-Dist: numpy<2.0.0,>=1.17.4
25
25
  Requires-Dist: scipy
26
26
  Requires-Dist: matplotlib
27
27
  Requires-Dist: opencv-python
@@ -32,17 +32,17 @@ Requires-Dist: pgraph-python
32
32
  Requires-Dist: ansitable
33
33
  Requires-Dist: mvtb-data
34
34
  Provides-Extra: dev
35
- Requires-Dist: pytest ; extra == 'dev'
36
- Requires-Dist: coverage ; extra == 'dev'
37
- Requires-Dist: flake8 ; extra == 'dev'
35
+ Requires-Dist: pytest; extra == "dev"
36
+ Requires-Dist: coverage; extra == "dev"
37
+ Requires-Dist: flake8; extra == "dev"
38
38
  Provides-Extra: docs
39
- Requires-Dist: sphinx ; extra == 'docs'
40
- Requires-Dist: recommonmark ; extra == 'docs'
41
- Requires-Dist: sphinx-rtd-theme ; extra == 'docs'
42
- Requires-Dist: sphinx-autorun ; extra == 'docs'
43
- Requires-Dist: sphinxcontrib-jsmath ; extra == 'docs'
44
- Requires-Dist: sphinx-markdown-tables ; extra == 'docs'
45
- Requires-Dist: sphinx-favicon ; extra == 'docs'
39
+ Requires-Dist: sphinx; extra == "docs"
40
+ Requires-Dist: recommonmark; extra == "docs"
41
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
42
+ Requires-Dist: sphinx-autorun; extra == "docs"
43
+ Requires-Dist: sphinxcontrib-jsmath; extra == "docs"
44
+ Requires-Dist: sphinx-markdown-tables; extra == "docs"
45
+ Requires-Dist: sphinx-favicon; extra == "docs"
46
46
 
47
47
  # Machine Vision Toolbox for Python
48
48
 
@@ -56,7 +56,7 @@ Requires-Dist: sphinx-favicon ; extra == 'docs'
56
56
  [![Powered by Open3D](https://raw.githubusercontent.com/petercorke/machinevision-toolbox-python/master/.github/svg/open3d_powered.svg)](https://open3d.org)
57
57
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
58
58
 
59
- [![Build Status](https://github.com/petercorke/machinevision-toolbox-python/workflows/Test-master/badge.svg?branch=master)](https://github.com/petercorke/machinevision-toolbox-python/actions?query=workflow%3Abuild)
59
+ [![Build Status](https://github.com/petercorke/machinevision-toolbox-python/actions/workflows/main.yml/badge.svg)](https://github.com/petercorke/machinevision-toolbox-python/actions?query=workflow%3Abuild)
60
60
  [![Coverage](https://codecov.io/gh/petercorke/machinevision-toolbox-python/branch/master/graph/badge.svg)](https://codecov.io/gh/petercorke/machinevision-toolbox-python)
61
61
  [![PyPI - Downloads](https://img.shields.io/pypi/dw/machinevision-toolbox-python)](https://pypistats.org/packages/machinevision-toolbox-python)
62
62
 
@@ -68,16 +68,17 @@ Requires-Dist: sphinx-favicon ; extra == 'docs'
68
68
  <img src="https://github.com/petercorke/machinevision-toolbox-python/raw/master/figs/VisionToolboxLogo_NoBackgnd@2x.png" width="200"></td>
69
69
  <td style="border:0px">
70
70
 
71
- A Python implementation of the <a href="https://github.com/petercorke/machinevision-toolbox-matlab">Machine Vision Toolbox for MATLAB<sup>&reg;</sup></a><ul>
72
-
73
71
  <li><a href="https://github.com/petercorke/machinevision-toolbox-python">GitHub repository </a></li>
74
72
  <li><a href="https://petercorke.github.io/machinevision-toolbox-python/">Documentation</a></li>
75
73
  <li><a href="https://github.com/petercorke/machinevision-toolbox-python/wiki">Examples and details</a></li>
76
74
  <li><a href="installation#">Installation</a></li>
75
+ <li><a href="https://github.com/petercorke/machinevision-toolbox-python/wiki">Examples and details</a></li>
76
+ <li><a href="https://github.com/petercorke/machinevision-toolbox-python/blob/master/CHANGELOG.md">Changelog</a></li>
77
77
  </ul>
78
78
  </td>
79
79
  </tr>
80
80
  </table>
81
+ <p style="font-size:80%">A Python implementation of the <a href="https://github.com/petercorke/machinevision-toolbox-matlab">Machine Vision Toolbox for MATLAB<sup>&reg;</sup></a></p>
81
82
 
82
83
  ## Synopsis
83
84
 
@@ -0,0 +1,44 @@
1
+ machinevisiontoolbox/BagOfWords.py,sha256=kHAt6P5Tz5iWpP9N-fcdL3KTwimBqxu9QQ1WRs04Lzc,19680
2
+ machinevisiontoolbox/BundleAdjust.py,sha256=syBFkXwJcRWpNr0k6gu-J3shzH0ozbgOgwaZp844fqk,36081
3
+ machinevisiontoolbox/Camera.py,sha256=ehwAJ9gZF3towz7Sh1pgGzhqXa8Ywws_XqdEf95-QWE,136514
4
+ machinevisiontoolbox/ImageBlobs.py,sha256=NMIbh9-coQW-B97u3IFN2gDra2FFOoIud5EtPrY-9D8,53283
5
+ machinevisiontoolbox/ImageColor.py,sha256=V4T_fs0EvFJegAQrNr-3pR7rg7Dye00g2V3JVEK4oOA,17111
6
+ machinevisiontoolbox/ImageConstants.py,sha256=K1xa_cTdMIJr-i2nwsNXhu-aJ9twPqLSxoylLnr7fMM,15844
7
+ machinevisiontoolbox/ImageCore.py,sha256=E3Zv00hjPNTSZ-oXrM_tUA88HD2NLEL_T3ler9sb1sk,94122
8
+ machinevisiontoolbox/ImageIO.py,sha256=jBlCPMJDOKpUGCjXYnXaEiNA_AWGDSnEbDwUhjmULPE,21840
9
+ machinevisiontoolbox/ImageLineFeatures.py,sha256=IlWSZOrvLnzvLx1vf4GuXb4Icpj214jz9FCDleguV9s,9104
10
+ machinevisiontoolbox/ImageMorph.py,sha256=1xn29MyEkrYRbw-Dg8X6qFKTf4_MDTQ5ddi7y8D-arw,23759
11
+ machinevisiontoolbox/ImageMultiview.py,sha256=wQWcq2Er-EzNNQ7_0mZRuID9VFh4rCjXPxZ-yMiQ-W4,14703
12
+ machinevisiontoolbox/ImagePointFeatures.py,sha256=zMDyRwJxlytgGIa25PMOTZk85v9LSX2zn7OpZC1Tsfg,76218
13
+ machinevisiontoolbox/ImageProcessing.py,sha256=MUTXer1ogtwiiksQiWxfJa2qCaQ0xFj131CO3ZgTa4A,38120
14
+ machinevisiontoolbox/ImageRegionFeatures.py,sha256=PAMhoYlbzDjnprElCQBv40Qk7zm9sXYAXVnuDG5iqmo,28134
15
+ machinevisiontoolbox/ImageReshape.py,sha256=6C7zyhDukrTauY7ckJyMwBb-gA_3rcQQREhOU8AXa70,45725
16
+ machinevisiontoolbox/ImageSpatial.py,sha256=2zHvNFx4MMOd_coQrZZJRI1WryFHx7ffc9ATu4UWDyc,69785
17
+ machinevisiontoolbox/ImageWholeFeatures.py,sha256=JnmNPKoZAKhUzRXI_JoDP-fw4ft-yXFOijgMFwIAQDo,27818
18
+ machinevisiontoolbox/PointCloud.py,sha256=yBrZ0xj0cnBowBiOubOgrU2CAcgs67JPhqP62wNHGeU,18645
19
+ machinevisiontoolbox/Sources.py,sha256=KYkbFYRV9ekOObTCiAXx34lCyZzUSUSNv4NHOkOXbgM,28621
20
+ machinevisiontoolbox/VisualServo.py,sha256=PlHCz_jbvV1JFfDrePQQ7cP94Wr5nliulHtYEMGwJtw,59294
21
+ machinevisiontoolbox/__init__.py,sha256=lQT3d6cxtO8c6pQqaA2MI14lq5sLVNOIh8g6Uj5AN1s,1265
22
+ machinevisiontoolbox/camera_derivatives.py,sha256=uChdvsXC-25Cg7-xH7tIbxe3tcxxJGQ_nslSiCvnL88,17557
23
+ machinevisiontoolbox/fiducial.py,sha256=syC7iIz95nYBTr_njo4ZzLXOyAdAIqoAZq_wRVhyUHQ,1374
24
+ machinevisiontoolbox/mvtb_types.py,sha256=66Oa8-J0UyBTtZQ0hH1B9qPqqw7i7dTCYe3CvgnGpXg,434
25
+ machinevisiontoolbox/newcameras.py,sha256=iQ8Fduiub4Clh1JIEVvQTXZVqXrmgzf0Y5iMPGTOW-w,1225
26
+ machinevisiontoolbox/base/__init__.py,sha256=qOSo4MRdhRAPoe8lVXG4-JAyaMi2f3FXSqNRYwagnwU,1470
27
+ machinevisiontoolbox/base/color.py,sha256=syipdtbxUrHXFl0288HLMpY31khdCYcGeDwCoMj-MMk,54593
28
+ machinevisiontoolbox/base/data.py,sha256=tDwUOHzpvGnkzlUaA3p5jQr1rlv9WIr6taRBv3gnmeU,5988
29
+ machinevisiontoolbox/base/esttheta.py,sha256=pYFguhXwo9FoaH0sRAXxrcEkECYttDPnHWBi1YMfISY,2323
30
+ machinevisiontoolbox/base/findpeaks.py,sha256=e0dcG6ZqHMwpzRFQrZH9-OITzhspi6Lq1TZoS37IPQY,10377
31
+ machinevisiontoolbox/base/graphics.py,sha256=WkqJPLKzZvSIJE5tFQ07Fa5dK8gBmJxGEvCwmxkVtrU,24663
32
+ machinevisiontoolbox/base/imageio.py,sha256=mWYqjTEC1sGLR0HL1fwWw0J6EzdeaWRvmnhY2gTAdXM,37418
33
+ machinevisiontoolbox/base/meshgrid.py,sha256=JYuZaw33ZJZQqqSxW8W7jLwr9iGzuXLUa3UKSlb51oQ,2350
34
+ machinevisiontoolbox/base/shapes.py,sha256=2FmjeuDnehNF5ZmssFDawmblDpKhuVUI3CmXCcILJxg,9926
35
+ machinevisiontoolbox/base/types.py,sha256=3znb81yTFb7sLrgjgt0P5q5wZa9_iRZGw_hWbINhCxc,4716
36
+ machinevisiontoolbox/blocks/__init__.py,sha256=EsdfK2WxYLSo0SA5HSmlgsivLT1kDnoH9v-RTBDnWwc,142
37
+ machinevisiontoolbox/blocks/camera.py,sha256=qWT_LoDFcZkCAlRqQkRDzX0fr4EvkEgdtGo0QPoO_8s,9776
38
+ machinevisiontoolbox/blocks/test_blocks.py,sha256=ZKsDG_nG9g1t_OIFR6vX823yJojEAi5mtqj9PxKoDrg,1701
39
+ machinevision_toolbox_python-1.0.0.dist-info/LICENSE,sha256=niCzY_CzbIMKuUZlUkGimjrKp0vkLAtcwFmoSuSKrdw,1068
40
+ machinevision_toolbox_python-1.0.0.dist-info/METADATA,sha256=gy0rBjlJXvGxYFDSJgLIijjcv_HAxfBpUi8Hh1RDdYs,19855
41
+ machinevision_toolbox_python-1.0.0.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
42
+ machinevision_toolbox_python-1.0.0.dist-info/entry_points.txt,sha256=2937LutzgheCcZNPCsgXkfYEZM4f4myyniln-rErgso,68
43
+ machinevision_toolbox_python-1.0.0.dist-info/top_level.txt,sha256=HC0K-k55L-H_l8QzNMmP1_vLgBkdTrsM0Kj_rjRrulo,21
44
+ machinevision_toolbox_python-1.0.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (75.7.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mvtbtool = machinevisiontoolbox.bin.mvtbtool:main
@@ -71,16 +71,16 @@ class CameraBase(ABC):
71
71
  :type camtype: str, optional
72
72
  :param rho: pixel size, defaults to 1
73
73
  :type rho: scalar or array_like(2), optional
74
- :param imagesize: image dimension in pixels, defaults to None
74
+ :param imagesize: image dimension (width, height) in pixels, defaults to None
75
75
  :type imagesize: int or array_like(2), optional
76
- :param sensorsize: image sensor size, defaults to None
76
+ :param sensorsize: image sensor size (width, height), defaults to None
77
77
  :type sensorsize: array_like(2), optional
78
78
  :param pp: principal point position, defaults to None
79
79
  :type pp: array_like(2), optional
80
80
  :param noise: standard deviation of image plane projection noise, defaults to None
81
81
  :type noise: float, optional
82
82
  :param pose: camera pose, defaults to None
83
- :type pose: :class:`~spatialmath..pose3d.SE3`, optional
83
+ :type pose: :class:`~spatialmath.pose3d.SE3`, optional
84
84
  :param limits: bounds of virtual image plane [umin, umax, vmin, vmax], defaults to None
85
85
  :type limits: array_like(4), optional
86
86
  :param labels: axis labels for virtual image plane, defaults to ``('u', 'v')``
@@ -118,8 +118,8 @@ class CameraBase(ABC):
118
118
  self.pp = pp
119
119
 
120
120
  if sensorsize is not None:
121
- self._rhou = sensorsize[0] / self.imagesize[1]
122
- self._rhov = sensorsize[1] / self.imagesize[0]
121
+ self._rhou = sensorsize[0] / self.imagesize[0]
122
+ self._rhov = sensorsize[1] / self.imagesize[1]
123
123
  else:
124
124
  rho = base.getvector(rho)
125
125
  if len(rho) == 1:
@@ -277,9 +277,8 @@ class CameraBase(ABC):
277
277
  """
278
278
  Set/get size of virtual image plane (base method)
279
279
 
280
- The dimensions of the virtual image plane is a 2-tuple, width and
281
- height, that can be read or written. For writing the size must be an
282
- iterable of length 2.
280
+ The dimensions of the virtual image plane is a 2-tuple (width, height) that can
281
+ be read or written. For writing the size must be an iterable of length 2.
283
282
 
284
283
  Example:
285
284
 
@@ -629,7 +628,7 @@ class CameraBase(ABC):
629
628
  >>> camera.project_point([0, 0, 2])
630
629
  >>> camera.project_point([0, 0, 2])
631
630
 
632
- :seealso: :meth:`project`
631
+ :seealso: :meth:`project_point`
633
632
  """
634
633
  return self._noise
635
634
 
@@ -637,6 +636,39 @@ class CameraBase(ABC):
637
636
  def noise(self, noise):
638
637
  self._noise = noise
639
638
 
639
+ @property
640
+ def distortion(self):
641
+ r"""
642
+ Set/Get distortion model (base method)
643
+
644
+ :return: distortion model
645
+ :rtype: array_like()
646
+
647
+ The distortion model is a vector of various lengths that can be read or written.
648
+ It is passed directly to OpenCV functions which define how it is used:
649
+
650
+ - 4-element vector, :math:`[k_1, k_2, p_1, p_2]`
651
+ - 5-element vector, :math:`[k_1, k_2, p_1, p_2, k_3]`
652
+ - 8-element vector, :math:`[k_1, k_2, p_1, p_2, k_3, k_4, k_5, k_6]`
653
+ - 12-element vector, :math:`[k_1, k_2, p_1, p_2, k_3, k_4, k_5, k_6, s_1, s_2, s_3, s_4]`,
654
+ - 14-element vector, :math:`[k_1, k_2, p_1, p_2, k_3, k_4, k_5, k_6, s_1, s_2, s_3, s_4, \tau_x, \tau_y]`
655
+
656
+ The elements :math:`k_i` are radial distortion coefficients, :math:`p_i` are
657
+ tangential distortion coefficients, :math:`s_i` are thin prism
658
+ distortion coefficients, and :math:`\tau_i` apply when the camera's image plane
659
+ is tilted with respect to the lens.
660
+
661
+ If the vector is ``None``, then zero distortion (all coefficients are zero) is
662
+ assumed.
663
+
664
+ :seealso: :meth:`~CentralCamera.project_point` `OpenCV distortion model <https://docs.opencv.org/4.x/d9/d0c/group__calib3d.html>`_
665
+ """
666
+ return self._distortion
667
+
668
+ @distortion.setter
669
+ def distortion(self, distortion):
670
+ self._distortion = distortion
671
+
640
672
  def move(self, T, name=None, relative=False):
641
673
  """
642
674
  Move camera (base method)
@@ -1690,7 +1722,7 @@ class CentralCamera(CameraBase):
1690
1722
  >>> line = camera.ray((100, 200))
1691
1723
  >>> line
1692
1724
 
1693
- :reference:
1725
+ :references:
1694
1726
  - "Multiview Geometry", Hartley & Zisserman, p.162
1695
1727
  - Robotics, Vision & Control for Python, Section 14.3, P. Corke, Springer 2023.
1696
1728
 
@@ -2196,7 +2228,7 @@ class CentralCamera(CameraBase):
2196
2228
  :note: Since only :math:`f s_x` and :math:`f s_y` can be estimated we
2197
2229
  set :math:`s_x = 1`.
2198
2230
 
2199
- :reference:
2231
+ :references:
2200
2232
  - Multiple View Geometry, Hartley&Zisserman, p 163-164
2201
2233
  - Robotics, Vision & Control for Python, Section 13.2.3, P. Corke,
2202
2234
  Springer 2023.
@@ -2355,7 +2387,7 @@ class CentralCamera(CameraBase):
2355
2387
  of inliers is also returned, True means the corresponding input
2356
2388
  point pair is an inlier.
2357
2389
 
2358
- :reference:
2390
+ :references:
2359
2391
  - Robotics, Vision & Control for Python, Section 14.2.4, P. Corke,
2360
2392
  Springer 2023.
2361
2393
 
@@ -2418,7 +2450,7 @@ class CentralCamera(CameraBase):
2418
2450
  >>> T.printline(orient="camera")
2419
2451
  >>> normals
2420
2452
 
2421
- :reference:
2453
+ :references:
2422
2454
  - Robotics, Vision & Control for Python, Section 14.2.4, P. Corke,
2423
2455
  Springer 2023.
2424
2456
 
@@ -2504,7 +2536,7 @@ class CentralCamera(CameraBase):
2504
2536
  >>> F = camera1.F(SE3.Tx(0.2))
2505
2537
  >>> F
2506
2538
 
2507
- :reference:
2539
+ :references:
2508
2540
  - Y.Ma, J.Kosecka, S.Soatto, S.Sastry, "An invitation to 3D",
2509
2541
  Springer, 2003. p.177
2510
2542
  - Robotics, Vision & Control for Python, Section 14.2.1, P. Corke,
@@ -2656,7 +2688,7 @@ class CentralCamera(CameraBase):
2656
2688
  * a relative motion represented by a :class:`~spatialmath..pose3d.SE3`
2657
2689
  * a fundamental matrix
2658
2690
 
2659
- :reference:
2691
+ :references:
2660
2692
  - Y.Ma, J.Kosecka, S.Soatto, S.Sastry, "An invitation to 3D",
2661
2693
  Springer, 2003. p.177
2662
2694
 
@@ -2758,7 +2790,7 @@ class CentralCamera(CameraBase):
2758
2790
  - a single 3D world point in front of the camera
2759
2791
  - a :class:`~machinevisiontoolbox.ImagePointFeatures.FeatureMatch` object
2760
2792
 
2761
- :reference:
2793
+ :references:
2762
2794
  - Robotics, Vision & Control for Python, Section 14.2.2 P. Corke,
2763
2795
  Springer 2023.
2764
2796
 
@@ -3494,9 +3526,9 @@ class CatadioptricCamera(CameraBase):
3494
3526
 
3495
3527
  :note:
3496
3528
  - If ``K`` is not specified it is computed such that the circular
3497
- imaging region maximally fills the image plane.
3529
+ imaging region maximally fills the image plane.
3498
3530
  - This camera model assumes central projection, that is, the focal point
3499
- is at :math:`z=0` and the image plane is at :math:`z=f`. The image is not inverted.
3531
+ is at :math:`z=0` and the image plane is at :math:`z=f`. The image is not inverted.
3500
3532
 
3501
3533
  :references:
3502
3534
  - Robotics, Vision & Control for Python, Section 13.3.2, P. Corke, Springer 2023.
@@ -25,6 +25,7 @@ import random as rng
25
25
  rng.seed(13543) # would this be called every time at Blobs init?
26
26
  import matplotlib.pyplot as plt
27
27
 
28
+
28
29
  # decorators
29
30
  def scalar_result(func):
30
31
  def innerfunc(*args):
@@ -99,32 +100,58 @@ class Blob:
99
100
  level = None
100
101
 
101
102
  def __init__(self):
103
+ """Constructor for Blob class
104
+
105
+ A :class:`Blob` instance is a simple container for the parameters
106
+ of a single blob.
107
+
108
+ A set of blobs is represented by a :class:`Blobs` instance which acts
109
+ like a list of :class:`Blob` instance.
110
+ """
102
111
  return
103
112
 
104
113
  def __str__(self):
105
- l = [f"{key}: {value}" for key, value in self.__dict__.items()]
106
- return "\n".join(l)
114
+ """Create a compact string representation of the Blob object
115
+
116
+ :return: compact string representation
117
+ :rtype: str
118
+ """
119
+ return f"Blob[{self.id}](area={self.moments.m00:.2g}, color={self.color}, parent={self.parent.id if self.parent else None})]"
107
120
 
108
121
  def __repr__(self):
109
122
  return str(self)
110
123
 
124
+ def print(self):
125
+ """Create a detailed string representation of the Blob object
126
+
127
+ :return: detailed string representation
128
+ :rtype: str
129
+
130
+ The string representation includes all the attributes of the Blob object.
131
+ """
132
+ l = [f"{key}: {value}" for key, value in self.__dict__.items()]
133
+ return "\n".join(l)
134
+
111
135
 
112
136
  class Blobs(UserList): # lgtm[py/missing-equals]
113
137
 
114
138
  _image = [] # keep image saved for each Blobs object
115
139
 
116
- def __init__(self, image=None, **kwargs):
140
+ def __init__(self, image=None, kulpa=True, **kwargs):
117
141
  """
118
142
  Find blobs and compute their attributes
119
143
 
120
144
  :param image: image to use, defaults to None
121
145
  :type image: :class:`Image`, optional
146
+ :param kulpa: apply Kulpa's correction factor to circularity, defaults to True
147
+ :type kulpa: bool, optional
122
148
 
123
149
  Uses OpenCV functions ``findContours`` to find a hierarchy of regions
124
150
  represented by their contours, and ``boundingRect``, ``moments`` to
125
151
  compute moments, perimeters, centroids etc.
126
152
 
127
- This class behaves like a list and each blob is an element of the list
153
+ This class behaves like a list and each element of the list is a blob
154
+ represented by a :class:`Blob` instance
128
155
 
129
156
  .. runblock:: pycon
130
157
 
@@ -154,8 +181,56 @@ class Blobs(UserList): # lgtm[py/missing-equals]
154
181
 
155
182
  >>> blobs.area
156
183
 
184
+ A blob has many attributes:
185
+
186
+ .. list-table::
187
+ :header-rows: 1
188
+
189
+ * - Attribute
190
+ - Description
191
+ * - :meth:`area`
192
+ - The area of the blob.
193
+ * - :meth:`u`, :meth:`v`
194
+ - The centroid (center of mass) of the blob.
195
+ * - :meth:`bbox`
196
+ - The bounding box of the blob.
197
+ * - :meth:`color`
198
+ - The vaue of pixels within the blob.
199
+ * - :meth:`touch`
200
+ - True if the blob touches the border.
201
+ * - :meth:`contour_point`
202
+ - A point on the contour of the blob.
203
+ * - :meth:`perimeter`
204
+ - A 2xN array of points on the perimeter of the blob.
205
+ * - :meth:`perimeter_length`
206
+ - The perimeter length of the blob.
207
+ * - :meth:`circularity`
208
+ - The circularity of the blob.
209
+ * - :meth:`moments`
210
+ - The moments of the blob including central, normalized upto 3rd order.
211
+ * - :meth:`orientation`
212
+ - Orientation of the equivalent ellipse.
213
+ * - :meth:`a, b`
214
+ - The equivalent ellipse radii.
215
+ * - :meth:`children`
216
+ - A list of references to child :class:`Blob` instances.
217
+ * - :meth:`parent`
218
+ - A reference to the parent :class:`Blob` instance, or None if no parent.
219
+ * - :meth:`level`
220
+ - The depth of the blob in the region tree.
221
+
157
222
  :note: A color image is internally converted to greyscale.
158
223
 
224
+ :note: ``findContours`` can give surprising results for small images:
225
+
226
+ - The perimeter length is computed between the mid points of the pixels, and
227
+ the OpenCV function ``arcLength`` seems to underestimate the perimeter
228
+ even more. The perimeter length is not the same as the number of pixels
229
+ in the contour.
230
+ - The area will be less than the number of pixels in the blob, because the
231
+ area is computed from the moments of the blob, which are computed from the
232
+ contour, see above.
233
+
159
234
  :references:
160
235
  - Robotics, Vision & Control for Python, Section 12.1.2.1, P. Corke, Springer 2023.
161
236
 
@@ -197,6 +272,7 @@ class Blobs(UserList): # lgtm[py/missing-equals]
197
272
 
198
273
  ## first pass: moments, children, bbox
199
274
 
275
+ runts = 0
200
276
  for i, (contour, hier) in enumerate(zip(contours, hierarchy)):
201
277
 
202
278
  blob = Blob()
@@ -204,8 +280,8 @@ class Blobs(UserList): # lgtm[py/missing-equals]
204
280
 
205
281
  ## bounding box: umin, vmin, width, height
206
282
  u1, v1, w, h = cv.boundingRect(contour)
207
- u2 = u1 + w
208
- v2 = v1 + h
283
+ u2 = u1 + w - 1
284
+ v2 = v1 + h - 1
209
285
  blob.bbox = np.r_[u1, u2, v1, v2]
210
286
 
211
287
  blob.touch = u1 == 0 or v1 == 0 or u2 == image.umax or v2 == image.vmax
@@ -225,6 +301,9 @@ class Blobs(UserList): # lgtm[py/missing-equals]
225
301
  child = hierarchy[child, 0]
226
302
  blob.children = children
227
303
 
304
+ pp = contour[0, :]
305
+ blob.color = image.A[pp[1], pp[0]]
306
+
228
307
  ## moments
229
308
 
230
309
  # get moments as a dictionary for each contour
@@ -239,7 +318,9 @@ class Blobs(UserList): # lgtm[py/missing-equals]
239
318
 
240
319
  ## For a single set pixel OpenCV returns all moments as zero, skip such blobs
241
320
  ## TODO handle this situation by setting m00=1, m10=x, m01=y etc.
242
- if blob.moments["m00"] != 0:
321
+ if blob.moments["m00"] == 0:
322
+ runts += 1
323
+ else:
243
324
  self.data.append(blob)
244
325
 
245
326
  ## second pass: equivalent ellipse
@@ -289,9 +370,14 @@ class Blobs(UserList): # lgtm[py/missing-equals]
289
370
  # Comparison. Proc. IAPR Workshop on Machine Vision Applications.,
290
371
  # December 13-15, 1994, Kawasaki, Japan
291
372
  # L. Yang, F. Albregtsen, T. Loennestad, P. Groettum
292
- kulpa = np.pi / 8.0 * (1.0 + np.sqrt(2.0))
373
+ if kulpa is True:
374
+ kfactor = np.pi / 8.0 * (1.0 + np.sqrt(2.0))
375
+ elif isinstance(kulpa, (int, float)):
376
+ kfactor = kulpa
377
+ else:
378
+ kfactor = 1.0
293
379
  blob.circularity = (4.0 * np.pi * M.m00) / (
294
- blob.perimeter_length * kulpa
380
+ blob.perimeter_length * kfactor
295
381
  ) ** 2
296
382
 
297
383
  ## third pass, region tree coloring to determine vertex depth
@@ -306,6 +392,17 @@ class Blobs(UserList): # lgtm[py/missing-equals]
306
392
 
307
393
  self.filter(**kwargs)
308
394
 
395
+ for blob in self.data:
396
+ if blob.parent != -1:
397
+ blob.parent = self.data[blob.parent]
398
+ else:
399
+ blob.parent = None
400
+ if len(blob.children) > 0:
401
+ blob.children = [self.data[i] for i in blob.children]
402
+
403
+ if runts > 0:
404
+ print(f"blobs: found {runts} runt blob{'s' if runts > 1 else ''}")
405
+
309
406
  return
310
407
 
311
408
  def filter(self, area=None, circularity=None, color=None, touch=None, aspect=None):
@@ -347,10 +444,15 @@ class Blobs(UserList): # lgtm[py/missing-equals]
347
444
 
348
445
  >>> from machinevisiontoolbox import Image
349
446
  >>> img = Image.Read('sharks.png')
447
+ >>> blobs = img.blobs()
350
448
  >>> blobs
351
449
  >>> blobs.filter(area=10_000)
352
450
  >>> blobs.filter(area=10_000, circularity=0.3)
353
451
 
452
+ .. warning:: Filtering can destroy the hierarchy of the blobs, deleting
453
+ parents and children in the blob tree. A blob may have references
454
+ to parents and children that are not in the filtered set.
455
+
354
456
  :references:
355
457
  - Robotics, Vision & Control for Python, Section 12.1.2.1, P. Corke, Springer 2023.
356
458
 
@@ -493,7 +595,7 @@ class Blobs(UserList): # lgtm[py/missing-equals]
493
595
  for b in self.data:
494
596
  table.row(
495
597
  b.id,
496
- b.parent,
598
+ b.parent.id if b.parent else -1,
497
599
  f"{b.uc:.1f}, {b.vc:.1f}",
498
600
  b.moments.m00,
499
601
  b.touch,
@@ -1490,7 +1592,10 @@ class Blobs(UserList): # lgtm[py/missing-equals]
1490
1592
  # add the nodes including name and position
1491
1593
  for id, blob in enumerate(self):
1492
1594
  print(' "{:d}"'.format(id), file=f)
1493
- print(' "{:d}" -> "{:d}"'.format(blob.parent, id), file=f)
1595
+ print(
1596
+ ' "{:d}" -> "{:d}"'.format(blob.parent.id if blob.parent else -1, id),
1597
+ file=f,
1598
+ )
1494
1599
 
1495
1600
  print("}", file=f)
1496
1601