本人正在学习 OpenCV 计算机视觉相关知识,此篇为个人学习笔记,参考书籍为冯振等人的《OpenCV4 快速入门》,主要记录 OpenCV 在 Linux 环境下的编译、运行实操流程与相关知识点总结。

笔记内容均为亲自实操验证,过程中踩过不少编译配置、运行参数的坑,整理出来一方面用于自己复盘巩固,另一方面也希望能帮助到刚入门 OpenCV、在编译运行示例程序时遇到问题的小伙伴。文中内容如有疏漏或错误之处,欢迎各位大佬评论区指正交流,共同进步~

第七章 目标检测

7.1 形状检测

7.1.1 直线检测

  • 霍夫变换:将图像中的像素在一个空间坐标系中变换到另一个空间坐标系中,使得在这两个空间中形成峰值,成为统计峰值的问题。
  • 优点:抗干扰能力强 缺点:时间复杂度和空间复杂度高
  • 步骤:1.将坐标轴离散化 2. 将图像中每个非零像素通过映射关系求取参数空间通过的方格

3. 统计参数空间内每个方格出现的次数 4. 将参数空间中表示直线的方格参数作为图像中直线的参数

  • 标准霍夫变换和多尺度霍夫变换 HoughLines()函数 ,,,无法知道直线的长度,只能得到图像中是否存在符合要求的直线
void cv::HoughLines(
    InputArray image,       // 输入图像,必须是CV_8U的单通道二值图像
    OutputArray lines,      // 输出直线参数
    double rho,                 // ρ步长,以像素为单位的距离分辨率,数值越小,精度越高
    double theta,               // θ步长,以弧度为单位的角度分辨率,数值越小,精度越高
    int threshold,              // 投票数阈值(阈值越低,检测的直线越多)
    double srn = 0,             // ρ方向尺度除数(多尺度用,非负数)
    double stn = 0,             // θ方向尺度除数(多尺度用,非负数)
    double min_theta = 0,       // θ最小值
    double max_theta = CV_PI    // θ最大值
);

示例:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

void drawLine(Mat &img, //要标记直线的图像
    vector<Vec2f> lines,   //检测的直线数据
    double rows,   //原图像的行数(高)
    double cols,  //原图像的列数(宽)
    Scalar scalar,  //绘制直线的颜色
    int n  //绘制直线的线宽
)
{
    Point pt1, pt2;
    for (size_t i = 0; i < lines.size(); i++)
    {
        float rho = lines[i][0];  //直线距离坐标原点的距离
        float theta = lines[i][1];  //直线过坐标原点垂线与x轴夹角
        double a = cos(theta);  //夹角的余弦值
        double b = sin(theta);  //夹角的正弦值
        double x0 = a*rho, y0 = b*rho;  //直线与过坐标原点的垂线的交点
        double length = max(rows, cols);  //图像高宽的最大值
                                          //计算直线上的一点
        pt1.x = cvRound(x0 + length  * (-b));
        pt1.y = cvRound(y0 + length  * (a));
        //计算直线上另一点
        pt2.x = cvRound(x0 - length  * (-b));
        pt2.y = cvRound(y0 - length  * (a));
        //两点绘制一条直线
        line(img, pt1, pt2, scalar, n);
    }
}

int main()
{
    Mat img = imread("HoughLines.jpg", IMREAD_GRAYSCALE);
    if (img.empty())
    {
        cout << "请确认图像文件名称是否正确" << endl;
        return -1;
    }
    Mat edge;

    //检测边缘图像,并二值化
    Canny(img, edge, 80, 180, 3, false);
    threshold(edge, edge, 170, 255, THRESH_BINARY);

    //用不同的累加器进行检测直线
    vector<Vec2f> lines1, lines2;
    HoughLines(edge, lines1, 1, CV_PI / 180, 50, 0, 0);
    HoughLines(edge, lines2, 1, CV_PI / 180, 150, 0, 0);

    //在原图像中绘制直线
    Mat img1, img2;
    img.copyTo(img1);
    img.copyTo(img2);
    drawLine(img1, lines1, edge.rows, edge.cols, Scalar(255), 2);
    drawLine(img2, lines2, edge.rows, edge.cols, Scalar(255), 2);

    //显示图像
    imshow("edge", edge);
    imshow("img", img);
    imshow("img1", img1);
    imshow("img2", img2);
    waitKey(0);
    return 0;
}
  • 渐进概率式霍夫变换 HoughLinesP()函数
void cv::HoughLines(
    InputArray image,       // 输入图像,必须是CV_8U的单通道二值图像
    OutputArray lines,      // 输出直线参数
    double rho,                 // ρ步长,以像素为单位的距离分辨率,数值越小,精度越高
    double theta,               // θ步长,以弧度为单位的角度分辨率,数值越小,精度越高
    int threshold,              // 投票数阈值(阈值越低,检测的直线越多)
    double minLineLength = 0,       // 直线的最小长度
    double maxLineGap = 0    // 同一直线上相邻两个点之间的最大距离
);

示例:

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;

int main()
{
    Mat img = imread("HoughLines.jpg", IMREAD_GRAYSCALE);
    if (img.empty())
    {
        cout << "请确认图像文件名称是否正确" << endl;
        return -1;
    }
    Mat edge;

    //检测边缘图像,并二值化
    Canny(img, edge, 80, 180, 3, false);
    threshold(edge, edge, 170, 255, THRESH_BINARY);
    //利用渐进概率式霍夫变换提取直线
    vector<Vec4i> linesP1, linesP2;
    HoughLinesP(edge, linesP1, 1, CV_PI / 180, 150, 30, 10);  //两个点连接最大距离10
    HoughLinesP(edge, linesP2, 1, CV_PI / 180, 150, 30, 30);  //两个点连接最大距离30
    //绘制两个点连接最大距离10直线检测结果
    Mat img1;
    img.copyTo(img1);
    for (size_t i = 0; i < linesP1.size(); i++)
    {
        line(img1, Point(linesP1[i][0], linesP1[i][1]),
            Point(linesP1[i][2], linesP1[i][3]), Scalar(255), 3);
    }
    //绘制两个点连接最大距离30直线检测结果
    Mat img2;
    img.copyTo(img2);
    for (size_t i = 0; i < linesP2.size(); i++)
    {
        line(img2, Point(linesP2[i][0], linesP2[i][1]),
            Point(linesP2[i][2], linesP2[i][3]), Scalar(255), 3);
    }
    //显示图像
    imshow("img1", img1);
    imshow("img2", img2);
    waitKey(0);
    return 0;
}
  • 在含有坐标系众多点中寻找是否存在直线 HoughLinesPointSet()函数

7.1.2 直线拟合

将所有数据只拟合出一条直线

  • 利用最小二乘M-estimator方法拟合直线的fitLine()函数

void cv::fitLine(
    cv::InputArray points,       // 输入点集(vector<Point>/Mat/Nx2矩阵)
    cv::OutputArray line,        // 输出直线参数(4维向量:vx, vy, x0, y0)
    int distType,                // 距离类型(拟合算法选择)
    double param,                // 距离参数(通常设0,使用默认值)
    double reps,                 // 距离精度(通常设0.01)
    double aeps                  // 角度精度(通常设0.01)
);

7.1.3 圆形检测 HoughCircles()函数

方法与直线检测相似

void cv::HoughCircles(
    cv::InputArray image,       // 输入图像,必须是CV_8UC1
    cv::OutputArray circles,    // 输出圆参数(x,y,r)
    int method,                 // 检测方法(仅支持HOUGH_GRADIENT)
    double dp,                  // 图像分辨率缩放因子
    double minDist,             // 圆心之间的最小距离
    double param1 = 100,        // Canny边缘检测的高阈值
    double param2 = 100,        // 圆心投票阈值(核心),阈值越大,检测的越精确
    int minRadius = 0,          // 最小圆半径
    int maxRadius = 0           // 最大圆半径
); 

7.2 轮廓检测 findContours()函数

由外到内轮廓的级别越来越低

  • 检测轮廓函数
void cv::findContours(
    cv::InputArray image,               // 输入图像(二值图或单通道灰度图)
    cv::OutputArrayOfArrays contours,   // 输出轮廓集合
    cv::OutputArray hierarchy,          // 输出轮廓层级(可选)
    int mode,                           // 轮廓检索模式
    int method,                         // 轮廓逼近方法
    cv::Point offset = cv::Point()      // 轮廓点偏移量(默认无偏移)
);

  • 显示轮廓函数 drawContours()函数
void cv::drawContours(
   InputOutputArray image,       // 要绘制轮廓的目标图像(彩色/灰度均可)
   InputArrayOfArrays contours,  // 轮廓集合(来自findContours())
   int contourIdx,                   // 要绘制的轮廓数目,负数表示绘制所有轮廓
   const Scalar& color,          // 轮廓绘制颜色
   int thickness = 1,                // 线宽(填充用-1)
   int lineType = LINE_8,            // 线条类型(默认即可)
   InputArray hierarchy = noArray(),  // 轮廓层级(可选,用于层级过滤)
   int maxLevel = INT_MAX,           // 绘制的轮廓层级深度(可选)
   Point offset = Point()    // 轮廓点坐标偏移(0绘制指定轮廓;1绘制轮廓+嵌套轮廓
                             //               2绘制轮廓+嵌套轮廓+嵌套的嵌套轮廓)
);

示例:

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
using namespace cv;
using namespace std;

int main()
{
    system("color F0");  //更改输出界面颜色
    Mat img = imread("keys.jpg");
    if (img.empty())
    {
        cout << "请确认图像文件名称是否正确" << endl;
        return -1;
    }
    imshow("原图", img);
    Mat gray, binary;
    cvtColor(img, gray, COLOR_BGR2GRAY);  //转化成灰度图
    GaussianBlur(gray, gray, Size(13, 13), 4, 4);  //平滑滤波
    threshold(gray, binary, 170, 255, THRESH_BINARY | THRESH_OTSU);  //自适应二值化
    // 轮廓发现与绘制
    vector<vector<Point>> contours;  //轮廓
    vector<Vec4i> hierarchy;  //存放轮廓结构变量
    findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
    //绘制轮廓
    for (int t = 0; t < contours.size(); t++)
    {
        drawContours(img, contours, t, Scalar(0, 0, 255), 2, 8);
    }
    //输出轮廓结构描述
    for (int i = 0; i < hierarchy.size(); i++)
    {
        cout << hierarchy[i] << endl;
    }
    //显示结果
    imshow("轮廓检测结果", img);
    waitKey(0);
    return 0;
}

7.2.2 轮廓面积 contourArea()函数

定义:指每个轮廓中所有像素点围成的区域面积

double cv::contourArea(
    cv::InputArray contour,    // 输入单个轮廓(点集合)
    bool oriented = false      // 是否返回有方向的面积(正/负)
);

7.2.3 轮廓周长 arcLength()函数

轮廓或曲线相邻两个像素点之间的连线距离

double cv::arcLength(
    cv::InputArray curve,    // 输入轮廓/曲线(点集合)
    bool closed             // 是否为闭合轮廓(true表示闭合)
);

7.2.4 轮廓外接多边形

  • 求取轮廓最大外接矩形 boundingRect()函数
cv::Rect cv::boundingRect(cv::InputArray array)    // 输入轮廓/点集合);
  • 求取轮廓最小外接矩形 minAreaRect()函数

同上

  • 寻找逼近轮廓的多边形 approxPolyDP()函数
void cv::approxPolyDP(
    cv::InputArray curve,    // 输入轮廓/曲线(点集合)
    cv::OutputArray approxCurve,  // 输出逼近后的多边形点集合
    double epsilon,          // 逼近精度(核心参数)
    bool closed              // 是否闭合多边形
);

7.2.5 点到轮廓距离 pointPolygonTest()函数

double cv::pointPolygonTest(
   InputArray contour,    // 输入闭合轮廓(点集合)
    Point2f pt,            // 需要计算与轮廓距离的像素点
    bool measureDist );   // 计算距离是否有方向性,当为true,点在轮廓内部时,距离为正,当                            为false时,只检测点是否在轮廓内

7.2.6 凸包检测 convexHull()函数

对于形状复杂的物体,利用凸包,将二维平面上的点集最外层的点连接起来构成的凸多边形。

void cv::convexHull(InputArray points,          // 输入的二维点集
    OutputArray hull,           // 输出凸包顶点
    bool clockwise = false,     // 凸包点的排列方向(true顺时针/逆时针)
    bool returnPoints = true    // 返回值类型:true=输出凸包顶点的坐标,false=返回原数组索引
);

7.3 矩的计算

矩:描述图像特征的算子,应用于图像检索和识别,图像匹配、重建、压缩等领域。

7.3.1 计算几何矩与中心矩 moments()函数

几何矩的计算方式,,,零阶矩(可以用于计算某个形状的质心),,,一阶矩

质心的计算公式

图像中心矩的计算方式

图像归一化几何矩的计算方式

cv::Moments cv::moments(InputArray  array,          // 输入数据:二值图/轮廓点集
                        bool binaryImage = false    // 二值图像模式开关
);   //会返回一个moments类变量,例如Moments.m00是零阶矩

7.3.2 Hu矩 HuMoments()函数

Hu矩具有旋转、平移和缩放不变性,应用更广泛

void cv::HuMoments(
    const Moments& moments,    // 输入:cv::moments()计算得到的矩结构体
    OutputArray hu             // 输出:7个Hu矩值,存储为Mat/数组
);

7.3.3 基于Hu矩的轮廓匹配 matchShapes()函数

对比两个二维形状(轮廓 / 二值图)的相似程度,返回相似度度量值。返回值越小,两个形状越相似;

double cv::matchShapes(
    InputArray  contour1,      // 第一个形状(轮廓/单通道二值图)
    InputArray  contour2,      // 第二个形状(轮廓/单通道二值图)
    int         method,        // 相似度计算方法(3种可选)
    double      parameter      // 扩展参数,OpenCV官方定义为保留参数,固定传0
);

方法名称

cv2.CONTOURS_MATCH_I1

1方法

cv2.CONTOURS_MATCH_I2

2方法

cv2.CONTOURS_MATCH_I3

3方法

7.4 点集拟合

  • 寻找包围二维点集的三角形 minEnclosingTriangle()函数
double cv::minEnclosingTriangle(输入待寻找包围三角形的二维点集,输入拟合出的三角形的3个顶点坐标,vector<Point2f>类型的变量中)
  • 寻找包围二维点集的圆形 minEnclosingCircle()函数
void cv::minEnclosingCircle(输入待寻找的二维点集,圆形的圆心,圆形的半径)
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
using namespace cv;
using namespace std;

int main()
{
    Mat img(500, 500, CV_8UC3, Scalar::all(0));
    RNG& rng = theRNG();  //生成随机点

    while (true)
    {
        int i, count = rng.uniform(1, 101);
        vector<Point> points;
        //生成随机点
        for (i = 0; i < count; i++)
        {
            Point pt;
            pt.x = rng.uniform(img.cols / 4, img.cols * 3 / 4);
            pt.y = rng.uniform(img.rows / 4, img.rows * 3 / 4);
            points.push_back(pt);
        }

        //寻找包围点集的三角形 
        vector<Point2f> triangle;
        double area = minEnclosingTriangle(points, triangle);

        //寻找包围点集的圆形
        Point2f center;
        float radius = 0;
        minEnclosingCircle(points, center, radius);

        //创建两个图片用于输出结果
        img = Scalar::all(0);
        Mat img2;
        img.copyTo(img2);

        //在图像中绘制坐标点
        for (i = 0; i < count; i++)
        {
            circle(img, points[i], 3, Scalar(255, 255, 255), FILLED, LINE_AA);
            circle(img2, points[i], 3, Scalar(255, 255, 255), FILLED, LINE_AA);
        }
            
        //绘制三角形
        for (i = 0; i < 3; i++)
        {
            if (i==2)
            {
               ine(img, triangle[i], triangle[0], Scalar(255, 255, 255), 1, 16);
                break;
            }
          line(img, triangle[i], triangle[i + 1], Scalar(255, 255, 255), 1, 16);
        }

        //绘制圆形
       circle(img2, center, cvRound(radius), Scalar(255, 255, 255), 1, LINE_AA);

        //输出结果
        imshow("triangle", img);
        imshow("circle", img2);

        //按q键或者ESC键退出程序
        char key = (char)waitKey();
        if (key == 27 || key == 'q' || key == 'Q')
        {
            break;
        }
    }
    return 0;
}

7.5 QR二维码检测

  • 搜索二维码位置探测图形,三个“回”字区域detect()函数
bool cv::QRCodeDetector::detect(待检测图像,四个顶点坐标(数据类型为vector<Point>))
//有二维码返回true,否则false
  • 对二维码进行解码,提取其中的信息decode()函数
std::string cv::QRCodeDetector::decode(含有二维码图像,四个顶点坐标,经过校正和二值化的二维码)
  • 直接完成定位+解码 detectAndDecode()函数
std::string cv::QRCodeDetector::detectAndDecode(含有二维码图像,四个顶点坐标,经过校正和二值化的二维码)
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

int main()
{
    Mat img = imread("qrcode2.png");
    if (img.empty())
    {
        cout << "请确认图像文件名称是否正确" << endl;
        return -1;
    }
    Mat gray, qrcode_bin;
    cvtColor(img, gray, COLOR_BGR2GRAY);
    QRCodeDetector qrcodedetector;
    vector<Point> points;
    string information;
    bool isQRcode;
    isQRcode = qrcodedetector.detect(gray, points);  //识别二维码
    if (isQRcode)
    {
        //解码二维码
        information = qrcodedetector.decode(gray, points, qrcode_bin);
        cout << points << endl;  //输出二维码四个顶点的坐标
    }
    else
    {
        cout << "无法识别二维码,请确认图像时候含有二维码" << endl;
        return -1;
    }
    //绘制二维码的边框
    for (int i = 0; i < points.size(); i++)
    {
        if (i == points.size() - 1)
        {
            line(img, points[i], points[0], Scalar(0, 0, 255), 2, 8);
            break;
        }
        line(img, points[i], points[i + 1], Scalar(0, 0, 255), 2, 8);
    }
    //将解码内容输出到图片上
    putText(img, information.c_str(), Point(20, 30), 0, 1.0, Scalar(0, 0, 255), 2, 8);

    //利用函数直接定位二维码并解码
    string information2;
    vector<Point> points2;
    information2 = qrcodedetector.detectAndDecode(gray, points2);
    cout << points2 << endl;
    putText(img, information2.c_str(), Point(20, 55), 0, 1.0, Scalar(0, 0, 0), 2, 8);

    //输出结果
    imshow("result", img);
    namedWindow("qrcode_bin", WINDOW_NORMAL);
    imshow("qrcode_bin", qrcode_bin);
    waitKey(0);
    return 0;
}

7.6 总结

至此,第七章就结束了,望各位读者“一键三连”,再次感谢读到这里的各位读者。有任何问题欢迎评论区留言或者私信。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐