Learning Opencv 3 —— 十二章 图像分析

Opencv 图像分析

之前提到的图像变换都是将原图的某个像素点转换到目标图像的另一个位置,其结果本质上仍然是一幅图像。这里提到的图像分析则是将图像转换为另一种完全不同的形式,有可能是数组甚至是向量,比如离散傅立叶变换和 Hough 直线变换。最后将给出图像分割方法。

离散傅立叶变换

对于 N 个离散的点,一维离散傅立叶变换

而二维离散傅立叶变换

使用传统方法将需要 O(N^2),而如果使用快速傅立叶变换,复杂度将下降为 O(Nlog^N)

cv::dft(): The Discrete Fourier Transform

void cv::dft(
	cv::InputArray src, // Input array (real or complex)
	cv::OutputArray dst, // Output array
	int flags = 0, // for inverse, or other options
	int nonzeroRows = 0 // number of rows to not ignore
);

参数说明:

  • src:输入数组,必须为浮点数。如果为单通道则默认输入数据为实数,将使用一个名为 complex conjugate symmetrical (CCS) 的结构进行存储,从而节约存储空间。如果为双通道,则输入将直接理解为数据的实部和虚部,此时可能由于存在大量的零而浪费输入和输出数据的存储空间

单通道CCS的输出如下

  • flags:具体需要执行的操作
    • 默认执行 forward transform
    • cv::DFT_INVERSE 进行反变换
    • cv::DFT_SCALE 对结果使用 N^{-1} 或 (N_xN_y)^{-1} 进行缩放
    • cv::DFT_INV_SCALE 或者 cv::DFT_INVERSE_SCALE 直接执行翻转和缩放
    • cv::DFT_ROWS 将数据的每一行独立处理,分别计算
    • cv::DFT_COMPLEX_OUTPUT 以包含零,也就是不是 CCS 的形式给出计算结果 
    • cv::DFT_REAL_OUTPUT 对于满足复共轭对称性的输入,将产生维数小于输入数据的输出数据
  • nonzero_rows:在进行 DFT 时,由于Opencv算法倾向于处理 2^p3^q5^r 长度的数据,因此通常 cv::getOptimalDFTSize() 使用零对输入数据进行填充。但在计算时却可以指定填充的零行从而节省运算时间 

cv::idft(): The Inverse Discrete Fourier Transform

虽然 cv::dft() 可以实现傅立叶反变换,但是为了代码的可读性,还是建议使用独立的函数。

void cv::idft(
	cv::InputArray src, // Input array (real or complex)
	cv::OutputArray dst, // Output array
	int flags = 0, // for variations
	int nonzeroRows = 0 // number of rows to not ignore
);

其中参数与 cv::dft() 类似,其中如果 flags 设置为 cv::DFT_INVERSE,该函数将执行傅立叶变换

cv::mulSpectrums(): Spectrum Multiplication

其实现从 CCS 储存的数据中得到通常使用的数据

void cv::mulSpectrums(
	cv::InputArray src1, // Input array (ccs or complex)
	cv::InputArray src2, // Input array (ccs or complex)
	cv::OutputArray dst, // Result array
	int flags, // for row-by-row computation
	bool conj = false // true to conjugate src2
);

使用离散傅立叶变换进行卷积

由于时域系统的卷积等价于频域系统的乘积,因此可以通过分别对图像和系统进行傅立叶变换,在对变换结果的乘积进行反变换就能快速的得到卷积的结果。

// Example 12-1. Using cv::dft() and cv::idft() to accelerate
// the computation of convolutions 

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

using std::cout;
using std::endl;

int main(int argc, char** argv) {
    if (argc != 2) {
        cout    << "\nExample 12-1. Using cv::dft() and cv::idft() to accelerate the"
                << "\n computation of convolutions"
                << "\nFourier Transform\nUsage: "
                << argv[0] << " <path/imagename>\n" << endl;
        return -1;
    }

    cv::Mat A = cv::imread(argv[1], 0);

    if (A.empty()) {
        cout << "Cannot load " << argv[1] << endl;
        return -1;
    }

    cv::Size patchSize(100, 100);
    cv::Point topleft(A.cols / 2, A.rows /2);
    cv::Rect roi(topleft.x, topleft.y, patchSize.width, patchSize.height);
    cv::Mat B = A(roi);

    int dft_M = cv::getOptimalDFTSize(A.rows + B.rows - 1);
    int dft_N = cv::getOptimalDFTSize(A.cols + B.cols - 1);

    cv::Mat dft_A = cv::Mat::zeros(dft_M, dft_N, CV_32F);
    cv::Mat dft_B = cv::Mat::zeros(dft_M, dft_N, CV_32F);

    cv::Mat dft_A_part = dft_A(cv::Rect(0, 0, A.cols, A.rows));
    cv::Mat dft_B_part = dft_B(cv::Rect(0, 0, B.cols, B.rows));

    A.convertTo(dft_A_part, dft_A_part.type(), 1, -mean(A)[0]);
    B.convertTo(dft_B_part, dft_B_part.type(), 1, -mean(B)[0]);

    cv::dft(dft_A, dft_A, 0, A.rows);
    cv::dft(dft_B, dft_B, 0, B.rows);

    // set the last parameter to false to compute convolution instead of correlation
    //
    cv::mulSpectrums(dft_A, dft_B, dft_A, 0, true);
    cv::idft(dft_A, dft_A, cv::DFT_SCALE, A.rows + B.rows - 1);

    cv::Mat corr = dft_A(cv::Rect(0, 0, A.cols + B.cols - 1, A.rows + B.rows - 1));
    cv::normalize(corr, corr, 0, 1, cv::NORM_MINMAX, corr.type());
    cv::pow(corr, 3.0, corr);

    B ^= cv::Scalar::all(255);

    cv::imshow("Image", A);
    cv::imshow("ROI", B);

    cv::imshow("Correlation", corr);
    cv::waitKey();

    return 0;
}

通过使用此技巧,图像匹配的计算量从 O(N^2M^2) 下降到了 O(N^2log^N)

cv::dct(): The Discrete Cosine Transform

对于实数据,DFT 通过将数据分为两份分别赋值给假象数据的实部和虚部从而提高速度。离散余弦变换只处理实数据使用如下公式

其中其默认对计算结果进行归一化。

void cv::dct(
	cv::InputArray src, // Input array (even size)
	cv::OutputArray dst, // Output array
	int flags = 0 // for row-by-row or inverse
);

其参数与 DFT 函数基本类似,其中由于 DCT 在内部调用了 DFT,只是数据大小只是输入数据的一半,因此 DCT 的最佳数据长度

其中 N 是需要变换的数据长度

cv::idct(): The Inverse Discrete Cosine Transform

出于与 DFT 相同的原因,也存在反离散余弦变换

void cv::idct(
	cv::InputArray src, // Input array
	cv::OutputArray dst, // Output array
	int flags = 0, // for row-by-row computation
);

积分图像

Opencv 提供 cv::integral() 函数实现子区域图像的快速求和,其最突出的一个应用就是哈儿小波。而图像积分又分成求和、平方求和和斜和。

基于上述三个结果就可以组合出需要的结果,比如一块区域的和可以通过这下式计算

通过这种方式可以更快地进行平滑,近似梯度,计算均值、标准差以及针对不同的窗口大小进行 block correlations。

标准求和积分运算

void cv::integral(
	cv::InputArray image, // Input array, W x H
	cv::OutputArray sum, // Output sum results, (W + 1) x (H + 1)
	int sdepth = -1 // Results depth (e.g., cv::F32, cv::S32, cv::F64)
);

平方求和积分运算

void cv::integral(
	cv::InputArray image, // Input array
	cv::OutputArray sum, // Output sum results
	cv::OutputArray sqsum, // Output sum of squares results
	int sdepth = -1 // Results depth (e.g., cv::F32)
);

其中 sqsum 就是所需要的平方求和结果。

斜和积分运算

void cv::integral(
	cv::InputArray image, // Input array
	cv::OutputArray sum, // Output sum results
	cv::OutputArray sqsum, // Output sum of squares results
	cv::OutputArray tilted, // Output tilted sum results
	int sdepth = -1 // Results depth (e.g., cv::F32)
);

其中 tilted 就是所需要的斜和结果

Canny 边缘检测算子

void cv::Canny(
	cv::InputArray image, // Input single channel image
	cv::OutputArray edges, // Output edge image
	double threshold1, // "lower" threshold
	double threshold2, // "upper" threshold
	int apertureSize = 3, // Sobel aperture
	bool L2gradient = false // true=L2-norm (more accurate)
);

参数说明:

  • image:输入图像,必须是单通道
  • edges:输出的边缘图像,二值图像
  • threshold1, threshold2:低高阈值,大于高阈值确认为边缘,低于低阈值确认为非边缘,两者之间如果紧邻确认边缘则确认为边缘
  • apertureSize:算法内部 Sobel 微分算子的计算范围
  • L2gradient:选择是否使用更精确的 L2 范数还是更快的 L1 范数

霍夫变换

霍夫变换是一种从图像中找出直线,圆或者其他简单图像的方法。

霍夫线变换

基本原理是:针对每个点,对其斜率进行离散化,通常为 180,这样直线的斜率精度为 1,从而在斜率和截距的平面中绘制通过该直线的所有可能直线的图像(通常也为一条直线)。这样该直线就包含了所有通过该点的直线,之后对每个点进行相同的操作。接着在斜率截距平面上找出多条直线相交的点,即这几个点均在该交点所表示的直线上。最后通过设置阈值就可以找出图像中的直线了。同时,考虑斜率可能存在直线垂直的情况,因此通常不使用斜率截距而使用极坐标系的距离和方向角,不过这样转换之后的所有直线的轨迹将是一条曲线。

标准霍夫变换(standard hough transform,SHT)以及多尺度霍夫变换(multiscale Hough transform,MHT)

void cv::HoughLines(
	cv::InputArray image, // Input single channel image
	cv::OutputArray lines, // N-by-1 two-channel array
	double rho, // rho resolution (pixels)
	double theta, // theta resolution (radians)
	int threshold, // Unnormalized accumulator threshold
	double srn = 0, // rho refinement (for MHT)
	double stn = 0 // theta refinement (for MHT)
);

参数说明:

  • image:输入图像,必须为 8 为图像,不过算法将其作为二值图像进行处理(所有非零参数作为相同处理)
  • lines:N x 2 的二通道浮点数,其中 N 为检测到的直线数,而每一行分别给出了直线的距离(像素点)和方向角(弧度)
  • threshold:所找到的直线必须通过的点数
  • srn,stn:使用更高的分辨率求解直线,其中 MHT 最后的分辨率就是 rho = rho / srn 和 theta = theta / stn。如果设置为零,则进行 SHT

渐进概率霍夫变换

void cv::HoughLinesP(
	cv::InputArray image, // Input single channel image
	cv::OutputArray lines, // N-by-1 4-channel array
	double rho, // rho resolution (pixels)
	double theta, // theta resolution (radians)
	int threshold, // Unnormalized accumulator threshold
	double minLineLength = 0, // required line length
	double maxLineGap = 0 // required line separation
);

参数说明

  • lines:四通道 Vec4i 的数组,其中分别表示 x0, y0, x1, y1
  • minLineLength:返回的最短直线段长度
  • maxLineGap:共线线段能够被融合的最大距离

霍夫圆变换

霍夫圆变换可以使用与霍夫直线变换相同的思想,但由于表示圆需要三个值(圆心的横纵坐标和半径),因此也就需要更多的存储空间和更长的计算时间。而 Opencv 在实现时使用了霍夫梯度方法的技巧来提高效率。算法的基本流程如下:

  • 使用比如 Canny 的边缘检测算子得到边缘信息
  • 针对每个边缘点,使用 Sobel 计算 x 和 y 方向上的局部梯度
  • 沿着梯度上升的方向移动,并记录所有经过的位置
  • 在累加结果中选取超过阈值同时大于紧邻像素点的位置作为候选圆心
  • 对这些候选中心进行降序排列
  • 对每一个候选中心计算所有的边缘像素点到这个候选中心的距离,选择其中涵盖最多像素点的半径,并判断是否覆盖了足够的非零点
  • 重复上一个操作,直至没有满足要求的圆心存在

不过这样虽然提高的效率,但是也带来一些问题:

  • 其中使用了 Sobel 计算局部梯度来近似局部切线,这在绝大多数情况下是可行的。但是这可能使得结果中存在噪声
  • 对于每个圆都遍历了所有的非零点,而如果阈值设置过低,算法可能花费很长时间
  • 因为每个圆心只查找一个圆,因此如果存在同心圆将只返回一个
  • 同时,由于更大的圆通常具有更多的点,因此算法倾向于寻找同心圆中较大的圆
void cv::HoughCircles(
	cv::InputArray image, // Input single channel image
	cv::OutputArray circles, // N-by-1 3-channel or vector of Vec3f
	int method, // Always cv::HOUGH_GRADIENT
	double dp, // Accumulator resolution (ratio)
	double minDist, // Required separation (between lines)
	double param1 = 100, // Upper Canny threshold
	double param2 = 100, // Unnormalized accumulator threshold
	int minRadius = 0, // Smallest radius to consider
	int maxRadius = 0 // Largest radius to consider
);

参数说明:

  • image:输入图像,8 位图片,其将在内部调用 Sobel;而霍夫直线必须要求二值图像。
  • circles:矩阵或者向量。如果传入的是矩阵,那么将输出格式为 CV::F32C3,其中三通道分别为圆心和半径。如果是一个向量,那么输出格式为 std::vector<Vec3f>
  • method:一定要设置为 cv::HOUGH_GRADIENT
  • dp:累计图像的分辨率;如果设置为 1,将使用原始图像的分辨率;如果大于 1,将对原始图像进行缩小。dp 不能小于 1.
  • minDist:两个圆允许的最小距离
  • param1, param2:Canny 算法的上下阈值
  • minRadius, maxRadius:可接受圆的最小半径和最大半径。
// Example 12-2. Using cv::HoughCircles() to return a sequence of circles found in a
// grayscale image

#include <math.h>

#include <iostream>
#include <vector>

#include <opencv2/opencv.hpp>

using std::cout;
using std::endl;
using std::vector;

void help(char** argv) {
        cout  << "\nExample 12-1. Using cv::dft() and cv::idft() to accelerate the computation of convolutions"  
              << "\nHough Circle detect\nUsage: " << argv[0] <<" <path/imagename>\n" 
              << "Example:\n" << argv[0] << " ../stuff.jpg\n" << endl;
}

int main(int argc, char** argv) {
	help(argv);
    if (argc != 2) {
        return -1;
    }

    cv::Mat src, image;

    src = cv::imread(argv[1], 1);
    if (src.empty()) {
        cout << "Cannot load " << argv[1] << endl;
        return -1;
    }

    cv::cvtColor(src, image, cv::COLOR_BGR2GRAY);
    cv::GaussianBlur(image, image, cv::Size(5, 5), 0, 0);

    vector<cv::Vec3f> circles;
    cv::HoughCircles(image, circles, cv::HOUGH_GRADIENT, 2, image.cols/4);

    for (size_t i = 0; i < circles.size(); ++i) {
        cv::circle(src,
            cv::Point(cvRound(circles[i][0]), cvRound(circles[i][1])),
            cvRound(circles[i][2]),
            cv::Scalar(0, 0, 255),
            2,
            cv::LINE_AA);
    }

    cv::imshow("Hough Circles", src);
    cv::waitKey(0);

    return 0;
}

距离变换

一副图像的距离变换被定义为一张新图,其中的每个点的像素值被设置为其原始图像对应点到最近的像素值零点的距离。其输入通常为边缘检测结果,这样所有边缘点的距离变换都为零,而非边缘的都为非零值。

非标记距离变换

void cv::distanceTransform(
	cv::InputArray src, // Input image
	cv::OutputArray dst, // Result image
	int distanceType, // Distance metric to use
	int maskSize // Mask to use (3, 5, or see below)
);

参数说明:

  • dst:输出图像为 32 位浮点数,CV::F32
  • distanceType:cv::DIST_C(4 连通),cv::DIST_L1(8 连通), cv::DIST_L2(欧式距离)。
  • maskSize:3,5,cv::DIST_MASK_PRECISE。使用 cv::DIST_L1 或 cv::DIST_C 可以设置为 3。cv::DIST_L2 通常设置为 5 或者 cv::DIST_MASK_PRECISE

标记距离变换

void cv::distanceTransform(
	cv::InputArray src, // Input image
	cv::OutputArray dst, // Result image
	cv::OutputArray labels, // Connected component ids
	int distanceType, // Distance metric to use
	int maskSize, // (3, 5, or see below)
	int labelType = cv::DIST_LABEL_CCOMP // How to label
);

参数说明:

  • labels:和 dst 同维,同时对应非零像素点的 label 将被置为距离最近的 label。其将为典型的沃罗诺伊图
  • labelType:cv::DIST_LABEL_CCOMP(自动将连通的零区置为相同的 label) 或者 cv::DIST_LABEL_PIXEL(为每一个零值设置一个不同的 label)

分割

漫水法

int cv::floodFill(
	cv::InputOutputArray image, // Input image, 1 or 3 channels
	cv::Point seed, // Start point for flood
	cv::Scalar newVal, // Value for painted pixels
	cv::Rect* rect, // Output bounds painted domain
	cv::Scalar lowDiff = cv::Scalar(),// Maximum down color distance
	cv::Scalar highDiff = cv::Scalar(),// Maximum up color distance
	int flags // Local/global, and mask-only
);
int cv::floodFill(
	cv::InputOutputArray image, // Input w-by-h, 1 or 3 channels
	cv::InputOutputArray mask, // 8-bit, w+2-by-h+2 (Nc=1)
	cv::Point seed, // Start point for flood
	cv::Scalar newVal, // Value for painted pixels
	cv::Rect* rect, // Output bounds painted domain
	cv::Scalar lowDiff = cv::Scalar(), // Maximum down color distance
	cv::Scalar highDiff = cv::Scalar(), // Maximum up color distance
	int flags // Local/global, and mask-only
);

参数说明:

  • image:输入图像,可以是 8 位或者浮点数,通道数可以是 1 或者 3
  • mask:作为输入,其零值像素点标记了不需要算法处理的区域,输出则是最后图像分割的结果
  • seed,newVal,loDiff,upDiff:算法将 seed 指定的颜色范围在 loDiff 和 upDiff 之间的像素点标记为 newVal
  • flags
    • cv::FLOODFILL_FIXED_RANGE:每个像素点将于 seed 比较而不是其邻近的像素点
    • cv::FLOODFILL_MASK_ONLY:只修改 mask 的值,而不改变 image
    • 4,8:选择 4 连通还是 8 连通,比如 lags = 8 | cv::FLOODFILL_MASK_ONLY

具体 flags 的设置还比较复杂,其中低八位控制算法的连通性,而高八位设置填充的值。比如需要8连通,只填充固定距离,只修改 mask,使用 47 填充,flags 应该被设置为

flags = 8
	| cv::FLOODFILL_MASK_ONLY
	| cv::FLOODFILL_FIXED_RANGE
	| (47 << 8);

分水岭算法

在实际应用中,如果想要分割一幅图像但又没有任何分离背景的信息,就可以考虑分水岭算法。其首先检测图像的边缘并将其作为山峰,而将较为平坦的区域作为山谷,最后从山谷开始漫水实现图像分割。

void cv::watershed(
	cv::InputArray image, // Input 8-bit, three channels
	cv::InputOutputArray markers // 32-bit float, single channel
);

参数说明:

  • image:8 位或者三通道图像
  • makers:CV::S32 单通道整数图像。作为输入,非零位置表示了哪些像素点属于同一区域。作为输入,不同的区域将被赋值为正数,而边缘将被赋值为 -1。不过如果两个相邻的像素点被提前指定了类别,那边输出将不用 -1 进行分割

Grabcuts

Graphcuts 使用用户标记的前景和背景从而建立两类分布直方图,并假定未标记的前景和背景平滑且连通从而具有相似的分布,最终基于这些假设定义能量函数,算法最后的优化方向就是最小化指定的能量函数。

Grabcuts 是 Graphcuts 的改进,主要包括

  1. 使用高斯混合模型代替直方图模型是算法能够处理彩色图像的情况
  2. 使用迭代的方式最小化能量函数,使得计算结果更佳,同时用户自定义标记也更方便
  3. Graphcuts 允许用户只指定前景和背景中的一项,而 Graphcuts 用户必须同时指定
void cv::grabCut(
	cv::InputArray img,
	cv::InputOutputArray mask,
	cv::Rect rect,
	cv::InputOutputArray bgdModel,
	cv::InputOutputArray fgdModel,
	int iterCount,
	int mode = cv::GC_EVAL
);

参数说明:

  • img:输入图像
  • mask:输出标示。如果作为输入,将基于 mode 的选择。如果 mode 包含 cv::GC_INIT_WITH_MASK,则算法将 mask 作为算法的初始值。其必须为一个单通道 cv::U8 数据。其中具体内容的意义如下表。

  • rect:只有当未使用 mask 作为初始值,同时设置 cv::GC_INIT_WITH_RECT 时,表示矩形外部确定为背景,而内部可能为前景
  • bgdModel,fgdModel:算法计算的中间值,如果用户希望从某个中间点继续进行计算,则可以将之前算法的结果直接传入
  • itercount:Grabcuts 本质上依旧调用 Graphcuts,只是在迭代过程中有时会重复计算混合模型。其通常被设置为 10 -12,虽然通常需要根据实际情况进行调整。

均值漂移分割

均值漂移分割——cv::pyrMeanShiftFiltering() 查找空间中颜色分布的极值。与此类似的均值漂移算法则被用于跟踪与运动,其主要用于处理序列图像的分布。

给定多维数据的集合,其中可能包含 x, y, blue, green, red,而均值漂移一个空间上的扫描窗得到数据密度最高的块。不过由于不同的维度尺度差别很大,因此通常需要针对不同的维度指定不同的窗口大小,至少也应该针对位置和颜色分别指定两个大小。而过程中,图像金字塔也将被使用,高层的结果将在低层继续优化。

void cv::pyrMeanShiftFiltering(
	cv::InputArray src, // 8-bit, Nc=3 image
	cv::OutputArray dst, // 8-bit, Nc=3, same size as src
	cv::double sp, // Spatial window radius
	cv::double sr, // Color window radius
	int maxLevel = 1, // Max pyramid level
	cv::TermCriteria termcrit = TermCriteria(
		cv::TermCriteria::MAX_ITER | cv::TermCriteria::EPS,
		5,
		1
	)
);

参数说明:

  • src,dst:输入输出图像,必须为 8 位三通道图像
  • spatialRadius,colorRadius:定义了空间和颜色窗口的大小。对于 640 * 480 的图像,通常 spatialRadius=20,而 colorRadius=40
  • max_level:多少图像金字塔需要被使用。对于 640 * 480 的图像,通常设置为 2 或者 3
  • cv::TermCriteria:迭代终止条件
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值