Source code for machinevisiontoolbox.ImageLineFeatures

import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
from matplotlib.ticker import ScalarFormatter

import cv2 as cv
from spatialmath import base


class ImageLineFeaturesMixin:
    """
    Line features are common in in many human-built environments.

    """

    def Hough(self, **kwargs):
        """
        Find Hough line features

        :return: Hough lines
        :rtype: :class:`~machinevisiontoolbox.ImageLineFeatures.HoughFeature`

        Compute the Hough transform of the image and return an object that
        represents the lines found within the image.

        :seealso: :class:`~machinevisiontoolbox.ImageLineFeatures.HoughFeature`
        """
        return HoughFeature(self, **kwargs)


# --------------------- supporting classes -------------------------------- #


[docs]class HoughFeature: def __init__(self, image, ntheta=180, drho=1): r""" Hough line features :param image: greyscale image :type image: :class:`Image` :param ntheta: number of steps in the :meth:`\theta` direction, defaults to 180 :type ntheta: int, optional :param drho: increment size in the :meth:`\rho` direction, defaults to 1 :type drho: int, optional Create a Hough line feature object. It can be used to detect: - lines using the classical Hough algorithm :meth:`lines` - line segments using the probabilistic Hough algorith :meth:`lines_p` The Hough accumulator is a 2D array that counts votes for lines .. math:: u \cos \theta + v \sin \theta = \rho with quantized parameters :math:`\theta` and :math:`\rho`. The parameter :math:`\theta` is quantized into ``ntheta`` steps spanning the interval :math:`[-\pi, \pi)`, while :math:`\rho` is quantized into steps of ``drho`` spanning the vertical dimension of the image. :note: Lines are not detected until :meth:`lines` or :meth:`lines_p` is called. This instance simply holds parameters. :reference: - Robotics, Vision & Control for Python, Section 12.2, P. Corke, Springer 2023. :seealso: :meth:`lines` :meth:`lines_p` """ self.image = image.to_int() self.dtheta = np.pi / ntheta self.drho = drho self.A = None
[docs] def lines(self, minvotes): r""" Get Hough lines :param minvotes: only return lines with at least this many votes :type minvotes: int :return: Hough lines, one per row as :math:`(\theta, \rho)` :rtype: ndarray(n,2) Return a set of lines that have at least ``minvotes`` of support. Each line is described by :math:`(\theta, \rho)` such that .. math:: u \cos \theta + v \sin \theta = \rho :seealso: :meth:`plot_lines` :meth:`lines_p` `opencv.HoughLines <https://docs.opencv.org/3.4/dd/d1a/group__imgproc__feature.html#ga46b4e588934f6c8dfd509cc6e0e4545a>`_ """ lines = cv.HoughLines( image=self.image, rho=self.drho, theta=self.dtheta, threshold=minvotes ) if lines is None: return np.zeros((0, 2)) else: return np.array((lines[:, 0, 1], lines[:, 0, 0])).T
[docs] def lines_p(self, minvotes, minlinelength=30, maxlinegap=10, seed=None): r""" Get probabilistic Hough lines :param minvotes: only return lines with at least this many votes :type minvotes: int :param minlinelength: minimum line length. Line segments shorter than that are rejected. :type minlinelength: int :param maxlinegap: maximum allowed gap between points on the same line to link them. :type maxlinegap: int :return: Hough lines, one per row as :math:`(u_1, v_1, u_2, v_2)` :rtype: ndarray(n,4) Return a set of line segments that have at least ``minvotes`` of support. Each line segment is described by its end points :math:`(u_1, v_1)` and :math:`(u_2, v_2)`. :seealso: :meth:`plot_lines` :meth:`lines` `opencv.HoughLinesP <https://docs.opencv.org/3.4/dd/d1a/group__imgproc__feature.html#ga8618180a5948286384e3b7ca02f6feeb>`_ """ if seed is not None: cv.setRNGSeed(seed) lines = cv.HoughLinesP( image=self.image, rho=self.drho, theta=self.dtheta, threshold=minvotes, minLineLength=minlinelength, maxLineGap=maxlinegap, ) if lines is None: return np.zeros((0, 4)) else: return lines[:, 0, :]
[docs] def plot_lines(self, lines, *args, **kwargs): r""" Plot Hough lines :param lines: Hough or probabilistic Hough lines :type lines: ndarray(n,2), ndarray(n,4) :param args: positional arguments passed to Matplotlib :obj:`~matplotlib.pyplot.plot` :param kwargs: arguments passed to Matplotlib :obj:`~matplotlib.pyplot.plot` Detected lines are given as rows of ``lines``: - for Hough lines, each row is :math:`(\theta, \rho)`, and lines are clipped by the bounds of the current plot. - for probabilistic Hough lines, each row is :math:`(u_1, v_1, u_2, v_2)`, and lines segments are drawn on the current plot. :seealso: :meth:`lines` :meth:`lines_p` """ if lines.shape[1] == 2: # Hough lines theta, rho = lines.T homlines = np.row_stack((np.cos(theta), np.sin(theta), -rho)) base.plot_homline(homlines, *args, **kwargs) else: for line in lines: plt.plot(line[[0, 2]], line[[1, 3]], *args, **kwargs)
[docs] def accumulator(self, skip=1): r""" Compute the Hough accumulator :param skip: increment for line strength threshold, defaults to 1 :type skip: int, optional It creates two new attributes for the instance: - ``A`` which is the Hough "accumulator" array, rows represent :math:`\rho` and columns represent :math:`\theta`. - ``votes`` is a list of the number of lines found versus threshold, it can be used to select an optimal threshold. - ``extent`` is :math:`[\theta_{\mbox{min}}, \theta_{\mbox{max}}, \rho_{\mbox{min}}, \rho_{\mbox{max}}]`. .. warning:: The OpenCV ``HoughLines`` function does not expose the accumulator array. This method "reverse engineers" the accumulator array through a costly process of computing the Hough transform for all possible thresholds (increasing in steps of ``skip``). This is helpful for pedagogy but very inefficient in practice. :seealso: :meth:`plot_accumulator` """ self.nz = np.sum(self.image > 0) t = 0 theta = np.empty((0,)) rho = np.empty((0,)) votes = [] while True: lines = cv.HoughLines( image=self.image, rho=self.drho, theta=self.dtheta, threshold=t ) if lines is None: # no lines found at this threshold, bail out self.t = t - 1 break # append the found lines and votes theta = np.concatenate((theta, lines[:, 0, 1].flatten())) rho = np.concatenate((rho, lines[:, 0, 0].flatten())) votes.append(lines.shape[0]) t += skip # increment the line strength threshold # now create the accumulator array theta_bins = np.arange( theta.min() - self.dtheta / 2, theta.max() + self.dtheta / 2, self.dtheta ) rho_bins = np.arange( rho.min() - self.drho / 2, rho.max() + self.drho / 2, self.drho ) self.extent = [theta_bins[0], theta_bins[-1], rho_bins[0], rho_bins[-1]] self.A = np.histogram2d(theta, rho, bins=(theta_bins, rho_bins))[0].T self.votes = votes
[docs] def plot_accumulator(self, **kwargs): r""" Plot the Hough accumulator array :param kwargs: options passed to :func:`~matplotlib.pyplot.imshow` The Hough accumulator is computed, if not already existing, and the displayed as an image where brightness is proportional to the number of votes for that :math:`(\theta, \rho)` coordinate. :seealso: :meth:`accumulator` """ if self.A is None: self.accumulator() plt.imshow( self.A, aspect="auto", interpolation="nearest", origin="lower", extent=(self.extent), **kwargs, ) plt.xlabel(r"$\theta$ (radians)") plt.xlim(0, np.pi) plt.ylabel(r"$\rho$ (pixels)") plt.grid(True)
if __name__ == "__main__": from machinevisiontoolbox import Image from math import pi square = Image.Squares(number=1, size=256, fg=128).rotate(0.3) edges = square.canny() h = edges.hough() print(h)