Source code for machinevisiontoolbox.ImageMorph

#!/usr/bin/env python

import numpy as np
import cv2 as cv
import time
import scipy as sp


[docs]class ImageMorphMixin: """ Image processing morphological operations on the Image class """ def _getse(self, se): """ Get structuring element :param se: structuring element :type se: array (N,H) :return se: structuring element :rtype: Image instance (N,H) as uint8 - ``IM.getse(se)`` converts matrix ``se`` into a uint8 numpy array for opencv, which only accepts kernels of type CV_8U """ se = np.array(se).astype(np.uint8) if se.min() < 0: raise ValueError('cannot convert array with negative values to a structuring element') return se
[docs] def erode(self, se, n=1, border='replicate', bordervalue=0, **kwargs): """ Morphological erosion :param se: structuring element :type se: ndarray(N,M) :param n: number of times to apply the erosion, defaults to 1 :type n: int, optional :param border: option for boundary handling, see :meth:`~machinevisiontoolbox.ImageSpatial.convolve`, defaults to 'replicate' :type border: str, optional :param bordervalue: padding value, defaults to 0 :type bordervalue: scalar, optional :param kwargs: addition options passed to :func:`opencv.erode` :return: eroded image :rtype: :class:`Image` Returns the image after morphological erosion with the structuring element ``se`` applied ``n`` times. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> import numpy as np >>> img = Image.Squares(1,7) >>> img.print() >>> img.erode(np.ones((3,3))).print() .. note:: - It is cheaper to apply a smaller structuring element multiple times than one large one, the effective structuing element is the Minkowski sum of the structuring element with itself N times. - The structuring element typically has odd side lengths. - For a greyscale image this is the maximum value over the structuring element. :references: - Robotics, Vision & Control for Python, Section 11.6, P. Corke, Springer 2023. :seealso: :meth:`dilate` `opencv.erode <https://docs.opencv.org/3.4/d4/d86/group__imgproc__filter.html#gaeb1e0c1033e3f6b891a25d0511362aeb>`_ """ # check if valid input: se = self._getse(se) # TODO check if se is valid (odd number and less than im.shape) # consider cv.getStructuringElement? # eg, se = cv.getStructuringElement(cv.MORPH_RECT, (3,3)) if not isinstance(n, int): n = int(n) if n <= 0: raise ValueError(n, 'n must be greater than 0') out = cv.erode(self.to_int(), se, iterations=n, borderType=self._bordertype_cv(border, exclude=('wrap')), borderValue=bordervalue, **kwargs) return self.__class__(out)
[docs] def dilate(self, se, n=1, border='replicate', bordervalue=0, **kwargs): """ Morphological dilation :param se: structuring element :type se: ndarray(N,M) :param n: number of times to apply the dilation, defaults to 1 :type n: int, optional :param border: option for boundary handling, see :meth:`~machinevisiontoolbox.ImageSpatial.convolve`, defaults to 'replicate' :type border: str, optional :param bordervalue: padding value, defaults to 0 :type bordervalue: scalar, optional :param kwargs: addition options passed to :func:`opencv.dilate` :return: dilated image :rtype: :class:`Image` Returns the image after morphological dilation with the structuring element ``se`` applied ``n`` times. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> import numpy as np >>> pixels = np.zeros((7,7)); pixels[3,3] = 1 >>> img = Image(pixels) >>> img.print() >>> img.dilate(np.ones((3,3))).print() .. note:: - It is cheaper to apply a smaller structuring element multiple times than one large one, the effective structuing element is the Minkowski sum of the structuring element with itself N times. - The structuring element typically has odd side lengths. - For a greyscale image this is the minimum value over the structuring element. :references: - Robotics, Vision & Control for Python, Section 11.6, P. Corke, Springer 2023. :seealso: :meth:`erode` `opencv.dilate <https://docs.opencv.org/3.4/d4/d86/group__imgproc__filter.html#ga4ff0f3318642c4f469d0e11f242f3b6c>`_ """ # check if valid input: se = self._getse(se) if not isinstance(n, int): n = int(n) if n <= 0: raise ValueError(n, 'n must be greater than 0') # for im in [img.image in self]: # then can use cv.dilate(im) out = cv.dilate(self.to_int(), se, iterations=n, borderType=self._bordertype_cv(border, exclude=('wrap')), borderValue=bordervalue, **kwargs) return self.__class__(out)
[docs] def morph(self, se, op, n=1, border='replicate', bordervalue=0, **kwargs): """ Morphological neighbourhood processing :param se: structuring element :type se: ndarray(N,M) :param op: morphological operation, one of: 'min', 'max', 'diff' :type op: str :param n: number of times to apply the operation, defaults to 1 :type n: int, optional :param border: option for boundary handling, see :meth:`~machinevisiontoolbox.ImageSpatial.ve`, defaults to 'replicate' :type border: str, optional :param bordervalue: padding value, defaults to 0 :type bordervalue: scalar, optional :param kwargs: addition options passed to ``opencv.morphologyEx`` :return: morphologically transformed image :rtype: :class:`Image` Apply the morphological operation ``oper`` with structuring element ``se`` to the image ``n`` times. ============= ======================================================= ``'oper'`` description ============= ======================================================= ``'min'`` minimum value over the structuring element ``'max'`` maximum value over the structuring element ``'diff'`` maximum - minimum value over the structuring element ============= ======================================================= .. note:: - It is cheaper to apply a smaller structuring element multiple times than one large one, the effective structuing element is the Minkowski sum of the structuring element with itself N times. - Performs greyscale morphology - The structuring element should have an odd side length. - For a binary image, min = erosion, max = dilation. :references: - Robotics, Vision & Control for Python, Section 11.6, P. Corke, Springer 2023. :seealso: :meth:`erode` :meth:`dilate` `opencv.morphologyEx <https://docs.opencv.org/3.4/d4/d86/group__imgproc__filter.html#ga67493776e3ad1a3df63883829375201f>`_ """ # check if valid input: # se = cv.getStructuringElement(cv.MORPH_RECT, (3,3)) se = self._getse(se) # TODO check if se is valid (odd number and less than im.shape), # can also be a scalar if not isinstance(op, str): raise TypeError(op, 'oper must be a string') if not isinstance(n, int): n = int(n) if n <= 0: raise ValueError(n, 'n must be greater than 0') if self.isbool: image = self.to_int() else: image = self.A if op == 'min': out = cv.morphologyEx(image, cv.MORPH_ERODE, se, iterations=n, borderType=self._bordertype_cv(border), borderValue=bordervalue, **kwargs) elif op == 'max': out = cv.morphologyEx(self.A, cv.MORPH_DILATE, se, iterations=n, borderType=self._bordertype_cv(border), borderValue=bordervalue, **kwargs) elif op == 'diff': se = self.getse(se) out = cv.morphologyEx(self.A, cv.MORPH_GRADIENT, se, iterations=n, borderType=self._bordertype_cv(border), borderValue=bordervalue, **kwargs) else: raise ValueError('morph does not support oper') if self.isbool: out = out.astype(bool) return self.__class__(out)
[docs] def open(self, se, n=1, border='replicate', bordervalue=0, **kwargs): """ Morphological opening :param se: structuring element :type se: ndarray(N,M) :param n: number of times to apply the erosion then dilation, defauts to 1 :type n: int, optional :param border: option for boundary handling, see :meth:`~machinevisiontoolbox.ImageSpatial.convolve`, defaults to 'replicate' :type border: str, optional :param bordervalue: padding value, defaults to 0 :type bordervalue: scalar, optional :param kwargs: addition options passed to ``opencv.morphologyEx`` :return: dilated image :rtype: :class:`Image` Returns the image after morphological opening with the structuring element ``se`` applied as ``n`` erosions followed by ``n`` dilations. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> import numpy as np >>> img = Image.Read("eg-morph1.png") >>> img.print('{:1d}') >>> img.open(np.ones((5,5))).print('{:1d}') .. note:: - For binary image an opening operation can be used to eliminate small white noise regions. - It is cheaper to apply a smaller structuring element multiple times than one large one, the effective structuing element is the Minkowski sum of the structuring element with itself N times. - The structuring element typically has odd side lengths. :references: - Robotics, Vision & Control for Python, Section 11.6, P. Corke, Springer 2023. :seealso: :meth:`close :meth:`morph` `opencv.morphologyEx <https://docs.opencv.org/3.4/d4/d86/group__imgproc__filter.html#ga67493776e3ad1a3df63883829375201f>`_ """ # probably cleanest approach: # out = [self.erode(se, **kwargs).dilate(se, **kwargs) for im in self] # return self.__class__(out) out = cv.morphologyEx(self.to_int(), cv.MORPH_OPEN, se, iterations=n, borderType=self._bordertype_cv(border), borderValue=bordervalue, **kwargs) return self.__class__(out)
[docs] def close(self, se, n=1, border='replicate', bordervalue=0, **kwargs): """ Morphological closing :param se: structuring element :type se: ndarray(N,M) :param n: number of times to apply the dilation then erosion, defauts to 1 :type n: int, optional :param border: option for boundary handling, see :meth:`~machinevisiontoolbox.ImageSpatial.convolve`, defaults to 'replicate' :type border: str, optional :param bordervalue: padding value, defaults to 0 :type bordervalue: scalar, optional :param kwargs: addition options passed to ``opencv.morphologyEx`` :return: dilated image :rtype: :class:`Image` Returns the image after morphological opening with the structuring element ``se`` applied as ``n`` dilations followed by ``n`` erosions. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> import numpy as np >>> img = Image.Read("eg-morph2.png") >>> img.print('{:1d}') >>> img.close(np.ones((5,5))).print('{:1d}') .. note:: - For binary image a closing operation can be used to eliminate joins between regions. - It is cheaper to apply a smaller structuring element multiple times than one large one, the effective structuing element is the Minkowski sum of the structuring element with itself N times. - The structuring element typically has odd side lengths. :references: - Robotics, Vision & Control for Python, Section 11.6, P. Corke, Springer 2023. :seealso: :meth:`open` :meth:`morph` `opencv.morphologyEx <https://docs.opencv.org/3.4/d4/d86/group__imgproc__filter.html#ga67493776e3ad1a3df63883829375201f>`_ """ out = cv.morphologyEx(self.to_int(), cv.MORPH_CLOSE, se, iterations=n, borderType=self._bordertype_cv(border), borderValue=bordervalue, **kwargs) return self.__class__(out)
[docs] def hitormiss(self, s1, s2=None, border='replicate', bordervalue=0, **kwargs): r""" Hit or miss transform :param s1: structuring element 1 :type s1: ndarray(N,M) :param s2: structuring element 2 :type s2: ndarray(N,M) :param kwargs: arguments passed to ``opencv.morphologyEx`` :return: transformed image :rtype: :class:`Image` Return the hit-or-miss transform of the binary image which is defined by two structuring elements structuring elements .. math:: Y = (X \ominus S_1) \cap (X \ominus S_2) which is the logical-and of the binary image and its complement, eroded by two different structuring elements. This preserves pixels where ones in the window are consistent with :math:`S_1` and zeros in the window are consistent with :math:`S_2`. If only ``s1`` is provided it has three possible values: * 1, must match a non-zero value * -1, must match a zero value * 0, don't care, matches any value. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> import numpy as np >>> pixels = np.array([[0,0,1,0,1,1],[1,1,1,1,0,1],[0,1,0,1,1,0],[1,1,1,1,0,0],[0,1,1,0,1,0]]) >>> img = Image(pixels) >>> img.print() >>> se = np.array([[0,1,0],[1,-1,1],[0,1,0]]) >>> se >>> img.hitormiss(se).print() .. note:: For the single argument case ``s1`` :math:`=S_1 - S_2`. :references: - Robotics, Vision & Control for Python, Section 11.6.3, P. Corke, Springer 2023. :seealso: :meth:`thin` :meth:`endpoint` :meth:`triplepoint` `opencv.morphologyEx <https://docs.opencv.org/3.4/d4/d86/group__imgproc__filter.html#ga67493776e3ad1a3df63883829375201f>`_ """ # check valid input # TODO also check if binary image? if s2 is not None: s1 = s1 - s2 out = cv.morphologyEx(self.A, cv.MORPH_HITMISS, s1) return self.__class__(out)
[docs] def thin(self, **kwargs): """ Morphological skeletonization :param kwargs: options passed to :meth:`hitormiss` :return: Image :rtype: :class:`Image` instance Return the thinned version (skeleton) of the binary image as another binary image. Any non-zero region is replaced by a network of single-pixel wide lines. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.String('000000|011110|011110|000000') >>> img.print() >>> img.thin().print() >>> img = Image.Read("shark2.png") >>> skeleton = img.thin() :references: - Robotics, Vision & Control for Python, Section 11.6.3, P. Corke, Springer 2023. :seealso: :meth:`thin_animate` :meth:`hitormiss` :meth:`endpoint` :meth:`triplepoint` """ # create a binary image (True/False) # im = im > 0 # create structuring elements sa = np.array([[-1, -1, -1], [0, 1, 0], [1, 1, 1]]) sb = np.array([[0, -1, -1], [1, 1, -1], [0, 1, 0]]) im = self.to('uint8') o = im while True: for i in range(4): r = im.hitormiss(sa) # might also use the bitwise operator ^ im -= r r = im.hitormiss(sb) im -= r sa = np.rot90(sa) sb = np.rot90(sb) if np.all(o.A == im.A): break o = im return self.__class__(o)
[docs] def thin_animate(self, delay=0.5, **kwargs): """ Morphological skeletonization with animation :param delay: time in seconds between each iteration of display, default to 0.5 :type delay: float, optional :param kwargs: options passed to :meth:`hitormiss` :return: Image :rtype: :class:`Image` instance Return the thinned version (skeleton) of the binary image as another binary image. Any non-zero region is replaced by a network of single-pixel wide lines. The algorithm is iterative, and the result of of each iteration is displayed using Matplotlib. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.Read("shark2.png") >>> img.thin_animate() :references: - Robotics, Vision & Control for Python, Section 11.6.3, P. Corke, Springer 2023. :seealso: :meth:`thin` :meth:`hitormiss` :meth:`endpoint` :meth:`triplepoint` """ # create a binary image (True/False) # im = im > 0 # create structuring elements sa = np.array([[-1, -1, -1], [0, 1, 0], [1, 1, 1]]) sb = np.array([[0, -1, -1], [1, 1, -1], [0, 1, 0]]) im = self.to('uint8') o = im h = im.disp() while True: for i in range(4): r = im.hitormiss(sa) # might also use the bitwise operator ^ im -= r r = im.hitormiss(sb) im -= r sa = np.rot90(sa) sb = np.rot90(sb) if delay > 0: h.set_data(im.A) time.sleep(delay) if np.all(o.A == im.A): break o = im return self.__class__(o)
[docs] def endpoint(self, **kwargs): """ Find end points on a binary skeleton image :param kwargs: options passed to :meth:`hitormiss` :return: Image :rtype: :class:`Image` instance Return a binary image where pixels are True if the corresponding pixel in the binary image is the end point of a single-pixel wide line such as found in an image skeleton. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.String('000000|011110|000000') >>> img.print() >>> img.endpoint().print() .. note:: Computed using the hit-or-miss morphological operator. :references: - Robotics, Vision & Control for Python, Section 11.6.3, P. Corke, Springer 2023. :seealso: :meth:`hitormiss` :meth:`thin` :meth:`triplepoint` """ se = np.zeros((3, 3, 8)) se[:, :, 0] = np.array([[-1, 1, -1], [-1, 1, -1], [-1, -1, -1]]) se[:, :, 1] = np.array([[-1, -1, 1], [-1, 1, -1], [-1, -1, -1]]) se[:, :, 2] = np.array([[-1, -1, -1], [-1, 1, 1], [-1, -1, -1]]) se[:, :, 3] = np.array([[-1, -1, -1], [-1, 1, -1], [-1, -1, 1]]) se[:, :, 4] = np.array([[-1, -1, -1], [-1, 1, -1], [-1, 1, -1]]) se[:, :, 5] = np.array([[-1, -1, -1], [-1, 1, -1], [1, -1, -1]]) se[:, :, 6] = np.array([[-1, -1, -1], [1, 1, -1], [-1, -1, -1]]) se[:, :, 7] = np.array([[1, -1, -1], [-1, 1, -1], [-1, -1, -1]]) out = np.zeros(self.shape) for i in range(se.shape[2]): out = np.logical_or(out, self.hitormiss(se[:, :, i]).A) return self.__class__(out)
[docs] def triplepoint(self, **kwargs): """ Find triple points :param kwargs: options passed to :meth:`hitormiss` :return: Image :rtype: :class:`Image` instance Return a binary image where pixels are True if the corresponding pixel in the binary image is a triple point, that is where three single-pixel wide line intersect. These are the Voronoi points in an image skeleton. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Image >>> img = Image.String('000000|011110|001000|001000|000000') >>> img.print() >>> img.triplepoint().print() .. note:: Computed using the hit-or-miss morphological operator. :references: - Robotics, Vision & Control for Python, Section 11.6.3, P. Corke, Springer 2023. :seealso: :meth:`hitormiss` :meth:`thin` :meth:`endpoint` """ se = np.zeros((3, 3, 16), dtype='int8') se[:, :, 0] = np.array([[-1, 1, -1], [1, 1, 1], [-1, -1, -1]]) se[:, :, 1] = np.array([[1, -1, 1], [-1, 1, -1], [-1, -1, 1]]) se[:, :, 2] = np.array([[-1, 1, -1], [-1, 1, 1], [-1, 1, -1]]) se[:, :, 3] = np.array([[-1, -1, 1], [-1, 1, -1], [1, -1, 1]]) se[:, :, 4] = np.array([[-1, -1, -1], [1, 1, 1], [-1, 1, -1]]) se[:, :, 5] = np.array([[1, -1, -1], [-1, 1, -1], [1, -1, 1]]) se[:, :, 6] = np.array([[-1, 1, -1], [1, 1, -1], [-1, 1, -1]]) se[:, :, 7] = np.array([[1, -1, 1], [-1, 1, -1], [1, -1, -1]]) se[:, :, 8] = np.array([[-1, 1, -1], [-1, 1, 1], [1, -1, -1]]) se[:, :, 9] = np.array([[-1, -1, 1], [1, 1, -1], [-1, -1, 1]]) se[:, :, 10] = np.array([[1, -1, -1], [-1, 1, 1], [-1, 1, -1]]) se[:, :, 11] = np.array([[-1, 1, -1], [-1, 1, -1], [1, -1, 1]]) se[:, :, 12] = np.array([[-1, -1, 1], [1, 1, -1], [-1, 1, -1]]) se[:, :, 13] = np.array([[1, -1, -1], [-1, 1, 1], [1, -1, -1]]) se[:, :, 14] = np.array([[-1, 1, -1], [1, 1, -1], [-1, -1, 1]]) se[:, :, 15] = np.array([[1, -1, 1], [-1, 1, -1], [-1, 1, -1]]) out = np.zeros(self.shape, self.dtype) for i in range(se.shape[2]): out = np.bitwise_or(out, self.hitormiss(se[:, :, i]).A) return self.__class__(out)
# --------------------------------------------------------------------------# if __name__ == '__main__': img = Image.Read("shark2.png") img.thin_animate() # test run ImageProcessingColor.py print('ImageProcessingMorph.py')