找回密码
 立即注册
查看: 238|回复: 0

OpenCV学习笔记(二):图像处理

[复制链接]
发表于 2022-9-26 18:25 | 显示全部楼层 |阅读模式
几何变换

纺射变换

基本的纺射变换类型有平移、缩放、旋转 纺射变换的变换矩阵为: A = \left(\begin{array}{ccc} a_{11} &a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ 0 & 0 & 1 \end{array}\right) 其中最下方的((\begin{array}{cc}0 & 0 & 1\end{array})一般省略不写。
下面将首先介绍各个仿射变换的实现原理,最后给出这些变换的实现变化。


  • 平移:
平移过程为 \left(\begin{array}{c} \tilde{x} \\ \tilde{y} \\ 1 \end{array}\right) = \left(\begin{array}{ccc} 1 &0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{array}\right)\left(\begin{array}{c} x \\ y \\ 1 \end{array}\right) 其中 t_x 表示沿x轴移动, t_y 表示沿y轴移动。


  • 放大和缩小:
缩放变换的过程为 \left(\begin{array}{c} \tilde{x} \\ \tilde{y} \\ 1 \end{array}\right) = \left(\begin{array}{ccc} s_x &0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{array}\right)\left(\begin{array}{c} x \\ y \\ 1 \end{array}\right) 这里的缩放指的是坐标点与远点(0, 0)之间距离的缩放,即(\begin{array}{c} \tilde{x}, \tilde{y} \end{array}) = (\begin{array}{cc} s_x * x, s_y * y\end{array})。在上述式子中, s_x 即水平方向的缩放比例, s_y 即垂直方向的缩放比例。当 s_x=s_y 时便是等比例缩放。 如果想要以某一特定点为中心点进行缩放的话可以这样做 \left(\begin{array}{c} \tilde{x} \\ \tilde{y} \\ 1 \end{array}\right) = \left(\begin{array}{ccc} 1 &0 & x_0 \\ 0 & 1 & y_0 \\ 0 & 0 & 1 \end{array}\right)\left(\begin{array}{ccc} s_x &0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{array}\right)\left(\begin{array}{ccc} 1 &0 & -x_0 \\ 0 & 1 & -y_0 \\ 0 & 0 & 1 \end{array}\right)\left(\begin{array}{c} x \\ y \\ 1 \end{array}\right) 其意义是首先将原先的(0, 0)点移动到 (\begin{array}{c} x_0, y_0 \end{array}) ,进行缩放之后再平移回来。


  • 旋转:
图像绕(0, 0)点旋转过程为 \left(\begin{array}{c} \tilde{x} \\ \tilde{y} \\ 1 \end{array}\right) = \left(\begin{array}{ccc} \cos \alpha &\sin \alpha & 0 \\ -\sin \alpha & \cos \alpha & 0 \\ 0 & 0 & 1 \end{array}\right)\left(\begin{array}{c} x \\ y \\ 1 \end{array}\right) 其中 \alpha 是旋转角度, \alpha > 0 时逆时针旋转, \alpha < 0 时顺时针旋转。 同理如果想要以某一特定点为中心点旋转的话只要这样做 :
\left(\begin{array}{c} \tilde{x} \\ \tilde{y} \\ 1 \end{array}\right) = \left(\begin{array}{ccc} 1 &0 & x_0 \\ 0 & 1 & y_0 \\ 0 & 0 & 1 \end{array}\right)\left(\begin{array}{ccc} \cos \alpha &\sin \alpha & 0 \\ -\sin \alpha & \cos \alpha & 0 \\ 0 & 0 & 1 \end{array}\right)\left(\begin{array}{ccc} 1 &0 & -x_0 \\ 0 & 1 & -y_0 \\ 0 & 0 & 1 \end{array}\right)\left(\begin{array}{c} x \\ y \\ 1 \end{array}\right)


  • 计算仿射矩阵的方法:

    • 方程法:通过三个原始的坐标点和在某纺射矩阵之后的坐标点就可以直接求出纺射矩阵
    • 矩阵法:直接给定一个纺射矩阵或者通过矩阵的运算来给定纺射矩阵



  • 上述过程的代码实现
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    Mat img = imread("../image/5.jpg");
    // C++针对缩放和旋转有特定的api,这里首先演示缩放与旋转,之后演示通过纺射矩阵实现图像变换
    Mat img_narrow;
    Mat img_rotate;

    // 缩放
    // 获得图像的宽并乘以系数
    int high = img.rows;
    // 获得图像的长并乘以系数
    int weight = img.cols;
    // 按照绝对值对图像进行缩放,
    resize(img, img_narrow, Size(0.5 * weight, 0.3 * high));
    // 按照相对值方式对图像进行缩放,此时Size处输入Size()
    resize(img, img_narrow, Size(), 0.5, 0.3);

    // 旋转
    // ROTATE_90_CLOCKWISE = 0 顺时针旋转90度
    // ROTATE_180 = 1 旋转180度
    // ROTATE_90_COUNTERCLOCKWISE = 2 逆时针旋转90度
    rotate(img, img_rotate, ROTATE_90_CLOCKWISE);

    imshow("img", img);
    imshow("img_narrow", img_narrow);
    imshow("img_rotate", img_rotate);
    waitKey();  

    // 纺射变换
    Mat img_affine;

    // 解方程法
    // 创建两个二维浮点型的点数组
    Point2f srcTri[3];
    Point2f dstTri[3];
    // 创建一个2*3的图像矩阵
    Mat warp_mat(2, 3, CV_32FC1);

    // 初始点
    srcTri[0] = Point2f(0, 0);
    srcTri[1] = Point2f(img.cols - 1, 0);
    srcTri[2] = Point2f(0, img.rows - 1);

    // 经过变换的点
    dstTri[0] = Point2f(img.cols*0.0, img.rows*0.33);
    dstTri[1] = Point2f(img.cols*0.85, img.rows*0.25);
    dstTri[2] = Point2f(img.cols*0.15, img.rows*0.7);

    // 计算纺射矩阵
    warp_mat = getAffineTransform(srcTri, dstTri);

    // 纺射变换
    warpAffine(img, img_affine, warp_mat, Size(weight, high));

    // 矩阵法
    Mat img_affine2;

    Mat s = (Mat_<float>(3, 3)<<0.5, 0, 0, 0, 0.5, 0, 0, 0, 1); // 缩放矩阵
    Mat t = (Mat_<float>(3, 3)<<1, 0, 100, 0, 1, 200, 0, 0, 1); // 平移矩阵
    Mat warp_mat1(2, 3, CV_32FC1);
    gemm(s, t, 1, Mat(), 0, warp_mat1, 0); // 矩阵相乘

    // 由于warpaffine中矩阵参数只接收2*3的矩阵,所以需要删除最后一行
    Mat dst;
    int a=2; //需要删除的行 注意:需要删除的行要在warp_mat1的范围内
    for(int i=0; i<warp_mat1.rows; i++)
    {
        if(i!=a) //第i行不是需要删除的
        {
            dst.push_back(warp_mat1.row(i)); //把warp_mat1的第i行加到dst矩阵的后面
        }
    }
    warp_mat1=dst.clone();
    cout << warp_mat1 << endl;

    // 纺射变换
    warpAffine(img, img_affine2, warp_mat1, Size(weight, high));

    // 显示图片
    imshow("img", img);
    imshow("img_affine", img_affine);
    imshow("img_affine2", img_affine2);
    waitKey();

    return 0;
}
投影变换

投影变换即对图像进行视角变化,有如下公式计算 \left(\begin{array}{c} \tilde{x} \\ \tilde{y} \\ \tilde{z} \end{array}\right) = \left(\begin{array}{ccc} a_{11} &a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \end{array}\right)\left(\begin{array}{c} x \\ y \\ z \end{array}\right) 其中变换矩阵也可以写作 T = \left(\begin{array}{cc} T_1 &T_2  \\ T_3 & a_{33} \end{array}\right) 其中 T_1 为线性变换, T_2 为平移, T_3 为投影变换, a_{33} 通常为1。 所以我们需要找到4个点并其中任意三个不共线去确定变换矩阵 T ,或者也可以直接写出变换矩阵。
下面是代码实现:
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    Mat img = imread("../image/5.jpg");
    Mat img_change;
    // 定义四个点,分别是变换前和变换后
    Point2f src[4] = {Point2f(0, 0), Point2f(200, 0), Point2f(0, 200), Point2f(200, 200)};
    Point2f dst[4] = {Point2f(0, 20), Point2f(200, 20), Point2f(50, 150), Point2f(250, 170)};

    // 计算投影变换矩阵
    Mat P;
    P = getPerspectiveTransform(src, dst);

    // 进行投影变换
    warpPerspective(img, img_change, P, Size(img.cols, img.rows));
    // 显示图片
    imshow("img", img);
    imshow("img_change", img_change);
    waitKey();

    return 0;
}
图像金字塔

图像金字塔可以用来进行图像的缩放,主要是用于图像的分割,可以进行向上采样和向下采样。
下面是代码实现:
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    Mat img = imread("../image/5.jpg");
    Mat img_up;
    Mat img_down;

    pyrUp(img, img_up);   // 向上采样
    pyrDown(img, img_down);   // 向下采样
    // 显示图片
    imshow("img", img);
    imshow("img_up", img_up);
    imshow("img_down", img_down);
    waitKey();

return 0;

}
形态学操作

腐蚀与膨胀


  • 腐蚀即用一个结构元素扫描图像中的每一个像素,用结构元素中的每一个像素与其覆盖的像素做“与”操作,如果都为1,则像素为1,否则为0。主要作用是用来消除物体边界点,使目标缩小,可以消除小于结构元素的噪声点。
  • 膨胀就是用一个结构元素扫描图像中的每一个元素,用结构元素中每一个像素与其覆盖的像素做“与”操作,如果都为1,则像素为1,否则为0.主要作用是将物体接触的所有背景点合并到物体中,使目标增大,可以填补目标中的孔洞。
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    Mat img = imread("../image/6.jpg");
    Mat img_ero; // 腐蚀之后图像
    Mat img_dil; // 膨胀之后图像

    Mat kernel;

    // 矩形:MORPH_RECT = 0;
    // 交叉形:MORPH_CROSS = 1;
    // 椭圆形:MORPH_ELLIPSE = 2.
    // 生成核结构,参数分别是核形状、核大小、核中心
    kernel = getStructuringElement(MORPH_ELLIPSE, Size(10, 10), Point(-1, -1));
    erode(img, img_ero, kernel); // 腐蚀
    dilate(img, img_dil, kernel); // 膨胀

    // 显示图片
    imshow("img", img);
    imshow("img_erode", img_ero);
    imshow("img_dilate", img_dil);
    waitKey();

    return 0;
}
开闭操作


  • 开运算:先腐蚀后膨胀,作用是分离物体,消除小区域,特点是消除噪点,去掉小的干扰块而不影响原来的图像
  • 闭运算:先膨胀后腐蚀,作用是消除物体里的孔洞,可以填充闭合区域
代码见下面
礼帽和黑帽


  • 礼帽操作即原图像与开运算的结果图的差值,主要用来分离比临近点亮一些的斑块,当一幅图像有很大背景而微小图片比较有规律的时候可以进行背景提取
  • 黑帽操作为闭运算的结果图与原图像的差值,主要用来分离比临近点暗一些的斑块
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    Mat img = imread("../image/2.jpg");
    Mat img_close; // 腐蚀之后图像
    Mat img_open; // 膨胀之后图像
    Mat img_tophat; // 礼帽运算之后图像
    Mat img_black; // 黑帽运算之后图像
    Mat kernel;
    kernel = getStructuringElement(1, Size(50, 50), Point(-1, -1)); // 生成核结构

    // 礼帽运算:MORPH_TOPHAT = 5
    // 黑帽运算:MORPH_BLACKHAT = 6
    // 执行开运算:MORPH_OPEN = 2
    // 执行闭运算:MORPH_CLOSE = 3
    morphologyEx(img, img_close, MORPH_CLOSE, kernel); // 闭运算
    morphologyEx(img, img_open, MORPH_OPEN, kernel); // 开运算
    morphologyEx(img, img_tophat, MORPH_TOPHAT, kernel); // 礼帽运算
    morphologyEx(img, img_black, MORPH_BLACKHAT, kernel); // 黑帽运算

    // 显示图片
    imshow("img", img);
    imshow("img_open", img_open);
    imshow("img_close", img_close);
    imshow("img_tophat", img_tophat);
    imshow("img_black", img_black);
    waitKey();

    return 0;
}
图像平滑

图像噪声

常见的有椒盐噪声和高斯噪声:

  • 椒盐噪声即一种随机出现的白点和黑点;
  • 高斯噪声即噪声密度函数服从高斯分布的一种噪声。
如果要取出噪声的话就需要用滤波器,常见的滤波器有均值滤波、高斯滤波、中值滤波和双边滤波等。
均值滤波

即将图像中每一个位置的领域的平均值作为该位置的输出值。
高斯滤波

按照中心点作为原点,其他点按照其在二维高斯分布上的位置分配权重,这个点最终的像素即是他邻域像素值的加权平均,如果原图是彩色的话可以对RGB三个通道全部进行高斯平滑。
中值滤波

用像素点领域灰度值的中值替代该像素点的灰度值,对处理椒盐噪声很有用。
代码演示

#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    Mat img = imread("../image/2.jpg");
    Mat img_ang; // 中值滤波
    Mat img_gao; // 高斯滤波
    Mat img_mid; // 中值滤波  

    blur(img, img_ang, Size(5, 5)); // 均值滤波,分别是原图像,输出图像,核尺寸
    GaussianBlur(img, img_gao, Size(5, 5), 1, 1); // 高斯滤波,分别是原图像,输出图像,核尺寸,x轴标准差,y轴标准差
    medianBlur(img, img_mid, (5, 5)); //中值滤波

    // 显示图片
    imshow("img", img);
    imshow("img_ang", img_ang);
    imshow("img_gaosi", img_gao);
    imshow("img_mid", img_mid);
    waitKey();

    return 0;
}
直方图

直方图是一种图像中像素强度分布的图形表达方式,它统计了每一个强度值所具有的像素个数。 以下是使用官方提供的API calsHist()计算直方图。
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    Mat src, dst;
    src = imread("../image/3.jpg");

    // 将输入图像分为三个通道
    vector<Mat> bgr_planes;
    split(src, bgr_planes);

    // 设定像素取值范围
    int histSize = 256;
    float range[] = { 0,256 };
    const float *histRanges = { range };
    cout << range << "\t" <<*range << endl;
    cout << histRanges << "\t" << *histRanges << endl;

    // 三个通道分别计算直方图,绘图太麻烦了,就不绘制了
    Mat b_hist, g_hist, r_hist;
    calcHist(&bgr_planes[0], 1, 0, Mat(), b_hist, 1, &histSize, &histRanges, true, false);
    calcHist(&bgr_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, &histRanges, true, false);
    calcHist(&bgr_planes[2], 1, 0, Mat(), r_hist, 1, &histSize, &histRanges, true, false);

    return 0;
}
但是这种计算方式太过复杂,所以我们也可以直接手动去计算
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    Mat img;
    img = imread("../image/3.jpg");

    // 创建一个256*1的全零矩阵存放直方图
    Mat histogram = Mat::zeros(Size(256, 1), CV_32SC1);

    int rows = img.rows;
    int cols = img.cols;

    // 直接计算在某一点的像素点的个数
    for(int r = 0; r < rows; r++){
        for(int c = 0; c < cols; c++){
            int index = int(img.at<uchar>(r, c));
            histogram.at<int>(0, index) += 1;
        }
    }

    // histogram即为直方图,绘图太麻烦了,就不绘制了
    cout << histogram;

    return 0;
}
直方图均衡化

即把原始图像的灰度直方图从比较集中的某个灰度区间变成在更广泛的灰度范围的分布。直方图均衡化就是对图像进行非线性拉伸,重新分配图像像素值,使一定灰度范围内的像素数量大致相同。 这样可以提高图像整体的对比度,在X光图像中使用广泛,可以提高骨架结构的显示,另外在曝光过度和曝光不足的图像中也可以更好的突出细节。
自适应的直方图均衡化

直方图均衡化会导致可能会太暗或太亮,丢失很多信息,这时就要用到自适应的直方图均衡化。自适应的直方图均衡化将图像分成很多个小份(tiles),然后对每个小份的图像进行直方图均衡化。
同时具有对比度限制(bin限制),当某个直方图块的对比度过大时,将过大的tiles分散到其他的tiles中,使得其不至于出现过曝。
最后为了取出小块之间的边界,使用双线性差值,对每个小块进行拼接。
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    Mat img = imread("../image/5.jpg", 0);
    Mat img_zhi;

    // 直方图均衡化
    equalizeHist(img, img_zhi);

    imshow("img", img);
    imshow("img_zhi", img_zhi);
    waitKey(0);

    return 0;
}
边缘检测

边缘检测可以保留图像的重要的结构属性而大大减少图像的数据量,主要有两类边缘检测的算法

  • 基于搜索:通过寻找图像一阶导数中的最大值来检测边界,然后利用计算结果估计边缘的局部方向,通常采用梯度的方向,并利用此方向寻找局部梯度模的最大值,代表算法是Sobel算子和Scharr算子。
  • 基于零穿越:通过寻找图像二阶导数零穿越来寻找边界,代表算法是Laplacian算子。
卷积

首先要了解卷积的概念,卷积是分析数学中一种重要的运算,卷积的公式如下: (f * g)(x) = \int^\infty_{-\infty}f(t)g(x - t)\, dt  
这是卷积的连续形式,但是对于计算机图形学来说常常使用的是卷积的离散形式,在计算机图形学中的反映即首先定义一个核,名叫卷积核(kernel),如下图所示
\left(\begin{array}{ccc} 2 & 4 & 5 \\ 3 & 1 & 5 \\ 12 & 1 & 4\end{array}\right)(随便写的)
待处理图像可以看做是一个二维数组(灰色图像,如果是BRG图像的话是三通道的二维数组),然后在待处理图像的最外层补0,这是为了防止经过处理的图像的维数降低,然后将核置于待处理图像的每一个像素上,如上述定义的是一个3*3的卷积核,所以将卷积核所覆盖到的像素信息的值乘以在卷积核上对应的系数,加起来即为中心点的最终的像素值的大小。 将卷积核向右平移一个单位,再次进行上述操作,最终可以得到一幅经过处理之后的图像。
如下图所示:


Sobel检测算子

Sobel算子的特点是简单效率高,抗噪声能力强,但是检测不准确。 其主要思想是是求出导数的极大值所在的位置,对于图像上点x来说,其导数为 f^{'}(x) = \frac{f(x + 1) - f(x - 1)}{2}  所以当模板大小为3时,其水平卷积核和垂直卷积核分别是
G_x = \left[\begin{array}{ccc} -1 & 0 & +1 \\ -2 & 0 & +2 \\ -1 & 0 & +1\end{array}\right] , G_y = \left[\begin{array}{ccc} -1 & -2 & -1 \\ 0 & 0 & 0 \\ +1 & +2 & +1\end{array}\right]   
在图像的每一点,结合以上两个结果求出 G = \sqrt{G_x^2 + G_y^2} 当内核大小为3时,Sobel算子可能出现明显的误差,Scharr算子的计算方式和Sobel算子基本一样,但是结果更加准确,其卷积核为
G_x = \left[\begin{array}{ccc} -3 & 0 & +3 \\ -10 & 0 & +10 \\ -3 & 0 & +3\end{array}\right] , G_y = \left[\begin{array}{ccc} -3 & -10 & -3 \\ 0 & 0 & 0 \\ +3 & +10 & +3\end{array}\right]   
以下是代码实现:
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    Mat img = imread("../image/0.jpg", 0);
    resize(img, img, Size(), 0.5, 0.5);
    Mat img_sobel_x;
    Mat img_sobel_y;
    Mat img_sobel;

    // 计算Sobel卷积结果,其参数为(input, output, ddepth, dx, dy, ksize, scale, delta, borderType)
    // ddepth:代表输出图像深度,在8位输入图像的情况下,其导数可能会大于255或小于0,如果还是8位的话将导致导数截断,所以要采用16位
    // dx,dy:当dx不为0时水平方向核卷积,当dx为0,dy不为0时垂直方向核卷积
    // ksize:核的尺寸,默认为3,可以选1,3,5,7,如果ksize=-1,就会变成Scharr算子
    // scale:比例系数,默认是1
    // delta:平移系数,默认0
    // borderType:图像边界的模式
    Sobel(img, img_sobel_x, CV_16S, 1, 0, 3); // 水平方向的sobel
    Sobel(img, img_sobel_y, CV_16S, 0, 1, 3); // 垂直方向的sobel

    // 将图像转换为原来的uint8格式,否则无法显示
    convertScaleAbs(img_sobel_x, img_sobel_x);
    convertScaleAbs(img_sobel_y, img_sobel_y);

    // 将两个方向的组合起来
    addWeighted(img_sobel_x, 0.5, img_sobel_y, 0.5, 1, img_sobel);

    imshow("img", img);
    imshow("img_sobel_x", img_sobel_x);
    imshow("img_sobel_y", img_sobel_y);
    imshow("img_sobel", img_sobel);
    waitKey(0);

    return 0;
}
Laplacian算子

Laplacian是利用二阶导数来检测边缘,同样需要在两个方向上求导,如下图所示 \Delta src = \frac{\delta^2 src}{\delta x^2} + \frac{\delta^2 src}{\delta y^2}  所以不连续函数的二阶导数是 f^{''}(x) = f^{'}(x+1) - f{'}(x) = f(x + 1) + f(x - 1) - 2f(x) 所以使用的卷积核是 kernel = \left[\begin{array}{ccc} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0\end{array}\right]
以下是代码实现:
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    Mat img = imread("../image/8.jpg", 0);
    resize(img, img, Size(), 0.7, 0.7);
    Mat img_lap;

    Laplacian(img, img_lap, CV_16S, 1); // Laplacian转换,参数和Sobel基本差不多

    // 将图像转换为原来的uint8格式,否则无法显示
    convertScaleAbs(img_lap, img_lap);

    imshow("img", img);
    imshow("img_lap", img_lap);
    waitKey(0);

    return 0;
}
Canny边缘检测

Canny边缘检测是一种非常流行的边缘检测算法,被认为是最优的边缘检测算法
分别由4步构成:

  • 噪声去除:使用高斯滤波器进行噪声去除
  • 计算图像梯度:使用Sobel算子计算水平方向和竖直方向的一阶导数,根据这两幅梯度图找到边界的梯度和方向
  • 非极大值抑制:获得梯度方向和大小之后对整幅图像进行扫描,去除非边界上的点,对每个像素进行检查,观察这个点的梯度是不是周围具有相同梯度方向中最大的,是的话保留,否则抛弃
  • 滞后阈值,设置两个阈值,minVal和maxVal,如果灰度梯度高于maxVal的话是真边界,否则抛弃,介于两者之间的话看是否与真正的边界点相连,是就是,否则抛弃。
下面是代码实现:
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    Mat img = imread("../image/6.jpg", 1);
    resize(img, img, Size(), 0.7, 0.7);
    Mat img_canny;

    int lowThreshold = 0;
    int max_lowThreshold = 100;

    Canny(img, img_canny, lowThreshold, max_lowThreshold); // Canny检测

    imshow("img", img);
    imshow("img_canny", img_canny);
    waitKey(0);

    return 0;
}
边缘检测算子比较与适用场景



模板匹配和霍夫变换

模板匹配

模板匹配即在给定的图片中查找和模板最相似的区域,输入给定图片和模板,方法是按照滑窗的思路移动图片,计算图像的匹配度,将计算结果存储到结果矩阵中,查找最大值所在位置即是最匹配的地方,有三种方式

  • 平方差匹配:利用模板与图像之间的平方差进行匹配,最好结果是0
  • 相关匹配:利用模板与图像间的乘法进行匹配,数值越大匹配效果越好
  • 相关系数匹配:模板和图像之间的相关系数匹配,1表示完美匹配,-1表示最差匹配
#include <opencv2/core.hpp>
    #include <opencv2/highgui.hpp>
    #include <opencv2/imgproc.hpp>
    #include <iostream>
    using namespace std;
    using namespace cv;

    int main()
    {
    Mat img = imread("../image/12.jpg");
    Mat img_tem = imread("../image/12_T.png");
    Mat img_res;

    // 求出模板图像的高和宽
    int tem_w = img_tem.cols;
    int tem_h = img_tem.rows;

    // TM_SQDIFF:平方差匹配,越小越好
    // TM_CCORR:相关匹配,越大越好
    // TM_CCOEFF:利用相关系数匹配,接近1好(越大)
    matchTemplate(img, img_tem, img_res, TM_SQDIFF); // 模板匹配
    double min_val, max_val; // 模式识别匹配值,最大和最小
    Point min_loc, max_loc; // 匹配值对应的位置,最大和最小位置
    minMaxLoc(img_res, &min_val, &max_val, &min_loc, &max_loc); // 得到最大匹配值,最小匹配值和其对应的位置

    Point top_left = min_loc; // 最小匹配位置即左上角
    Point bottom_right(top_left.x + tem_w, top_left.y + tem_h); // 左上角加上模板的大小

    rectangle(img, top_left, bottom_right, Scalar(0, 255, 0)); // 用矩形将匹配的位置框起来

    imshow("img", img);
    waitKey(0);

    return 0;
    }
霍夫变换

霍夫变换主要用来提取图像中的园和直线。
在笛卡尔坐标系中,一条直线由两个点决定 y=kx+q ,可以写成 q=-kx+y ,以k为横坐标,q为纵坐标的坐标系即为霍夫空间,这样笛卡尔坐标系中的一条直线对应霍夫空间中就是一个 (k,q) 点。
同理笛卡尔坐标系中的一个点对应霍夫空间中的一个线。 对于笛卡尔坐标系中两个点来说两个点分别在霍夫空间中有一条直线,所以霍夫空间中有两条直线,两条直线有一个交点,这个交点即笛卡尔坐标系两个点的连线。
如果在笛卡尔坐标系中有多个点,他们可能共线,可能不共线,如果共线的话,他们在霍夫空间中必交于一点,如笛卡尔坐标系中三个点共线,在霍夫空间中就有3条线交于一点。
在霍夫空间中交点的线越多,在笛卡尔坐标系中这条线上面的点就越多,这条线就越长,所以可以通过检测霍夫空间中的交点来检测直线。
为了防止奇异点的存在,一般都是在极坐标系中进行霍夫变换,这时的霍夫空间不是(k,q)的空间,而是 (\rho , \theta ) 的空间, \rho 是原点到直线的垂直距离, \theta 是直线的垂线与横轴顺时针方向的夹角,可以进行如图的转换


霍夫变换的实现流程如下

  • 创建一个(\rho , \theta )的数组,叫做累加器,初始所有值是1。如果希望角度的精度是1度,那么\theta的大小为360,如果希望精度是像素级别,那么\rho应该和图像对角线的距离相等。
  • 取直线上一点 (x,y) 代入极坐标公式中,遍历\theta的值,分别求出\rho值,然后对应的地方加1
  • 找出累加器的最大值,找到对应的(\rho , \theta ),就可以在图像中用直线表示出来
下面是代码实现
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    Mat img = imread("../image/18.png", 0);
    Mat img_huo = imread("../image/18.png");

    Canny(img, img, 0, 100); // 将图像化为单通道8位图,这样才可以进行霍夫变换
    vector<Vec2f> lines; // 累加器

    // 霍夫变换api,参数为(input, output, rho, theta, threshold)
    // 分别是rho的精度,theta的精度和阈值
    // 2000年有人提出概率霍夫变换,效率比霍夫变换更好,精度也差不了哪里去,可以用概率霍夫变换代替霍夫变换,api:HoughLinesP()
    HoughLines(img, lines, 1.0, CV_PI/180, 450);

    // 在原图像中画出识别到的直线
    for (size_t i = 0; i < lines.size(); i++)
    {
        float rho = lines[0], theta = lines[1];
        Point pt1, pt2;
        double a = cos(theta), b = sin(theta);
        double x0 = a*rho, y0 = b*rho;
        pt1.x = cvRound(x0 + 2000 * (-b)); //把浮点数转化成整数
        pt1.y = cvRound(y0 + 2000 * (a));
        pt2.x = cvRound(x0 - 2000 * (-b));
        pt2.y = cvRound(y0 - 2000 * (a));
        line(img_huo, pt1, pt2, Scalar(255), 2);
    }

    imshow("img", img);
    imshow("img_huo", img_huo);
    waitKey(0);

    return 0;
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-25 01:02 , Processed in 0.091104 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表