Source code for machinevisiontoolbox.Kernel

"""
Convolution kernel classes for image processing operations.
"""

import matplotlib.pyplot as plt
import numpy as np
import spatialmath.base.argcheck as argcheck
from matplotlib import cm

from machinevisiontoolbox.mvtb_types import ArrayLike, Dtype


[docs] class Kernel:
[docs] def __init__(self, K: np.ndarray, name: str | None = None) -> None: """ Convolution kernel object :param K: kernel weighting matrix :type K: ndarray(N,M) :param name: name of the kernel, defaults to None :type name: str, optional :raises ValueError: ``K`` is not a 2D ndarray Kernel objects are used to represent convolution kernels for image processing operations. They are created by a number of class methods that generate common kernels such as Gaussian, Laplacian, etc. :class:`ImageCore.Image` :class:`machinevisiontoolbox.ImageCore.Image` :class:`machinevisiontoolbox.Image` :seealso: :meth:`Gauss` :meth:`Laplace` :meth:`Sobel` :meth:`DoG` :meth:`LoG` :meth:`DGauss` :meth:`Circle` :meth:`Box` """ if not isinstance(K, np.ndarray) and K.ndim != 2: raise ValueError("kernel must be a 2D ndarray") self.K = K self.name = name
def __str__(self) -> str: """Human readable kernel description :return: summary description of the kernel :rtype: str The summary includes the size of the kernel, and its minimum, maximum and mean values . If the kernel is symmetric this is noted. .. runblock:: pycon >>> from machinevisiontoolbox import Kernel >>> K = Kernel.Gauss(sigma=2) >>> print(K) """ s = f"Kernel: {self.K.shape[0]}x{self.K.shape[1]}" s += f", min={self.K.min():.2g}, max={self.K.max():.2g}, mean={self.K.mean():.2g}" if np.allclose(self.K, self.K.T, rtol=1e-05, atol=1e-08): s += ", SYMMETRIC" if self.name is not None: s += f" ({self.name})" return s def __repr__(self) -> str: """Compact representation of the kernel :return: compact representation of the kernel :rtype: str The representation includes the size of the kernel, and its minimum, maximum and mean values. .. runblock:: pycon >>> from machinevisiontoolbox import Kernel >>> K = Kernel.Gauss(sigma=2) >>> K """ return str(self) def disp3d(self, block: bool = False, **kwargs) -> None: """Show kernel as a 3D surface plot :param block: block until plot is dismissed, defaults to False :type block: bool, optional .. runblock:: pycon >>> from machinevisiontoolbox import Kernel >>> K = Kernel.Gauss(5, h=15) >>> K.disp3d() .. plot:: from machinevisiontoolbox import Kernel K = Kernel.Gauss(5, h=15) K.disp3d() """ fig = plt.figure() ax = fig.add_subplot(111, projection="3d") h = self.K.shape[0] // 2 x = np.arange(-h, h + 1) y = np.arange(-h, h + 1) X, Y = np.meshgrid(x, y) kwargs.setdefault("linewidth", 0) kwargs.setdefault("antialiased", False) kwargs.setdefault("cmap", cm.coolwarm) ax.plot_surface(X, Y, self.K, **kwargs) ax.set_xlabel("u") ax.set_ylabel("v") @property def T(self) -> "Kernel": return Kernel(self.K.T) @property def shape(self) -> tuple[int, int]: return self.K.shape def print( self, fmt: str | None = None, separator: str = " ", precision: int = 2 ) -> None: """ Print kernel weights 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 kernel values, defaults to 2 :type precision: int, optional Very compact display of kernel numerical values in grid layout. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Kernel >>> K = Kernel.Gauss(sigma=2) >>> K.print() """ if fmt is None: ff = f"{{:.{precision}f}}" width = max(len(ff.format(self.K.max())), len(ff.format(self.K.min()))) fmt = f"{separator}{{:{width}.{precision}f}}" for v in range(self.K.shape[0]): row = "" for u in range(self.K.shape[1]): row += fmt.format(self.K[v, u]) print(row)
[docs] @classmethod def Gauss(cls, sigma: float, h: int | None = None) -> "Kernel": r""" Gaussian kernel :param sigma: standard deviation of Gaussian kernel :type sigma: float :param h: half width of the kernel :type h: integer, optional :return: 2h+1 x 2h+1 Gaussian kernel :rtype: :class:`Kernel` Return the 2-dimensional Gaussian kernel of standard deviation ``sigma`` .. math:: \mathbf{K} = \frac{1}{2\pi \sigma^2} e^{-(u^2 + v^2) / 2 \sigma^2} The kernel is centred within a square array with side length given by: - :math:`2 \mbox{ceil}(3 \sigma) + 1`, or - :math:`2 \mathtt{h} + 1` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Kernel >>> K = Kernel.Gauss(sigma=1, h=2) >>> K.shape >>> print(K) >>> K.print() >>> K = Kernel.Gauss(sigma=2) >>> K.shape Example: .. code-block:: python Kernel.Gauss(5, 15).disp3d() .. plot:: from machinevisiontoolbox import Kernel K = Kernel.Gauss(5, h=15) K.disp3d() .. note:: - The volume under the Gaussian kernel is one. - If the kernel is strongly truncated, ie. it is non-zero at the edges of the window then the volume will be less than one. :references: - |RVC3|, Section 11.5.1.1. :seealso: :meth:`DGauss` """ # make sure sigma, w are valid input if h is None: h = np.ceil(3 * sigma) wi = np.arange(-h, h + 1) x, y = np.meshgrid(wi, wi) m = 1.0 / (2.0 * np.pi * sigma**2) * np.exp(-(x**2 + y**2) / 2.0 / sigma**2) # area under the curve should be 1, but the discrete case is only # an approximation # return m / np.sum(m) return cls(m / np.sum(m), name=f"Gaussian σ={sigma}")
[docs] @classmethod def Laplace(cls) -> "Kernel": r""" Laplacian kernel :return: 3 x 3 Laplacian kernel :rtype: Kernel Return the Laplacian kernel .. math:: \mathbf{K} = \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix} Example: .. runblock:: pycon >>> from machinevisiontoolbox import Kernel >>> K = Kernel.Laplace() >>> K >>> K.print() .. note:: - This kernel has an isotropic response to image gradient. :references: - |RVC3|, Section 11.5.1.3. :seealso: :meth:`LoG` :meth:`zerocross` """ # fmt: off K = np.array([[ 0, 1, 0], [ 1, -4, 1], [ 0, 1, 0]]) # fmt: on return cls(K, name="Laplacian")
[docs] @classmethod def Sobel(cls) -> "Kernel": r""" Sobel edge detector :return: 3 x 3 Sobel kernel :rtype: Kernel Return the Sobel kernel for horizontal gradient .. math:: \mathbf{K} = \frac{1}{8} \begin{bmatrix} 1 & 0 & -1 \\ 2 & 0 & -2 \\ 1 & 0 & -1 \end{bmatrix} Example: .. runblock:: pycon >>> from machinevisiontoolbox import Kernel >>> K = Kernel.Sobel() >>> K >>> K.print() .. note:: - This kernel is an effective vertical-edge detector - The y-derivative (horizontal-edge) kernel is ``K.T`` :references: - |RVC3|, Section 11.5.1.3. :seealso: :meth:`DGauss` """ # fmt: off K = np.array([[1, 0, -1], [2, 0, -2], [1, 0, -1]]) / 8.0 # fmt: on return cls(K, name="Sobel")
[docs] @classmethod def DoG( cls, sigma1: float, sigma2: float | None = None, h: int | None = None ) -> "Kernel": r""" Difference of Gaussians kernel :param sigma1: standard deviation of first Gaussian kernel :type sigma1: float :param sigma2: standard deviation of second Gaussian kernel :type sigma2: float, optional :param h: half-width of Gaussian kernel :type h: int, optional :return: 2h+1 x 2h+1 kernel :rtype: Kernel Return the 2-dimensional difference of Gaussian kernel defined by two standard deviation values: .. math:: \mathbf{K} = G(\sigma_1) - G(\sigma_2) where :math:`\sigma_1 > \sigma_2`. By default, :math:`\sigma_2 = 1.6 \sigma_1`. The kernel is centred within a square array with side length given by: - :math:`2 \mbox{ceil}(3 \sigma) + 1`, or - :math:`2\mathtt{h} + 1` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Kernel >>> K = Kernel.DoG(1) >>> K >>> K.print() Example: .. code-block:: python Kernel.DoG(5, 15).disp3d() .. plot:: from machinevisiontoolbox import Kernel K = Kernel.DoG(5, h=15) K.disp3d() .. note:: - This kernel is similar to the Laplacian of Gaussian and is often used as an efficient approximation. - This is a "Mexican hat" shaped kernel :references: - |RVC3|, Section 11.5.1.3. :seealso: :meth:`LoG` :meth:`Gauss` """ # sigma1 > sigma2 if sigma2 is None: sigma2 = 1.6 * sigma1 else: if sigma2 > sigma1: t = sigma1 sigma1 = sigma2 sigma2 = t # thus, sigma2 > sigma1 if h is None: h = np.ceil(3.0 * sigma1) m1 = Kernel.Gauss(sigma1, h) # thin kernel m2 = Kernel.Gauss(sigma2, h) # wide kernel return cls(m2.K - m1.K, name=f"DoG σ1={sigma1}, σ2={sigma2}")
[docs] @classmethod def LoG(cls, sigma: float, h: int | None = None) -> "Kernel": r""" Laplacian of Gaussian kernel :param sigma: standard deviation of first Gaussian kernel :type sigma: float :param h: half-width of kernel :type h: int, optional :return: 2h+1 x 2h+1 kernel :rtype: Kernel Return a 2-dimensional Laplacian of Gaussian kernel with standard deviation ``sigma`` .. math:: \mathbf{K} = \frac{1}{\pi \sigma^4} \left(\frac{u^2 + v^2}{2 \sigma^2} -1\right) e^{-(u^2 + v^2) / 2 \sigma^2} The kernel is centred within a square array with side length given by: - :math:`2 \mbox{ceil}(3 \sigma) + 1`, or - :math:`2\mathtt{h} + 1` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Kernel >>> K = Kernel.LoG(1) >>> K >>> K.print() Example: .. code-block:: python Kernel.LoG(5, 15).disp3d() .. plot:: from machinevisiontoolbox import Kernel K = Kernel.LoG(5, h=15) K.disp3d() .. note:: This is the classic "Mexican hat" shaped kernel :references: - |RVC3|, Section 11.5.1.3. :seealso: :meth:`Laplace` :meth:`DoG` :meth:`Gauss` :meth:`zerocross` """ if h is None: h = np.ceil(3.0 * sigma) wi = np.arange(-h, h + 1) x, y = np.meshgrid(wi, wi) log = ( 1.0 / (np.pi * sigma**4.0) * ((x**2 + y**2) / (2.0 * sigma**2) - 1) * np.exp(-(x**2 + y**2) / (2.0 * sigma**2)) ) # ensure that the mean is zero, for a truncated kernel this may not # be the case log -= log.mean() return cls(log, name=f"LoG σ={sigma}")
[docs] @classmethod def DGauss(cls, sigma: float, h: int | None = None) -> "Kernel": r""" Derivative of Gaussian kernel :param sigma: standard deviation of Gaussian kernel :type sigma: float :param h: half-width of kernel :type h: int, optional :return: 2h+1 x 2h+1 kernel :rtype: Kernel Returns a 2-dimensional derivative of Gaussian kernel with standard deviation ``sigma`` .. math:: \mathbf{K} = \frac{-x}{2\pi \sigma^2} e^{-(x^2 + y^2) / 2 \sigma^2} The kernel is centred within a square array with side length given by: - :math:`2 \mbox{ceil}(3 \sigma) + 1`, or - :math:`2\mathtt{h} + 1` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Kernel >>> K = Kernel.DGauss(1) >>> K >>> K.print() Example: .. code-block:: python Kernel.DGauss(5, 15).disp3d() .. plot:: from machinevisiontoolbox import Kernel K = Kernel.DGauss(5, h=15) K.disp3d() .. note:: - This kernel is the horizontal derivative of the Gaussian, :math:`dG/dx`. - The vertical derivative, :math:`dG/dy`, is the transpose of this kernel. - This kernel is an effective edge detector. :references: - |RVC3|, Section 11.5.1.3. :seealso: :meth:`HGauss` :meth:`Gauss` :meth:`Sobel` """ if h is None: h = np.ceil(3.0 * sigma) wi = np.arange(-h, h + 1) x, y = np.meshgrid(wi, wi) K = -x / sigma**2 / (2.0 * np.pi) * np.exp(-(x**2 + y**2) / 2.0 / sigma**2) return cls(K, name=f"DGauss σ={sigma}")
[docs] @classmethod def HGauss( cls, sigma: float, h: int | None = None ) -> tuple["Kernel", "Kernel", "Kernel"]: r""" Hessian of Gaussian kernel :param sigma: standard deviation of Gaussian kernel :type sigma: float :param h: half-width of kernel :type h: int, optional :return: 2h+1 x 2h+1 kernels: Hxx, Hyy, Hxy :rtype: (Kernel, Kernel, Kernel) Returns the Hessian of Gaussian with standard deviation ``sigma`` as three 2-dimensional kernels .. math:: \mathbf{K}_{xx} &= \frac{x^2 - \sigma^2}{2\pi \sigma^3} e^{-(x^2 + y^2) / 2 \sigma^2} \\ \mathbf{K}_{yy} &= \frac{y^2 - \sigma^2}{2\pi \sigma^3} e^{-(x^2 + y^2) / 2 \sigma^2} \\ \mathbf{K}_{xy} &= \frac{xy}{2\pi \sigma^6} e^{-(x^2 + y^2) / 2 \sigma^2} The second derivative of an image :math:`\bf{I}` at point :math:`(x,y)` is given by: .. math:: \begin{bmatrix} (\bf{K}_{xx} * \bf{I})_{x,y} & (\bf{K}_{xy} * \bf{I})_{x,y} \\ (\bf{K}_{xy} * \bf{I})_{x,y} & (\bf{K}_{yy} * \bf{I})_{x,y} \end{bmatrix} This second derivative matrix is the Gaussian curvature of the image at :math:`(x,y)`. The kernels are centred within a square array with side length given by: - :math:`2 \mbox{ceil}(3 \sigma) + 1`, or - :math:`2\mathtt{h} + 1` Example: .. runblock:: pycon >>> from machinevisiontoolbox import Kernel >>> Hxx, Hyy, Hxy = Kernel.HGauss(1) >>> Hxx >>> Hxx.print() Example: .. code-block:: python Hxx, Hyy, Hxy = Kernel.HGauss(5, 15) Hxx.disp3d() Hyy.disp3d() Hxy.disp3d() .. plot:: from machinevisiontoolbox import Kernel Hxx, Hyy, Hxy = Kernel.HGauss(5, 15) Hxx.disp3d() .. plot:: from machinevisiontoolbox import Kernel Hxx, Hyy, Hxy = Kernel.HGauss(5, 15) Hyy.disp3d() .. plot:: from machinevisiontoolbox import Kernel Hxx, Hyy, Hxy = Kernel.HGauss(5, 15) Hxy.disp3d() :seealso: :meth:`DGauss` :meth:`Gauss` :meth:`Sobel` """ if h is None: h = np.ceil(3.0 * sigma) wi = np.arange(-h, h + 1) x, y = np.meshgrid(wi, wi) K0 = np.exp(-(x**2 + y**2) / 2.0 / sigma**2) Kxx = (x**2 - sigma**2) / (2.0 * np.pi * sigma**3) * K0 Kyy = (y**2 - sigma**2) / (2.0 * np.pi * sigma**3) * K0 Kxy = (x * y) / (2.0 * np.pi * sigma**6) * K0 return ( cls(Kxx, name=f"Hxx σ={sigma}"), cls(Kyy, name=f"Hyy σ={sigma}"), cls(Kxy, name=f"Hxy σ={sigma}"), )
[docs] @classmethod def Circle( cls, radius: ArrayLike, h: int | None = None, normalize: bool = False, dtype: Dtype = "uint8", ) -> "Kernel": r""" Circular structuring element :param radius: radius of circular structuring element :type radius: scalar, array_like(2) :param h: half-width of kernel :type h: int :param normalize: normalize volume of kernel to one, defaults to False :type normalize: bool, optional :param dtype: data type for image, defaults to ``uint8`` :type dtype: str or NumPy dtype, optional :return: 2h+1 x 2h+1 circular kernel :rtype: Kernel Returns a circular kernel of radius ``radius`` pixels. Sometimes referred to as a tophat kernel. Values inside the circle are set to one, outside are set to zero. If ``radius`` is a 2-element vector the result is an annulus of ones, and the two numbers are interpreted as inner and outer radii respectively. The kernel is centred within a square array with side length given by :math:`2\mathtt{h} + 1`. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Kernel >>> K = Kernel.Circle(2) >>> K >>> K.print() >>> Kernel.Circle([2, 3]) :references: - |RVC3|, Section 11.5.1.1. :seealso: :meth:`Box` """ # check valid input: if not argcheck.isscalar(radius): # r.shape[1] > 1: radius = argcheck.getvector(radius) rmax = radius.max() rmin = radius.min() else: rmax = radius if h is not None: w = h * 2 + 1 elif h is None: w = 2 * rmax + 1 s = np.zeros((int(w), int(w)), dtype=dtype) c = np.floor(w / 2.0) if not argcheck.isscalar(radius): # circle case x = np.arange(w) - c X, Y = np.meshgrid(x, x) r2 = X**2 + Y**2 ll = np.where((r2 >= rmin**2) & (r2 <= rmax**2)) s[ll] = 1 else: # annulus case x = np.arange(w) - c X, Y = np.meshgrid(x, x) ll = np.where(np.round((X**2 + Y**2 - radius**2) <= 0)) s[ll] = 1 if normalize: s /= np.sum(s) return cls(s, name=f"Circle r={radius}")
[docs] @classmethod def Box(cls, h: int, normalize: bool = True) -> "Kernel": r""" Square structuring element :param h: half-width of kernel :type h: int :param normalize: normalize volume of kernel to one, defaults to True :type normalize: bool, optional :return: 2h+1 x 2h+1 kernel :rtype: Kernel Returns a square kernel with unit volume. The kernel is centred within a square array with side length given by :math:`2\mathtt{h} + 1`. Example: .. runblock:: pycon >>> from machinevisiontoolbox import Kernel >>> K = Kernel.Box(2) >>> K >>> K.print() >>> Kernel.Box(2, normalize=False) :references: - |RVC3|, Section 11.5.1.1. :seealso: :meth:`Circle` """ # check valid input: wi = 2 * h + 1 k = np.ones((wi, wi)) if normalize: k /= np.sum(k) return cls(k, name=f"Box h={h}")