对于平面中的一条直线,在笛卡尔坐标中,常见的有点斜式,两点式两种表示方法。然而在霍夫变换中,考虑的是另外一种表示方式:使用(r, theta)来表示一条直线。其中r为该直线到原点的距离,theta为该直线的垂线与x轴的夹角。如下图所示:
根据霍夫变换原理,利用极坐标形式表示直线时,在图像空间中经过某一点的所有直线映射到参数空间中是一个正弦曲线。图像空间中直线上的两个点在参数空间中映射的两条正弦曲线相交于一点。
通过上述的变换过程,将图像中的直线检测转换成了在参数空间中寻找某个点 通过的正线曲线最多的问题。由于在参数空间内的曲线是连续的,而在实际情况中图像的像素是离散的,因此我们需要将参数空间的坐标轴进行离散化,用离散后的方格表示每一条正弦曲线。首先寻找符合条件的网格,之后寻找该网格对应的图像空间中所有的点,这些点共同组成了原图像中的直线。
由此可见,霍夫变换算法检测图像中的直线主要分为4个步骤
霍夫检测具有抗干扰能力强,对图像中直线的残缺部分、噪声以及其它共存的非直线结构不敏感,能容忍特征边界描述中的间隙,并且相对不受图像噪声影响等优点,但是霍夫变换的时间复杂度和空间复杂度都很高,并且检测精度受参数离散间隔制约。离散间隔较大时会降低检测精度,离散间隔较小时虽然能提高精度,但是会增加计算负担,导致计算时间边长
public static void HoughLines(Mat image, Mat lines, double rho, double theta, int threshold, double srn, double stn, double min_theta)
使用标准霍夫变换和多尺度霍夫变换函数HoughLins()提取直线时无法准确知道图像中直线或者线段的长度,只能得到图像中是否存在符合要求的直线以及直线的极坐标解析式。如果需要准确的定位图像中线段的位置,HoughLins()函数便无法满足需求。但是OpenCV 4提供的渐进概率式霍夫变换函数HoughLinesP()可以得到图像中满足条件的直线或者线段两个端点的坐标,进而确定直线或者线段的位置。
public static void HoughLinesP(Mat image, Mat lines, double rho, double theta, int threshold, double minLineLength, double maxLineGap)
参数一:image,待检测直线的原图像,必须是CV_8U的单通道图像.
参数二:lines,输出线段。每条线由4元素表示。如下,分别代表每个线段的两个端点
package cn.onlyloveyd.demo.ui import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import cn.onlyloveyd.demo.R import cn.onlyloveyd.demo.databinding.ActivityHoughLineBinding import cn.onlyloveyd.demo.ext.showMat import org.opencv.android.Utils import org.opencv.core.Mat import org.opencv.core.Point import org.opencv.core.Scalar import org.opencv.imgproc.Imgproc import kotlin.math.cos import kotlin.math.roundToInt import kotlin.math.sin /** * 霍夫直线检测 * author: yidong * 2020/7/18 */ class HoughLineDetectActivity : AppCompatActivity() { private lateinit var mBinding: ActivityHoughLineBinding private lateinit var mGray: Mat private lateinit var mEdge: Mat override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mBinding = DataBindingUtil.setContentView(this, R.layout.activity_hough_line) mBinding.presenter = this mGray = Mat() mEdge = Mat() val bgr = Utils.loadResource(this, R.drawable.book) Imgproc.cvtColor(bgr, mGray, Imgproc.COLOR_BGR2GRAY) mBinding.ivLena.showMat(mGray) Imgproc.Canny(mGray, mEdge, 80.0, 150.0, 3, false) } override fun onDestroy() { mGray.release() mEdge.release() super.onDestroy() } fun doHoughLineDetect() { title = "HoughLine" val lines = Mat() Imgproc.HoughLines(mEdge, lines, 1.0, Math.PI / 180.0, 150) val out = Mat.zeros(mGray.size(), mGray.type()) val data = FloatArray(2) for (i in 0 until lines.rows()) { lines.get(i, 0, data) val rho = data[0] // 直线距离坐标原点的距离 val theta = data[1] // 直线过坐标原点垂线与x轴夹角 val a = cos(theta.toDouble()) //夹角的余弦值 val b = sin(theta.toDouble()) //夹角的正弦值 val x0 = a * rho //直线与过坐标原点的垂线的交点 val y0 = b * rho val pt1 = Point() val pt2 = Point() pt1.x = (x0 + 1000 * (-b)).roundToInt().toDouble() pt1.y = (y0 + 1000 * (a)).roundToInt().toDouble() pt2.x = (x0 - 1000 * (-b)).roundToInt().toDouble() pt2.y = (y0 - 1000 * (a)).roundToInt().toDouble() Imgproc.line(out, pt1, pt2, Scalar(255.0, 255.0, 255.0), 2, Imgproc.LINE_AA, 0) } mBinding.ivResult.showMat(out) out.release() lines.release() } fun doHoughLinePDetect() { title = "HoughLineP" val lines = Mat() Imgproc.HoughLinesP(mEdge, lines, 1.0, Math.PI / 180.0, 100, 50.0, 10.0) val out = Mat.zeros(mGray.size(), mGray.type()) for (i in 0 until lines.rows()) { val data = IntArray(4) lines.get(i, 0, data) val pt1 = Point(data[0].toDouble(), data[1].toDouble()) val pt2 = Point(data[2].toDouble(), data[3].toDouble()) Imgproc.line(out, pt1, pt2, Scalar(255.0, 255.0, 255.0), 2, Imgproc.LINE_AA, 0) } mBinding.ivResult.showMat(out) out.release() lines.release() } }