OpenCV2学习笔记(八):使用霍夫变换检测直线和圆

在研究一幅图像时,常常会遇到一些平面或线性问题,直线在图像中频繁可见。这些富有意义的特征在物体识别等图像处理过程中扮演着重要的角色。本节主要记录一种经典的检测直线算法——霍夫变换(Hough Transform),用Hough变换检测图像中的直线和圆,开发平台为Qt5.3.2+OpenCV2.4.9。

一:Hough变换检测图像的直线

1.基础Hough变换

在霍夫变换中,直线用以下方程表示:

其中,参数表示一条直线到图像原点(左上角)的距离, 表示与直线垂直的角度。如下图所示,直线1的垂直线的角度 等于0,而水平线5的 等于二分之π。同时,直线3的角度 等于四分之π,而直线4的角度大约是0.7π。为了能够在 为区间[0,π]之间得到所有可能的直线,半径值可取为负数,这正是直线2的情况。

OpenCV提供两种霍夫变换的实现,基础版本是:cv::HoughLines。它的输入为一幅包含一组点的二值图像,其中一些排列后形成直线,通常这是一幅边缘图像,比如来自Sobel算子或Canny算子。cv::HoughLines函数的输出是cv::Vec2f向量,每个元素都是一对代表检测到的直线的浮点数(p, r0)。在设计程序时,先求出图像中每点的极坐标方程,若相交于一点的极坐标曲线的个数大于最小投票数,则将该点所对应的(p, r0)放入vector中,即得到一条直线。cv::HoughLines函数的调用方法如下:

    // 基础版本的Hough变换
    // 首先应用Canny算子获取图像的轮廓
    cv::Mat image = cv::imread("c:/031.jpg", 0);
    cv::Mat contours;
    cv::Canny(image,contours,125,350);

    // Hough变换检测直线
    std::vector<cv::Vec2f> lines;
    cv::HoughLines(contours,lines,
                   1,PI/180,  // 步进尺寸
                   80);       // 最小投票数

实现基础Hough变换的完整代码如下,直接在main函数中添加:

#include <QCoreApplication>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <QDebug>
#define PI 3.1415926

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    // 基础版本的Hough变换
    // 首先应用Canny算子获取图像的轮廓
    cv::Mat image = cv::imread("c:/031.jpg", 0);
    cv::Mat contours;
    cv::Canny(image,contours,125,350);

    // Hough变换检测直线
    std::vector<cv::Vec2f> lines;
    cv::HoughLines(contours,lines,
                   1,PI/180,  // 步进尺寸
                   80);       // 最小投票数

    // 以下步骤绘制直线
    cv::Mat result(contours.rows,contours.cols,CV_8U,cv::Scalar(255));
    image.copyTo(result);
    // 以下遍历图像绘制每一条线
    std::vector<cv::Vec2f>::const_iterator it= lines.begin();
    while (it!=lines.end())
    {
        // 以下两个参数用来检测直线属于垂直线还是水平线
        float rho= (*it)[0];   // 表示距离
        float theta= (*it)[1]; // 表示角度

        if (theta < PI/4. || theta > 3.*PI/4.) // 若检测为垂直线
        {
            // 得到线与第一行的交点
            cv::Point pt1(rho/cos(theta),0);
            // 得到线与最后一行的交点
            cv::Point pt2((rho-result.rows*sin(theta))/cos(theta),result.rows);
            // 调用line函数绘制直线
            cv::line(result, pt1, pt2, cv::Scalar(255), 1);

        } else // 若检测为水平线
        {
            // 得到线与第一列的交点
            cv::Point pt1(0,rho/sin(theta));
            // 得到线与最后一列的交点
            cv::Point pt2(result.cols,(rho-result.cols*cos(theta))/sin(theta));
            // 调用line函数绘制直线
            cv::line(result, pt1, pt2, cv::Scalar(255), 1);
        }
        ++it;
    }

    // 显示结果
    cv::namedWindow("Detected Lines with Hough");
    cv::imshow("Detected Lines with Hough",result);

    return a.exec();
}

改变cv::HoughLines中的最小投票数,可以得到不同的检测结果,因此可知道投票数对于直线的判决具有重要意义。最小投票数为100时:

最小投票数为60时:

注意hough变换要求输入的是包含一组点的二值图像。

2.概率Hough变换

由输出结果可知,基础Hough变换检测出图像中的直线,但在许多应用场合中,我们需要的是局部的线段而非整条直线。正如代码中的情况,需要判定直线与图像边界的交点,才能确定一条线段,否则绘制的直线将穿过整幅图像。其次,霍夫变换仅仅查找边缘点的一种排列方式,由于意外的像素排列或是多条线穿过同一组像素,很有可能带来错误的检测。

为了克服这些难题,人们提出了改进后的算法,即概率霍夫变换,在OpenCV中对应函数cv::HoughLineP,以下给出该算法的实现过程,首先将其封装在类LineFinder中,linefinder.h中添加:

#ifndef LINEFINDER_H
#define LINEFINDER_H
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <QDebug>
#define PI 3.1415926

class LineFinder
{

private:

    // 存放原图像
    cv::Mat img;
    // 向量中包含检测到的直线的端点
    std::vector<cv::Vec4i> lines;
    // 累加器的分辨率参数
    double deltaRho;   // 距离
    double deltaTheta; // 角度
    // 被判定为直线所需要的投票数
    int minVote;
    // 直线的最小长度
    double minLength;
    // 沿直线方向的最大缺口
    double maxGap;

public:

    // 默认的累加器分辨率为单个像素,角度为1度
    // 不设置缺口和最小长度的值
    LineFinder() : deltaRho(1), deltaTheta(PI/180), minVote(10), minLength(0.), maxGap(0.) {}
    // 设置累加器的分辨率
    void setAccResolution(double dRho, double dTheta);
    // 设置最小投票数
    void setMinVote(int minv);
    // 设置缺口和最小长度
    void setLineLengthAndGap(double length, double gap);
    // 使用概率霍夫变换
    std::vector<cv::Vec4i> findLines(cv::Mat& binary);
    // 绘制检测到的直线
    void drawDetectedLines(cv::Mat &image, cv::Scalar color=cv::Scalar(255,255,255));
};

#endif // LINEFINDER_H

接着对各个函数进行定义,在linefinder.cpp中添加:

#include "linefinder.h"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <QDebug>
#define PI 3.1415926

// 设置累加器的分辨率
void LineFinder::setAccResolution(double dRho, double dTheta) {

    deltaRho= dRho;
    deltaTheta= dTheta;
}

// 设置最小投票数
void LineFinder::setMinVote(int minv) {

    minVote= minv;
}

// 设置缺口和最小长度
void LineFinder::setLineLengthAndGap(double length, double gap) {

    minLength= length;
    maxGap= gap;
}

// 使用概率霍夫变换
std::vector<cv::Vec4i> LineFinder::findLines(cv::Mat& binary)
{

    lines.clear();
    // 调用概率霍夫变换函数
    cv::HoughLinesP(binary,lines,deltaRho,deltaTheta,minVote, minLength, maxGap);
    return lines;
}

// 绘制检测到的直线
void LineFinder::drawDetectedLines(cv::Mat &image, cv::Scalar color)
{
    std::vector<cv::Vec4i>::const_iterator it2= lines.begin();

    while (it2!=lines.end()) {

        cv::Point pt1((*it2)[0],(*it2)[1]);
        cv::Point pt2((*it2)[2],(*it2)[3]);
        cv::line( image, pt1, pt2, color);

        ++it2;
    }
}

最后简单修改main函数即可:

#include <QCoreApplication>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <QDebug>
#include "linefinder.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    cv::Mat image = cv::imread("c:/031.jpg", 0);
    if (!image.data)
    {
        qDebug() << "No Input Image";
        return 0;
    }
    // 首先应用Canny算法检测出图像的边缘部分
    cv::Mat contours;
    cv::Canny(image, contours, 125, 350);

    LineFinder finder; // 创建一对象

    // 设置概率Hough参数
    finder.setLineLengthAndGap(100, 20);
    finder.setMinVote(80); //最小投票数

    // 以下步骤检测并绘制直线
    std::vector<cv::Vec4i>lines = finder.findLines(contours);
    finder.drawDetectedLines(image);
    cv::namedWindow("Detected Lines");
    cv::imshow("Detected Lines", image);

    return a.exec();
}

得到的结果如下:

简而言之,霍夫变换的目的是找到二值图像中经过足够多数量的点的所有直线,它分析每个单独的像素点,并识别出所有可能经过它的直线。当同一直线穿过许多点,便意味着这条线的存在足够明显。

而概率霍夫变换在原算法的基础上增加了少许改动,这些改动主要体现在:

  1. 不再系统地逐行扫描图像,而是随机挑选像素点,一旦累加器中的某一项达到给定的最小值,就扫描沿着对应直线的像素并移除所有经过的点(即使它们并未投过票);
  2. 概率霍夫变换定义了两个额外的参数:一个是可以接受的线段的最小长度,另一个是允许组成连续线段的最大像素间隔,虽然这些额外的步骤必然增加算法的复杂度,但由于参与投票的点数量有所减少,因此得到了一些补偿。

二:Hough变换检测图像中的圆

霍夫变换可以用于检测其他几何体,事实上,可以用参数方程表示的几何体都可以尝试用霍夫变换进行检测,比如圆形,它对应的参数方程为:

该函数包含三个参数,分别是圆心的坐标和圆的半径,这意味着需要三维的累加器。OpenCV中实现的霍夫圆检测算法通常使用两个步骤进行检测:

  1. 二维累加器用于寻找可能为圆的位置。由于在圆周上的点的梯度应该指向半径的方向,因此对于每一个点,只有沿着梯度方向的项才得到增加(这需要预先设定最大和最小的半径);
  2. 若找到了圆心,则构建一维的半径的直方图,这个直方图的峰值对应的是检测到的圆的半径。

以下给出霍夫变换检测圆形的实现方法,主要使用了函数cv::HoughCircles,它整合了Canny检测和霍夫变换,同时,在进行霍夫变换之前,建议对操作图像进行平滑,以减少可能引起误检测的噪声点:

    // 在调用cv::HoughCircles函数前对图像进行平滑,减少误差
    cv::GaussianBlur(image,image,cv::Size(7,7),1.5);
    std::vector<cv::Vec3f> circles;
    cv::HoughCircles(image, circles, CV_HOUGH_GRADIENT,
        2,   // 累加器的分辨率(图像尺寸/2)
        50,  // 两个圆之间的最小距离
        200, // Canny中的高阈值
        100, // 最小投票数
        20, 80); // 有效半径的最小和最大值

完整代码如下,只需在main函数中添加:

    // 检测图像中的圆形
    image= cv::imread("c:/44.png",0);
    if (!image.data)
    {
        qDebug() << "No Input Image";
        return 0;
    }
    // 在调用cv::HoughCircles函数前对图像进行平滑,减少误差
    cv::GaussianBlur(image,image,cv::Size(7,7),1.5);
    std::vector<cv::Vec3f> circles;
    cv::HoughCircles(image, circles, CV_HOUGH_GRADIENT,
        2,   // 累加器的分辨率(图像尺寸/2)
        50,  // 两个圆之间的最小距离
        200, // Canny中的高阈值
        100, // 最小投票数
        20, 80); // 有效半径的最小和最大值

    // 绘制圆圈
    image= cv::imread("c:/44.png",0);
    if (!image.data)
    {
        qDebug() << "No Input Image";
        return 0;
    }
    // 一旦检测到圆的向量,遍历该向量并绘制圆形
    // 该方法返回cv::Vec3f类型向量
    // 包含圆圈的圆心坐标和半径三个信息
    std::vector<cv::Vec3f>::const_iterator itc= circles.begin();
    while (itc!=circles.end())
    {
      cv::circle(image,
          cv::Point((*itc)[0], (*itc)[1]), // 圆心
          (*itc)[2], // 圆的半径
          cv::Scalar(255), // 绘制的颜色
          6); // 圆形的厚度
      ++itc;
    }

    cv::namedWindow("Detected Circles");
    cv::imshow("Detected Circles",image);

效果:

三:广义Hough变换

对于一些形状,其函数表达式比较复杂,如三角形、多边形等,但还是可能使用霍夫变换定位这些形状,其原理与以上的检测是一样的。先创建一个二维的累加器,用于表示所有可能存在目标形状的位置。因此必须定义一个参考点,图像上的每个特征点都对可能的参考点坐标进行投票。由于一个点可能位于形状轮廓内的任意位置,所有可能的参考位置将在累加器中描绘出一个形状,它将是目标形状的镜像。

时间: 2024-10-09 20:50:51

OpenCV2学习笔记(八):使用霍夫变换检测直线和圆的相关文章

angular学习笔记(八)

本篇介绍angular控制视图的显示和隐藏: 通过给元素添加ng-show属性或者ng-hide属性来控制视图的显示或隐藏: ng-show: 绑定的数据值为true时,显示元素,值为false时,隐藏元素 ng-hide: 绑定的数据值为true时,隐藏元素,值为false时,显示元素 (其实只要用到其中一个就可以了) 下面来看个简单的例子,点击按钮可以显示/隐藏元素: <!DOCTYPE html> <html ng-app> <head> <title>

Linux System Programming 学习笔记(八) 文件和目录管理

1. 文件和元数据 每个文件都是通过inode引用,每个inode索引节点都具有文件系统中唯一的inode number 一个inode索引节点是存储在Linux文件系统的磁盘介质上的物理对象,也是LInux内核通过数据结构表示的实体 inode存储相关联文件的元数据 ls -i 命令获取文件的inode number /* obtaining the metadata of a file */ #include <sys/types.h> #include <sys/stat.h>

马哥学习笔记八——LAMP编译安装之PHP及xcache

1.解决依赖关系: 请配置好yum源(可以是本地系统光盘)后执行如下命令: # yum -y groupinstall "X Software Development" 如果想让编译的php支持mcrypt扩展,此处还需要下载如下两个rpm包并安装之: libmcrypt-2.5.7-5.el5.i386.rpm libmcrypt-devel-2.5.7-5.el5.i386.rpm 2.编译安装php-5.4.13 首先下载源码包至本地目录. # tar xf php-5.4.13

Lua学习笔记(八):数据结构

table是Lua中唯一的数据结构,其他语言所提供的数据结构,如:arrays.records.lists.queues.sets等,Lua都是通过table来实现,并且在Lua中table很好的实现了这些数据结构. 1.数组 在Lua中通过整数下标访问table中元素,既是数组,并且数组大小不固定,可动态增长.通常我们初始化数组时,就间接地定义了数组的大小,例如: 1 a = {} -- new array 2 for i=1, 1000 do 3 a[i] = 0 4 end 5 6 --数

初探swift语言的学习笔记八(保留了许多OC的实现)

尽管swift作为一门新语言,但还保留了许多OC的机制,使得swift和OC更好的融合在一起.如果没有OC基础的先GOOGLE一下. 如:KVO,DELEGATE,NOTIFICATION. 详见DEMO. import Foundation @objc // 需要打开objc标识,否则@optional编译出错 protocol kvoDemoDelegate { func willDoSomething() @optional func didDoSomething() //可选实现, }

《Hibernate学习笔记八》:组件映射

<Hibernate学习笔记八>:组件映射 前面介绍了一对一的单向.双向外键关联,例如,学生证和学生是一个一对一的关系.这篇博文主要是介绍下组件映射,即一个是另一个的一部分,例如,学生证的信息也可以作为学生信息的一部分,即在数据库中只存在学生一个表,而不是有学生和学生证两个表,并且这两个表中有一个一对一的关联关系. 如下: 有人或许会说,那我们就将学生和学生证的信息写在一个类中,则就不需要组件映射了,确实可以这样,但是,根据类的设计原则,我们一般都会将其设计为两个类,然后将学生证的信息作为一个

OpenCV2学习笔记:图像的读取与显示

1. 图像读取:imread() Mat imread(const string& ?lename, int ?ags=1 ) 参数介绍: filename: 待加载的文件名称. flags: 此标志用来指定被加载图像的颜色类型(color type).这个标志的取值可以有: -- CV_LOAD_IMAGE_ANYDEPTH : 如果设置这个标志的话,如果图像为16位或32位深度的图像,则返回对应深度的图像:否则,将图像转换为8位深度图像再返回. -- CV_LOAD_IMAGE_COLOR

sencha touch权威指南---学习笔记5-经纬度获取计算直线距离

<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>    <meta charset="utf-8" />    <title></title>    <link rel="stylesheet" href="sdk-touch/resources/css/sencha-touch.

iOS学习笔记(八)——iOS网络通信http之NSURLConnection

转自:http://blog.csdn.net/xyz_lmn/article/details/8968182 移动互联网时代,网络通信已是手机终端必不可少的功能.我们的应用中也必不可少的使用了网络通信,增强客户端与服务器交互.这一篇提供了使用NSURLConnection实现http通信的方式. NSURLConnection提供了异步请求.同步请求两种通信方式. 1.异步请求 iOS5.0 SDK NSURLConnection类新增的sendAsynchronousRequest:queu