OpenCV 学习(直线拟合)
最后更新于:2022-04-01 11:25:37
## OpenCV 学习(直线拟合)
Hough 变换可以提取图像中的直线。但是提取的直线的精度不高。而很多场合下,我们需要精确的估计直线的参数,这时就需要进行直线拟合。
直线拟合的方法很多,比如一元线性回归就是一种最简单的直线拟合方法。但是这种方法不适合用于提取图像中的直线。因为这种算法假设每个数据点的X 坐标是准确的,Y 坐标是带有高斯噪声的。可实际上,图像中的每个数据点的XY 坐标都是带有噪声的。
下面就来讲讲适用于提取图像中直线的直线拟合算法。
一个点 (xi,yi) 到直线的距离用 ri 来表示。
所谓直线拟合,就是找到一条直线,使得:
∑ρ(ri)
最小。
ρ(r) 是距离函数。ρ(r) 函数取不同的形式,对应不同的直线拟合方法。OpenCV 中支持 6 种不同的ρ(r) 函数形式。分别是:
CV_DIST_L2
ρ(r)=r22
这种方法是以距离平方和为拟合判据。也就是常见的最小二乘拟合算法,运行速度也最快。但是这个算法也有个很大的问题,就是当干扰点离直线较远时,一个干扰点就可能将整条拟合直线拉偏了。简单的说就是对干扰点的鲁棒性不够。所以后来又提出了其他的函数。
CV_DIST_L1
ρ(r)=r
CV_DIST_L12
ρ(r)=2(1+r22−−−−−−√−1)
CV_DIST_FAIR
ρ(r)=C2(rC−log(1+rC))
其中 C = 1.3998
CV_DIST_WELSCH
ρ(r)=C22(1−exp(−(rC)2))
其中 C = 2.9846
CV_DIST_HUBER
ρ(r)={r22C(r−C2)if r<C,otherwise.
其中 C = 1.345
后面这 5 种函数我知道第一种,其他的不知道是怎么来的。OpenCV 的帮助文档给出了一个链接:[M-estimator](https://en.wikipedia.org/wiki/M-estimator)
但是这个页面也被墙了。
下面来说说 OpenCV 提供的直线拟合函数。函数原型如下:
~~~
void fitLine( InputArray points,
OutputArray line,
int distType,
double param,
double reps,
double aeps );
~~~
distType 指定拟合函数的类型,可以取 CV_DIST_L2、CV_DIST_L1、CV_DIST_L12、CV_DIST_FAIR、CV_DIST_WELSCH、CV_DIST_HUBER。
param 就是 CV_DIST_FAIR、CV_DIST_WELSCH、CV_DIST_HUBER 公式中的C。如果取 0,则程序自动选取合适的值。
reps 表示直线到原点距离的精度,建议取 0.01。
aeps 表示直线角度的精度,建议取 0.01。
计算出的直线信息存放在 line 中,为 cv::Vec4f 类型。line[0]、line[1] 存放的是直线的方向向量。line[2]、line[3] 存放的是直线上一个点的坐标。
如果直线用 y=kx+b 来表示,那么 k = line[1]/line[0],b = line[3] - k * line[2]。
如果直线用 ρ=xcosθ+ysinθ 来表示, 那么 θ=arctank+π2
下面是个测试图像:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db6b28d6.jpg "")
图像中有一条直线和一些干扰图案。
下面的代码可以从图像中提取出需要的坐标点。
~~~
std::vector<cv::Point> getPoints(cv::Mat &image, int value)
{
int nl = image.rows; // number of lines
int nc = image.cols * image.channels();
std::vector<cv::Point> points;
for (int j = 0; j < nl; j++)
{
uchar* data = image.ptr<uchar>(j);
for (int i = 0; i < nc; i++)
{
if(data[i] == value)
{
points.push_back(cv::Point(i, j));
}
}
}
return points;
}
~~~
下面的代码可以在图中画一条直线。
~~~
void drawLine(cv::Mat &image, double theta, double rho, cv::Scalar color)
{
if (theta < PI/4. || theta > 3.*PI/4.)// ~vertical line
{
cv::Point pt1(rho/cos(theta), 0);
cv::Point pt2((rho - image.rows * sin(theta))/cos(theta), image.rows);
cv::line( image, pt1, pt2, cv::Scalar(255), 1);
}
else
{
cv::Point pt1(0, rho/sin(theta));
cv::Point pt2(image.cols, (rho - image.cols * cos(theta))/sin(theta));
cv::line(image, pt1, pt2, color, 1);
}
}
~~~
下面的代码是程序的主体。
~~~
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
cv::Mat image = imread("c:\\line_test.png", cv::IMREAD_GRAYSCALE);
std::vector<cv::Point> points = getPoints(image, 0);
cv::Vec4f line;
cv::fitLine(points,
line,
CV_DIST_HUBER ,
0,
0.01,
0.01);
double cos_theta = line[0];
double sin_theta = line[1];
double x0 = line[2], y0 = line[3];
double phi = atan2(sin_theta, cos_theta) + PI / 2.0;
double rho = y0 * cos_theta - x0 * sin_theta;
std::cout << "phi = " << phi / PI * 180 << std::endl;
std::cout << "rho = " << rho << std::endl;
drawLine(image, phi, rho, cv::Scalar(0));
double k = sin_theta / cos_theta;
double b = y0 - k * x0;
double x = 0;
double y = k * x + b;
std::cout << k << std::endl;
std::cout << b << std::endl;
//cv::line(image, Point(x0,y0), Point(x,y), cv::Scalar(255), 1);
imshow("", image);
return a.exec();
}
~~~
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db6c8af1.jpg "")
如果直线拟合类型选择 CV_DIST_L2。那么效果就没这么好了。代码不贴了,就贴个结果。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db6de85e.jpg "")
OpenCV 学习(Hough 变换提取直线)
最后更新于:2022-04-01 11:25:35
## OpenCV 学习(Hough 变换提取直线)
在机器视觉应用中,我们经常要提取图像中的各种特征,最基本的特征就是图像中的线条、拐角等。这篇笔记就来讲讲如何提取图像中的直线。这里使用的方法叫做 Hough 变换。
Hough 变换这个名称最早是在 Richard Duda 和 Peter Hart 两人于 1972 年合写的发表于 Comm. ACM 文章 《Use of the Hough Transformation to Detect Lines and Curves in Pictures》 中提出的。 大家可能会好奇,这俩人没一个叫 Hough,为啥这个变换叫 Hough 变换呢。这还要追溯到更早的年代,1962 年 Paul Hough 申请了一个美国专利,专利的名称叫做 《Method and means for recognizing complex patterns》,这个专利中提出了 Hough 变换基本方法。不过 1962 年那时还没有所谓的机器视觉这个学科,计算机也不是一般人能见到的。所以这个专利并没有受到特别的重视。 Richard Duda 和 Peter Hart 不知是如何翻到这个 10 年前的专利,并敏锐的发现了它的价值,并将其用于机器视觉领域。从此就有了大名鼎鼎的 Hough 变换。
关于 Hough 更详细的历史发展大家可以参考:
[https://en.wikipedia.org/wiki/Hough_transform](https://en.wikipedia.org/wiki/Hough_transform)
Hough 变换的原理介绍也可以参考上面的 wiki。简单的说 Hough 变换采用的是一种证据收集的方式,遍历一幅图像上所有的直线位置,哪条直线上的特征点(证据)更多,哪条直线就更可能是我们希望找到的直线。
这里不准备详细介绍Hough 变换的原理。但是Hough 变换如何表示图像中的直线还是要介绍的。否则,我们都不知道如何使用获得的结果。
Hough 变换时,我们采用参数方程来表示直线。
ρ=xcosθ+ysinθ
ρ 的几何含义是直线到图像原点的距离。 θ 是直线的法向方向与 x 轴的夹角。 θ=0 表示的是垂直的直线,例如下图中直线 1。 θ=π/2 表示的是水平的直线,例如下图中直线 5。 θ 的取值范围是 0 到 π。由于限制了θ的取值范围,ρ 既可以为正也可以为负。比如下图中直线2,θ=0.8π ,ρ 为负。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db65bb5e.jpg "")
OpenCV 中提供了两个Hough变换提取直线的函数。
1. cv::HoughLines 函数
1. cv::HoughLinesP 函数
下面分别介绍。
### cv::HoughLines 函数
这个函数采用最原始的Hough 变换来计算直线的位置。
~~~
void HoughLines( InputArray image,
OutputArray lines,
double rho, // rho 的步长
double theta, // 角度步长
int threshold, // 阈值
double srn=0,
double stn=0 );
~~~
输入图像必须是单通道的。输出的直线存在一个
~~~
std::vector<cv::Vec2f> lines;
~~~
首先给出一个简单的测试图片。这个图片上有四条直线。没有其他的干扰物体。这属于最基本的情形。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db670816.jpg "")
下面是个测试代码。
~~~
#include <QCoreApplication>
#include <math.h>
#define PI 3.14159265358979
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
cv::Mat image = cv::imread("c:\\test.png");
cv::Mat contours;
cv::cvtColor(image, contours, cv::COLOR_BGR2GRAY);
cv::bitwise_not(contours, contours);
//cv::Canny(image, contours, 155, 350);
std::vector<cv::Vec2f> lines;
cv::HoughLines(contours, lines, 1, PI/180, 180);
//cv::imshow("cany",contours );
std::vector<cv::Vec2f>::const_iterator it= lines.begin();
while (it!=lines.end())
{
float rho= (*it)[0]; // first element is distance rho
float theta= (*it)[1]; // second element is angle theta
if (theta < PI/4. || theta > 3.*PI/4.)// ~vertical line
{
// point of intersection of the line with first row
cv::Point pt1(rho/cos(theta), 0);
// point of intersection of the line with last row
cv::Point pt2((rho - image.rows * sin(theta))/cos(theta), image.rows);
// draw a white line
cv::line( image, pt1, pt2, cv::Scalar(255), 1);
}
else
{ // ~horizontal line
// point of intersection of the
// line with first column
cv::Point pt1(0,rho/sin(theta));
// point of intersection of the line with last column
cv::Point pt2(image.cols, (rho - image.cols * cos(theta))/sin(theta));
// draw a white line
cv::line(image, pt1, pt2, cv::Scalar(255), 1);
}
++it;
}
cv::imshow("", image);
return a.exec();
}
~~~
输出结果如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db68455e.jpg "")
这几条线找的还是蛮准的。
### cv::HoughLinesP 函数
与 cv::HoughLines函数不同, cv::HoughLinesP 函数可以提取线段。
输出的直线存在一个
~~~
std::vector<cv::Vec4i> lines;
~~~
中。
cv::Vec4i 的四个整数分别是线段的起点和终点坐标。
~~~
void HoughLinesP( InputArray image,
OutputArray lines,
double rho, // rho 的步长
double theta, // 角度的步长,单位是度
int threshold, // 阈值
double minLineLength=0, // 线段的最小长度
double maxLineGap=0 ); // 线段之间的最小距离
~~~
下面把 HoughLinesP 函数封装到一个类中。
~~~
class LineFinder
{
private:
cv::Mat img; // original image
std::vector<cv::Vec4i> lines;
double deltaRho;
double deltaTheta;
int minVote;
double minLength; // min length for a line
double maxGap; // max allowed gap along the line
public:
// Default accumulator resolution is 1 pixel by 1 degree
// no gap, no mimimum length
LineFinder() : deltaRho(1),
deltaTheta(PI/180),
minVote(10),
minLength(0.),
maxGap(0.) {}
// Set the resolution of the accumulator
void setAccResolution(double dRho, double dTheta)
{
deltaRho= dRho;
deltaTheta= dTheta;
}
// Set the minimum number of votes
void setMinVote(int minv)
{
minVote= minv;
}
// Set line length and gap
void setLineLengthAndGap(double length, double gap)
{
minLength= length;
maxGap= gap;
}
// Apply probabilistic Hough Transform
std::vector<cv::Vec4i> findLines(cv::Mat& binary)
{
lines.clear();
cv::HoughLinesP(binary, lines, deltaRho, deltaTheta, minVote, minLength, maxGap);
return lines;
}
// Draw the detected lines on an image
void drawDetectedLines(cv::Mat &image, cv::Scalar color = cv::Scalar(255, 255, 255))
{
// Draw the lines
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, 2);
++it2;
}
}
};
~~~
用这个类实现图中线段的检测。
~~~
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
cv::Mat image = cv::imread("c:\\test.png");
cv::Mat contours;
cv::cvtColor(image, contours, cv::COLOR_BGR2GRAY);
cv::bitwise_not(contours, contours);
//cv::Canny(image, contours, 155, 350);
LineFinder finder;
// Set probabilistic Hough parameters
finder.setLineLengthAndGap(100, 20);
finder.setMinVote(80);
// Detect lines and draw them
std::vector<cv::Vec4i> lines = finder.findLines(contours);
finder.drawDetectedLines(image, cv::Scalar(0, 0, 255));
cv::namedWindow("Detected Lines with HoughP");
cv::imshow("Detected Lines with HoughP",image);
return a.exec();
}
~~~
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db69a53f.jpg "")
OpenCV 学习笔记(模板匹配)
最后更新于:2022-04-01 11:25:32
## OpenCV 学习笔记(模板匹配)
模板匹配是在一幅图像中寻找一个特定目标的方法之一。这种方法的原理非常简单,遍历图像中的每一个可能的位置,比较各处与模板是否“相似”,当相似度足够高时,就认为找到了我们的目标。
在 OpenCV 中,提供了相应的函数完成这个操作。
matchTemplate 函数:在模板和输入图像之间寻找匹配,获得匹配结果图像
minMaxLoc 函数:在给定的矩阵中寻找最大和最小值,并给出它们的位置
在具体介绍这两个函数之前呢,我们还要介绍一个概念,就是如何来评价两幅图像是否“相似”。
OpenCV 提供了 6 种计算两幅图像相似度的方法。
1. 差值平方和匹配 CV_TM_SQDIFF
1. 标准化差值平方和匹配 CV_TM_SQDIFF_NORMED
1. 相关匹配 CV_TM_CCORR
1. 标准相关匹配 CV_TM_CCORR_NORMED
1. 相关匹配 CV_TM_CCOEFF
1. 标准相关匹配 CV_TM_CCOEFF_NORMED
下面就分别来介绍。首先,先给出几个符号:
T(x,y) 用来表示我们的模板。I(x,y) 是我们的目标图像。 R(x,y) 是用来描述相似度的函数。
### 差值平方和匹配 CV_TM_SQDIFF
这类方法利用图像与模板各个像素差值的平方和来进行匹配,最好匹配为 0。 匹配越差,匹配值越大。
R(x,y)=∑x′,y′(T(x′,y′)−I(x+x′,y+y′))2
### 标准化差值平方和匹配 CV_TM_SQDIFF_NORMED
这个方法其实和差值平方和算法是类似的。只不过对图像和模板进行了标准化操作。
R(x,y)=∑x′,y′(T(x′,y′)−I(x+x′,y+y′))2∑x′,y′T(x′,y′)2∑x′,y′I(x+x′,y+y′)2−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−√
这种标准化操作可以保证当模板和图像各个像素的亮度都乘上了同一个系数时,相关度不发生变化。
也就是说当 I(x,y)和T(x,y) 变为k×I(x,y)和k×T(x,y) 时,R(x,y)不发生变化。
### 相关匹配 CV_TM_CCORR
这类方法采用模板和图像的互相关计算作为相似度的度量方法,所以较大的数表示匹配程度较高,0标识最坏的匹配效果。
R(x,y)=∑x′,y′(T(x′,y′)×I(x+x′,y+y′))
### 标准化相关匹配 CV_TM_CCORR_NORMED
这个方法和 标准化差值平方和匹配 类似,都是去除了亮度线性变化对相似度计算的影响。可以保证图像和模板同时变亮或变暗k倍时结果不变。
R(x,y)=∑x′,y′(T(x′,y′)×I(x+x′,y+y′))∑x′,y′T(x′,y′)2∑x′,y′I(x+x′,y+y′)2−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−√
### 相关匹配 CV_TM_CCOEFF
这种方法也叫做相关匹配,但是和上面的 CV_TM_CCORR 匹配方法还是有不通过的。简单的说,这里是把图像和模板都减去了各自的平均值,使得这两幅图像都没有直流分量。
T′(x,y)=T(x,y)−∑x′,y′T(x′,y′)w×hI′(x,y)=I(x,y)−∑x′,y′I(x′,y′)w×hR(x,y)=∑x′,y′(T′(x′,y′)×I′(x+x′,y+y′))
### 标准相关匹配 CV_TM_CCOEFF_NORMED
这是 OpenCV 支持的最复杂的一种相似度算法。这里的相关运算就是数理统计学科的相关系数计算方法。具体的说,就是在减去了各自的平均值之外,还要各自除以各自的方差。经过减去平均值和除以方差这么两步操作之后,无论是我们的待检图像还是模板都被标准化了,这样可以保证图像和模板分别改变光照亮不影响计算结果。计算出的相关系数被限制在了 -1 到 1 之间,1 表示完全相同,-1 表示两幅图像的亮度正好相反,0 表示两幅图像之间没有线性关系。
T′(x,y)=T(x,y)−1w×h∑x′,y′T(x′,y′)∑x′,y′T(x′,y′)2−−−−−−−−−−−−−√I′(x,y)=I(x,y)−1w×h∑x′,y′I(x′,y′)∑x′,y′I(x′,y′)2−−−−−−−−−−−−−√R(x,y)=∑x′,y′(T′(x′,y′)×I′(x+x′,y+y′))
下面给个例子,我们的测试图像如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db60b163.jpg "")
我们的模板如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db627c30.jpg "")
程序中会用到 OpenCV 的函数包括:
~~~
void matchTemplate( InputArray image, InputArray templ,
OutputArray result, int method );
~~~
其中 result 是一个矩阵,返回每一个点匹配的结果。
~~~
void minMaxLoc(InputArray src, CV_OUT double* minVal,
CV_OUT double* maxVal=0, CV_OUT Point* minLoc=0,
CV_OUT Point* maxLoc=0, InputArray mask=noArray());
~~~
这个函数可以在一个矩阵中寻找最大点或最小点,并将位置返回回来。
下面是完整的测试程序。
~~~
#include <QCoreApplication>
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
using namespace cv;
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
cv::Mat image = imread("D:/test.png", cv::IMREAD_COLOR );
cv::Mat templateImage = imread("D:/template.png", cv::IMREAD_COLOR);
int result_cols = image.cols - templateImage.cols + 1;
int result_rows = image.rows - templateImage.rows + 1;
cv::Mat result = cv::Mat( result_cols, result_rows, CV_32FC1 );
cv::matchTemplate( image, templateImage, result, CV_TM_SQDIFF );
double minVal, maxVal;
cv::Point minLoc, maxLoc, matchLoc;
cv::minMaxLoc( result, &minVal, &maxVal, &minLoc, &maxLoc, Mat() );
matchLoc = minLoc;
cv::rectangle( image, cv::Rect(matchLoc, cv::Size(templateImage.cols, templateImage.rows) ), Scalar(0, 0, 255), 2, 8, 0 );
imshow("", image);
return a.exec();
}
~~~
输出结果是这样的。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db63d853.jpg "")
其实上面的代码还可以封装一下。
~~~
double match(cv::Mat image, cv::Mat tepl, cv::Point &point, int method)
{
int result_cols = image.cols - tepl.cols + 1;
int result_rows = image.rows - tepl.rows + 1;
cv::Mat result = cv::Mat( result_cols, result_rows, CV_32FC1 );
cv::matchTemplate( image, tepl, result, method );
double minVal, maxVal;
cv::Point minLoc, maxLoc;
cv::minMaxLoc( result, &minVal, &maxVal, &minLoc, &maxLoc, Mat() );
switch(method)
{
case CV_TM_SQDIFF:
case CV_TM_SQDIFF_NORMED:
point = minLoc;
return minVal;
break;
default:
point = maxLoc;
return maxVal;
break;
}
}
~~~
利用这个封装代码,我们可以把所有的匹配方法都实验一下。并且比较一下计算速度。
~~~
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
cv::Mat image = imread("D:/test.png", cv::IMREAD_COLOR );
cv::Mat tepl = imread("D:/template.png", cv::IMREAD_COLOR);
cv::Point matchLoc;
double value;
int elapse;
QTime t;
t.start();
value = match(image, tepl, matchLoc, CV_TM_SQDIFF);
elapse = t.elapsed();
qDebug("CV_TM_SQDIFF Time elapsed: %d ms", elapse);
qDebug() << value;
t.start();
value = match(image, tepl, matchLoc, CV_TM_SQDIFF_NORMED);
elapse = t.elapsed();
qDebug("CV_TM_SQDIFF_NORMED Time elapsed: %d ms", elapse);
qDebug() << value;
t.start();
value = match(image, tepl, matchLoc, CV_TM_CCORR);
elapse = t.elapsed();
qDebug("CV_TM_CCORR Time elapsed: %d ms", elapse);
qDebug() << value;
t.start();
value = match(image, tepl, matchLoc, CV_TM_CCORR_NORMED);
elapse = t.elapsed();
qDebug("CV_TM_CCORR_NORMED Time elapsed: %d ms", elapse);
qDebug() << value;
t.start();
value = match(image, tepl, matchLoc, CV_TM_CCOEFF);
elapse = t.elapsed();
qDebug("CV_TM_CCOEFF Time elapsed: %d ms", elapse);
qDebug() << value;
t.start();
value = match(image, tepl, matchLoc, CV_TM_CCOEFF_NORMED);
elapse = t.elapsed();
qDebug("CV_TM_CCOEFF_NORMED Time elapsed: %d ms", elapse);
qDebug() << value;
cv::rectangle( image, cv::Rect(matchLoc, cv::Size(tepl.cols, tepl.rows) ), Scalar(0, 0, 255), 2, 8, 0 );
imshow("", image);
return a.exec();
}
~~~
输出结果如下:
~~~
CV_TM_SQDIFF Time elapsed: 734 ms
4
CV_TM_SQDIFF_NORMED Time elapsed: 699 ms
1.43391e-08
CV_TM_CCORR Time elapsed: 638 ms
2.78957e+08
CV_TM_CCORR_NORMED Time elapsed: 710 ms
1
CV_TM_CCOEFF Time elapsed: 721 ms
2.30675e+08
CV_TM_CCOEFF_NORMED Time elapsed: 759 ms
1
~~~
如果我们先将图像都转换为灰度图,那么计算速度会快很多。
~~~
CV_TM_SQDIFF Time elapsed: 249 ms
12
CV_TM_SQDIFF_NORMED Time elapsed: 246 ms
1.29052e-07
CV_TM_CCORR Time elapsed: 208 ms
9.29857e+07
CV_TM_CCORR_NORMED Time elapsed: 242 ms
1
CV_TM_CCOEFF Time elapsed: 246 ms
7.68916e+07
CV_TM_CCOEFF_NORMED Time elapsed: 281 ms
1
~~~
基本缩短到了 1/3 。所以,如果可以用灰度图来计算,就不要用彩色图。
我们还可以去掉模板大小对匹配度的影响:
~~~
double match(cv::Mat image, cv::Mat tepl, cv::Point &point, int method)
{
int result_cols = image.cols - tepl.cols + 1;
int result_rows = image.rows - tepl.rows + 1;
cv::Mat result = cv::Mat( result_cols, result_rows, CV_32FC1 );
cv::matchTemplate( image, tepl, result, method );
double minVal, maxVal;
cv::Point minLoc, maxLoc;
cv::minMaxLoc( result, &minVal, &maxVal, &minLoc, &maxLoc, Mat() );
switch(method)
{
case CV_TM_SQDIFF:
point = minLoc;
return minVal / (tepl.cols * tepl.cols);
break;
case CV_TM_SQDIFF_NORMED:
point = minLoc;
return minVal;
break;
case CV_TM_CCORR:
case CV_TM_CCOEFF:
point = maxLoc;
return maxVal / (tepl.cols * tepl.cols);
break;
case CV_TM_CCORR_NORMED:
case CV_TM_CCOEFF_NORMED:
default:
point = maxLoc;
return maxVal;
break;
}
}
~~~
这时的结果如下:
~~~
CV_TM_SQDIFF Time elapsed: 705 ms
0.000609663
CV_TM_SQDIFF_NORMED Time elapsed: 682 ms
1.43391e-08
CV_TM_CCORR Time elapsed: 615 ms
42517.5
CV_TM_CCORR_NORMED Time elapsed: 698 ms
1
CV_TM_CCOEFF Time elapsed: 703 ms
35158.5
CV_TM_CCOEFF_NORMED Time elapsed: 757 ms
1
~~~
OpenCV 学习 (Split 和 Merge)
最后更新于:2022-04-01 11:25:30
## OpenCV 学习 (Split 和 Merge)
我们在图像处理时,经常要单独对某一个颜色通道进行处理。这时可以利用 Opencv 提供的 split 和 merge 函数。
### split 函数
用于将一幅多通道的图像的各个通道分离。
这个函数的原型如下:
~~~
void split(const Mat& src, vector<Mat_<_Tp> >& mv)
~~~
用法很简单,src 是一幅多通道的图像。
mv 保存各个通道,每个通道存放到一个 mat 中。
### merge 函数
merge 与split 函数相反。可以将多个单通道图像合成一幅多通道图像。
函数原型如下:
~~~
void merge(const Mat* mv, size_t count, OutputArray dst);
void merge(const vector<Mat>& mv, OutputArray dst );
~~~
这两个函数非常简单,所以就不举例子了。
有时,我用Qt写的小程序中也需要这个功能,又不想为了这么点小功能就使用 opencv。所以就自己山寨了两个函数。(其实是这篇博客实在是太短了,要找些内容来凑数)
两个函数的函数声明如下:
~~~
QList<QImage> split(const QImage &image);
QImage merge(const QImage &channel_R, const QImage &channel_G, const QImage &channel_B);
~~~
这里 merge 函数只能合并 3 个颜色通道。如果需要 alpha 通道,可以在这个代码基础上修改。
下面是代码,不多解释。希望对大家有用。
~~~
#include <QImage>
#include <QVector>
inline static bool isContinuous(const QImage &image)
{
bool ret = false;
switch(image.format())
{
case QImage::Format_Indexed8:
ret = image.bytesPerLine() == image.width();
break;
case QImage::Format_ARGB32:
case QImage::Format_RGB32:
case QImage::Format_ARGB32_Premultiplied:
case QImage::Format_RGBX8888:
case QImage::Format_RGBA8888:
case QImage::Format_RGBA8888_Premultiplied:
case QImage::Format_BGR30:
case QImage::Format_A2BGR30_Premultiplied:
case QImage::Format_RGB30:
case QImage::Format_A2RGB30_Premultiplied:
ret = image.bytesPerLine() == 4 * image.width();
break;
case QImage::Format_RGB16:
case QImage::Format_RGB555:
case QImage::Format_RGB444:
case QImage::Format_ARGB4444_Premultiplied:
ret = image.bytesPerLine() == 2 * image.width();
break;
case QImage::Format_ARGB6666_Premultiplied:
case QImage::Format_ARGB8565_Premultiplied:
case QImage::Format_RGB666:
case QImage::Format_ARGB8555_Premultiplied:
case QImage::Format_RGB888:
ret = image.bytesPerLine() == 3 * image.width();
case QImage::Format_Mono:
case QImage::Format_MonoLSB:
ret = image.byteCount()* 8 == image.width() * image.height();
default:
ret = false;
break;
}
return ret;
}
QImage merge(const QImage &channel_R, const QImage &channel_G, const QImage &channel_B)
{
if(channel_R.size() != channel_G.size() || channel_R.size() != channel_B.size())
{
return QImage();
}
if(channel_R.format() != QImage::Format_Indexed8 ||
channel_G.format() != QImage::Format_Indexed8 ||
channel_B.format() != QImage::Format_Indexed8)
{
return QImage();
}
QImage image(channel_R.size(), QImage::Format_RGB32);
int width = image.width();
int height = image.height();
if(isContinuous(image) && isContinuous(channel_B) && isContinuous(channel_G) && isContinuous(channel_R))
{
// 如果图像占用的内存是连续的,则可以只用一个循环来处理
width = width * height;
height = 1;
}
for(int j = 0; j < height; j++)
{
QRgb* line = (QRgb*) image.scanLine(j);
const uchar * r = channel_R.constScanLine(j);
const uchar * g = channel_G.constScanLine(j);
const uchar * b = channel_B.constScanLine(j);
for(int i = 0; i < width; i++)
{
line[i] = qRgb(r[i], g[i], b[i]);
}
}
return image;
}
QList<QImage> split(const QImage &image)
{
QList<QImage> rgb;
if(image.isNull())
{
return rgb;
}
QImage::Format f = image.format();
if(f == QImage::Format_RGB32 || f == QImage::Format_ARGB32 || f == QImage::Format_ARGB32_Premultiplied)
{
rgb.append(QImage());
rgb.append(QImage());
rgb.append(QImage());
rgb[0] = QImage(image.size(), QImage::Format_Indexed8);
rgb[1] = QImage(image.size(), QImage::Format_Indexed8);
rgb[2] = QImage(image.size(), QImage::Format_Indexed8);
for(int i = 0; i < 256; i++)
{
rgb[0].setColor(i, qRgb(i, 0, 0));
rgb[1].setColor(i, qRgb(0, i, 0));
rgb[2].setColor(i, qRgb(0, 0, i));
}
int width = image.width();
int height = image.height();
for(int j = 0; j < height; j++)
{
const QRgb* line = (QRgb*) image.constScanLine(j);
uchar * line_r = rgb[0].scanLine(j);
uchar * line_g = rgb[1].scanLine(j);
uchar * line_b = rgb[2].scanLine(j);
for(int i = 0; i < width; i++)
{
line_r[i] = qRed(line[i]);
line_g[i] = qGreen(line[i]);
line_b[i] = qBlue(line[i]);
}
}
}
return rgb;
}
~~~
OpenCV 学习(图像的基本运算)
最后更新于:2022-04-01 11:25:28
## OpenCV 学习(图像的基本运算)
图像的基本运算有很多种,比如两幅图像可以相加、相减、甚至可以相乘、相除。图像可以放大、缩小、旋转,还可以截取中间的一副子图,各个颜色通道还可以分别提取。总之,对于图像可以进行的基本运算非常的多,这里不可能全部都写出来,只是挑了些特别常用的简单的写写。
### 图像间的加减乘除
OpenCV 中提供了如下的一些函数,用来进行图像的加减乘除。
~~~
void add(InputArray src1, InputArray src2, OutputArray dst,
InputArray mask=noArray(), int dtype=-1);
void subtract(InputArray src1, InputArray src2, OutputArray dst,
InputArray mask=noArray(), int dtype=-1);
void multiply(InputArray src1, InputArray src2,
OutputArray dst, double scale=1, int dtype=-1);
void divide(InputArray src1, InputArray src2, OutputArray dst,
double scale=1, int dtype=-1);
void divide(double scale, InputArray src2,
OutputArray dst, int dtype=-1);
void scaleAdd(InputArray src1, double alpha, InputArray src2, OutputArray dst);
void addWeighted(InputArray src1, double alpha, InputArray src2,
double beta, double gamma, OutputArray dst, int dtype=-1);
~~~
这些函数都要求相加的两幅图像具有相同的尺寸,并且像素类型是相同的。
比如我们有两幅尺寸相同的图像,分别如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db4085f8.jpg "")
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db5548fd.jpg "")
执行下面的操作后:
~~~
cv::addWeighted(image, 0.5, image2, 0.5, 0., result);
~~~
得到的输出图像如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db57b0d1.jpg "")
实际上,上面的代码还可以写为:
~~~
result= 0.5 * image1 + 0.5 * image2;
~~~
与此类似的还有位运算函数:
~~~
void bitwise_and(InputArray src1, InputArray src2,
OutputArray dst, InputArray mask=noArray());
void bitwise_or(InputArray src1, InputArray src2,
OutputArray dst, InputArray mask=noArray());
void bitwise_xor(InputArray src1, InputArray src2,
OutputArray dst, InputArray mask=noArray());
void bitwise_not(InputArray src, OutputArray dst,
InputArray mask=noArray());
~~~
用法很简单,就不多介绍了。
另外一个比较常用的运算是求两幅图像像素的差的绝对值。
~~~
void absdiff(InputArray src1, InputArray src2, OutputArray dst);
~~~
还有些函数是对单幅图像进行操作的,比如对每个像素的值取平方、平方根、对数等。
~~~
void sqrt(InputArray src, OutputArray dst);
void pow(InputArray src, double power, OutputArray dst);
void exp(InputArray src, OutputArray dst);
void log(InputArray src, OutputArray dst);
~~~
这里给出的函数很有限。但是,基本上我们能想到的各种操作,OpenCV 的作者都替我们实现了,需要时可以现查。
上面的操作都假定两幅图像是相同大小的。当图像大小不同时,我们可以在较大的图像中挖取出一块小区域。
### 取图像中的子区域(ROI)
下面的代码在一副图像中加入个 logo。 logo 图像如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db5aa0bd.jpg "")
~~~
cv::Mat imageROI;
imageROI= image(cv::Rect(385,270,logo.cols, logo.rows));
// add logo to image
cv::addWeighted(imageROI, 1.0, logo, 0.3,0., imageROI);
~~~
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db5c5724.jpg "")
如果我们的 ROI 由图像中的一些连续行或连续列组成。可以用下面的方式来定义:
~~~
cv::Mat imageROI= image.rowRange(start,end);
cv::Mat imageROI= image.colRange(start,end) ;
~~~
### 分离图像的通道
有时,我们需要单独处理图像的某一个通道,这时可以可以用 split函数来分离图像的通道。
~~~
void split(const Mat& src, vector<Mat_<_Tp> >& mv);
~~~
这个函数将一副图像的各个通道,分离成多个矩阵。
~~~
std::vector<cv::Mat> planes;
cv::split(image1, planes);
~~~
对某个通道处理完成后,可以用 merge 函数组合回彩色图像。
~~~
void merge(const vector<Mat>& mv, OutputArray dst );
~~~
OpenCV 学习(像素操作 2)
最后更新于:2022-04-01 11:25:26
在上一个学习笔记中,简单介绍了访问图像像素的几种方法,并对这几种方法的效率进行了些简单的比较。但是更多的情况是我们要同时访问多个像素,经过较为复杂的运算才能得到我们希望的结果。今天就来讲讲这种情况如何处理。
### 同时访问多行像素数据
下面我们以laplace 锐化为例,Laplacian 算子写为矩阵形式如下:
⎛⎝⎜0−10−15−10−10⎞⎠⎟
可以看到,计算当前点的输出时需要上下左右 4 邻近点的值。这时最简单的想法就是同时用三个指针,分别指向当前行、上一行和下一行。下面是个例子代码:
~~~
void sharpen(const cv::Mat &image, cv::Mat &result)
{
// allocate if necessary
result.create(image.size(), image.type());
for (int j = 1; j < image.rows - 1; j++)
{ // for all rows
// (except first and last)
const uchar* previous = image.ptr<const uchar>(j-1); // previous row
const uchar* current = image.ptr<const uchar>(j); // current row
const uchar* next = image.ptr<const uchar>(j+1); // next row
uchar* output= result.ptr<uchar>(j); // output row
for (int i=1; i<image.cols-1; i++)
{
*output++= cv::saturate_cast<uchar>(
5 *current[i] - current[i-1] - current[i+1] - previous[i] - next[i]);
}
}
// Set the unprocess pixels to 0
result.row(0).setTo(cv::Scalar(0));
result.row(result.rows-1).setTo(cv::Scalar(0));
result.col(0).setTo(cv::Scalar(0));
result.col(result.cols-1).setTo(cv::Scalar(0));
}
~~~
有几点需要说明:
1. 这个函数只对灰度图像有效。
1. 应为对原始图像我们只是读取而不改变其值,所以用的是 const uchar* 型的指针。
1. 运算结果有可能会超出 uchar 型能够表示的范围,所以程序中用了 cv::saturate_cast () 将超出的部分截断。
1. 图像的四条边框没有计算,而是直接填充了 0 值。
原始图像和处理后的图像对比如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db48b508.jpg "")
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db4b40e2.jpg "")
如果要处理彩色图像,可以这样写:
~~~
void sharpen1(const cv::Mat &image, cv::Mat &result)
{
// allocate if necessary
result.create(image.size(), image.type());
int nr = image.rows;
int nl = image.cols;
for (int j = 1; j < nr - 1; j++)
{ // for all rows
// (except first and last)
const cv::Vec3b* previous = image.ptr<const cv::Vec3b>(j-1); // previous row
const cv::Vec3b* current = image.ptr<const cv::Vec3b>(j); // current row
const cv::Vec3b* next = image.ptr<const cv::Vec3b>(j+1); // next row
cv::Vec3b* output= result.ptr<cv::Vec3b>(j); // output row
for (int i = 1; i<nl - 1; i++)
{
output[i][0] = cv::saturate_cast<uchar>(
5 *current[i][0] - current[i - 1][0] - current[i + 1][0] - previous[i][0] - next[i][0]);
output[i][1] = cv::saturate_cast<uchar>(
5 *current[i][1] - current[i - 1][1] - current[i + 1][1] - previous[i][1] - next[i][1]);
output[i][2] = cv::saturate_cast<uchar>(
5 *current[i][2] - current[i - 1][2] - current[i + 1][2] - previous[i][2] - next[i][2]);
}
}
// Set the unprocess pixels to 0
result.row(0).setTo(cv::Vec3b(0, 0, 0));
result.row(result.rows-1).setTo(cv::Vec3b(0, 0, 0));
result.col(0).setTo(cv::Vec3b(0, 0, 0));
result.col(result.cols-1).setTo(cv::Vec3b(0, 0, 0));
}
~~~
或者还是用 uchar 型来处理:
~~~
void sharpen2(const cv::Mat &image, cv::Mat &result)
{
// allocate if necessary
result.create(image.size(), image.type());
int nr = image.rows;
int nl = image.cols * image.elemSize();
int stride = image.elemSize();
for (int j = 1; j < nr - 1; j++)
{ // for all rows
// (except first and last)
const uchar* previous = image.ptr<const uchar>(j-1); // previous row
const uchar* current = image.ptr<const uchar>(j); // current row
const uchar* next = image.ptr<const uchar>(j+1); // next row
uchar* output= result.ptr<uchar>(j); // output row
for (int i = stride; i < nl - stride; i++)
{
*output++= cv::saturate_cast<uchar>(
5 *current[i] - current[i - stride] - current[i + stride] - previous[i] - next[i]);
}
}
// Set the unprocess pixels to 0
result.row(0).setTo(cv::Vec3b(0, 0, 0));
result.row(result.rows-1).setTo(cv::Vec3b(0, 0, 0));
result.col(0).setTo(cv::Vec3b(0, 0, 0));
result.col(result.cols-1).setTo(cv::Vec3b(0, 0, 0));
}
~~~
实际上,上面的操作还可以使用 cv::filter2D 函数来做。代码会非常简洁,而且计算速度也是最快的。
~~~
void sharpen2D(const cv::Mat &image, cv::Mat &result)
{
// Construct kernel (all entries initialized to 0)
cv::Mat kernel(3, 3, CV_32F, cv::Scalar(0));
// assigns kernel values
kernel.at<float>(1,1) = 5.0;
kernel.at<float>(0,1) = -1.0;
kernel.at<float>(2,1) = -1.0;
kernel.at<float>(1,0) = -1.0;
kernel.at<float>(1,2) = -1.0;
//filter the image
cv::filter2D(image, result, image.depth(), kernel);
}
~~~
处理前后的图像对比如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db4e71c1.jpg "")
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db518b00.jpg "")
OpenCV 学习(像素操作 Manipuating the Pixels)
最后更新于:2022-04-01 11:25:23
## OpenCV 学习(像素操作 Manipuating the Pixels)
OpenCV 虽然提供了许多类型的图像处理函数,可以对图像进行各种常见的处理,但是总会有些操作时没有的,这时我们就需要自己来操纵像素,实现我们需要的功能。今天就来讲讲 OpenCV 进行像素级操作的几种方法,并做个比较。
在 OpenCV 中,图像用矩阵来表示,对应的数据类型为 cv::Mat 。 cv::Mat 功能很强大,矩阵的元素可以为字节、字、浮点数、数组等多种形式。对于灰度图像,每个像素用一个 8 bit 字节来表示,对彩色图像,每个像素是一个三个元素的数组,分别存储 **BGR** 分量,这里大家没看错,就是 BGR 而不是 RGB,每个像素三个字节,第一个字节是蓝色分量,别问我为啥设计成这样,我也不知道。
### 访问单个像素 (at 函数)
cv::Mat 类有个 at(int y, int x) 方法,可以访问单个像素。但是我们知道cv::Mat 可以存储各种类型的图像。在调用这个函数时必须要指定返回的像素的类型,因为 at 函数是模板函数。
如果是灰度图,我们知道像素是以无符号字符型变量的形式存储的。那么要像下面这样访问。
~~~
image.at<uchar>(j,i)= value;
~~~
如果图像是24位真彩色的,那么可以这样:
~~~
image.at<cv::Vec3b>(j,i)[channel]= value;
~~~
下面是个简单的例子,打开一副彩色图像,在上面随机的添加一些噪声。原始图像如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db4085f8.jpg "")
核心的代码:
~~~
cv::Mat image = cv::imread("Q:\\test.jpg", CV_LOAD_IMAGE_COLOR);
for(int k = 0; k < 1000; k++)
{
int i = rand() % image.cols;
int j = rand() % image.rows;
image.at<cv::Vec3b>(i, j)[0] = 255;
image.at<cv::Vec3b>(i, j)[1] = 255;
image.at<cv::Vec3b>(i, j)[2] = 255;
}
~~~
处理后的图像如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db436d46.jpg "")
像上面这样每次用 at 函数时都指定类型很繁琐。这时可以利用 cv::Mat 的派生类,cv::Mat_ 类,这个类是模板类。在建立这个类的实例时就要指定类型,之后就无需每次使用时再指定类型了。下面是个例子。
~~~
cv::Mat_ <cv::Vec3b> ima = image;
cv::namedWindow("Origin image", cv::WINDOW_NORMAL);
cv::imshow("Origin image", image);
for(int k = 0; k < 1000; k++)
{
int i = rand() % ima.cols;
int j = rand() % ima.rows;
ima(i, j)[0] = 255;
ima(i, j)[1] = 255;
ima(i, j)[2] = 255;
}
~~~
这个代码处理后的效果是相同的。
上面的程序中有个
~~~
ima = image;
~~~
这里又涉及到 OpenCV 的一个特性,就是普通的矩阵拷贝操作都是所谓的浅拷贝。也就是说这样操作后 ima 和 image 共享相同的图像数据。
如果我们想要真正的复制图像数据。这时可以用 clone() 方法。类似下面的代码:
~~~
cv::Mat_ <cv::Vec3b> ima = image.clone();
~~~
这样之后 ima 和 image 就完全独立了。
### 利用指针遍历图像像素
经常,我们的算法需要遍历图像的全部像素。这时用 at 函数就会很慢。更高效的访问图像像素的方式是利用指针。
简单的说,我们通常是去获得一行像素的头指针。如果图像是灰度的,则类似这样操作。
~~~
uchar* data = image.ptr<uchar>(j);
~~~
如果图像是 24 位彩色的,则可以这样:
~~~
cv::Vec3b * data = image.ptr<cv::Vec3b> (j);
~~~
实际上,即使是彩色图像,也可以用一个 uchar 型指针去指向。只要我们自己去计算要访问的像素相对行首的位置偏移是多少。比如下面的函数,可以处理灰度图像,也能处理彩色图像,作用是缩减图像中使用到的颜色。
~~~
void colorReduce(cv::Mat &image, int div=64)
{
int nl = image.rows; // number of lines
// total number of elements per line
int nc = image.cols * image.channels();
for (int j = 0; j < nl; j++)
{
// get the address of row j
uchar* data= image.ptr<uchar>(j);
for (int i = 0; i < nc; i++)
{
// process each pixel ---------------------
data[i]= data[i] / div * div + div / 2;
// end of pixel processing ----------------
}
} // end of line
}
~~~
利用默认参数应用于我们的测试图像后得到的结果如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db4667b6.jpg "")
上面的代码中有这么一行,是用来计算一行像素有多少个字节的。当然这个前提是每个channel 占用一个字节。
~~~
int nc= image.cols * image.channels();
~~~
如果每个 channel 占用多个字节的话,上面的公式就是错误的了,这时我们可以这样计算。
~~~
int nc = image.cols * image.elemSize();
~~~
image.elemSize() 得到的是每个像素的字节数。乘以一行有多少个像素,正好就是一行有多少个字节。
上面的例子中,我们用了两重循环来遍历图像中的每一个像素。实际上,因为我们对每个像素进行的操作是相同的,我们根本不需要确定某个像素是哪一行的。因此上面的代码还可以进一步优化,只用一个循环来完成。
但是这时我们要特别注意,有些图像所占的内存空间是不连续的。在一行像素结束后,可能会空出一小块内存。之所以会这样是因为有些 CPU 的指令对数据有内存对其要求。这样虽然浪费了一些空间,但是运算起来会更快速。
图像所占内存是否是连续的可以利用 isContinuous() 来得到。如果是连续的则可以用一个循环将所有像素都处理完。下面是代码,这个代码兼顾了内存连续与不连续两种情况,内存不连续时就退化为两重循环:
~~~
void colorReduce2(cv::Mat &image, int div=64)
{
int nl = image.rows; // number of lines
int nc = image.cols * image.channels();
if (image.isContinuous())
{
// then no padded pixels
nc = nc * nl;
nl = 1; // it is now a 1D array
}
// this loop is executed only once
// in case of continuous images
for (int j = 0; j < nl; j++)
{
uchar* data = image.ptr<uchar>(j);
for (int i = 0; i < nc; i++)
{
// process each pixel ---------------------
data[i] = data[i] / div * div + div / 2;
// end of pixel processing ----------------
} // end of line
}
}
~~~
如果我们要获得图像数据的首地址,还可以这样:
~~~
uchar *data = image.data;
~~~
对于二维图像数据来说,每行图像所占据的字节数由成员变量 step 来存储。因此:
~~~
data += image.step;
~~~
使得 data 指向下一行图像的内存首地址。
当然,上面这些操作都是比较低级的指针操作,不建议使用。
### 利用 iterators 来遍历图像数据
C++ 的标准模板库(STL)中大量的使用到了 iterator。OpenCV 也模仿 STL 显示了自己的一套 iterator。
OpenCV 中设计了 cv::MatIterator_ 类,这个类与 cv::Mat_ 类似,也是模板类。将这个类实例化时需要指定具体的类型。比如下面的代码:
~~~
cv::MatIterator_<cv::Vec3b> it;
~~~
另一种使用方法如下:
~~~
cv::Mat_<cv::Vec3b>::iterator it;
~~~
如果我们只是用 iterator 来读取像素值而不改变它,则可以用常量型 iterator.
~~~
cv::MatConstIterator_<cv::Vec3b> it;
cv::Mat_<cv::Vec3b>::const_iterator it;
~~~
上面的例子用 iterator 重写后代码如下:
~~~
void colorReduce3(cv::Mat &image, int div=64)
{
// obtain iterator at initial position
cv::Mat_<cv::Vec3b>::iterator it = image.begin<cv::Vec3b>();
// obtain end position
cv::Mat_<cv::Vec3b>::iterator itend = image.end<cv::Vec3b>();
// loop over all pixels
for ( ; it!= itend; ++it)
{
// process each pixel ---------------------
(*it)[0] = (*it)[0] / div * div + div / 2;
(*it)[1] = (*it)[1] / div * div + div / 2;
(*it)[2] = (*it)[2] / div * div + div / 2;
}
}
~~~
这种方式有利也有弊,最大的缺点是这个代码只能处理 24 位真彩色图像。优点是无需关注内存是否连续的问题了。相对来说,利用 iterator 的代码的运算速度比直接指针操作还是要稍微的慢一点。
### 各种方法的速度比较
上面介绍了几种访问图像像素的方法,在不考虑效率的前提下,这些方法都很好,可以实现同样的功能。但是在计算机视觉应用场景中,计算效率(运行速度)经常是我们必须要考虑的关键因素。
因此这里专门用一个小节来比较各种方法的运行速度。
为了完整性,下面也给出了一个用 at函数访问像素的 colorReduce 函数。
~~~
void colorReduce0(cv::Mat &image, int div=64)
{
int nl = image.rows; // number of lines
int nc = image.cols;
for (int j=0; j<nl; j++)
{
for (int i=0; i<nc; i++)
{
// process each pixel ---------------------
image.at<cv::Vec3b>(j,i)[0]=
image.at<cv::Vec3b>(j,i)[0]/div*div + div/2;
image.at<cv::Vec3b>(j,i)[1]=
image.at<cv::Vec3b>(j,i)[1]/div*div + div/2;
image.at<cv::Vec3b>(j,i)[2]=
image.at<cv::Vec3b>(j,i)[2]/div*div + div/2;
// end of pixel processing ----------------
} // end of line
}
}
~~~
回顾一下我们实现的几种方法。
- colorReduce0(): 使用 at 函数访问像素
- colorReduce1(): 两重循环,用指针访问像素
- colorReduce2(): 当图像内存连续时用一重循环访问所有像素
- colorReduce3(): 用 iterator 访问像素
利用 cv::getTickCount() 来计算各个函数的运行时间。
- colorReduce0(): 240579
- colorReduce1(): 22363
- colorReduce2(): 21202
- colorReduce3(): 77573
结果一目了然,colorReduce0 运行的最慢,其次是 colorReduce3。
colorReduce1 和 colorReduce2 相差的不多。因此,我们写程序时,应尽可能的采用 colorReduce2 或 colorReduce1 这样的用法。
OpenCV 学习(利用滤波器进行边缘提取)
最后更新于:2022-04-01 11:25:21
## OpenCV 学习(利用滤波器进行边缘提取)
通过低通滤波器,我们可以将图像平滑,相反的,利用高通滤波器可以提取出图像的边缘。
### Sobel 滤波器
Sobel 滤波器是一种有方向性的滤波器,可以作用在 X 方向或 Y 方向。
关于这种滤波器的理论介绍可以参考:
[https://en.wikipedia.org/wiki/Sobel_operator](https://en.wikipedia.org/wiki/Sobel_operator)
函数原型如下:
~~~
void Sobel( InputArray src, OutputArray dst, int ddepth,
int dx, int dy, int ksize=3,
double scale=1, double delta=0,
int borderType=BORDER_DEFAULT );
~~~
这个滤波器结合了高斯平滑滤波和差分运算,对噪声不是很敏感,是一种很常用的边缘检测算子。 dx 和 dy 是 X 和 Y 方向差分运算的阶数。
如果对 X 方向求1 阶差分,这对参数设为 1, 0。 对 Y 方向则设为 0, 1。
ksize 是核的大小,只能为 1, 3, 5, 7。 ksize = 1 时核为 1行3列或 3行1列,这时高斯平滑的步骤就没有了。
ksize 还可以设为 CV_SCHARR (-1),这算是个隐藏功能。 这时实际上计算的是 3 * 3 Scharr 滤波器。
scale 是对计算结果的缩放,delta 是对计算结果的平移。
下面是一个例子,原始测试图像如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db3987f1.jpg "")
这里我们对 X 方向进行边缘检测。因为计算结果会出现负数,所以还做了些放缩和平移操作。代码如下:
~~~
cv::Sobel(image, result, CV_8U, 1, 0, 3, 0.5, 128);
~~~
处理后的图像如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db3af33f.jpg "")
可以看到图像X方向的正负边缘都检测出来了,并且正负边缘的数值是不同的,这对于我们需要提取某一种特定边缘时是很有利的。
如果我们不需要区分正负边缘,可以取个绝对值运算。类似下面这样。
~~~
cv::Sobel(image, result, CV_16S, 1, 0, 3);
result = abs(result);
result.convertTo(result, CV_8U);
~~~
这里需要注意的是,当 image 是 CV_8U 类型时,result 类型不能是 CV_8S。这里只能先转成 CV_16S 然后再转换为我们需要的 CV_8U。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db3c6920.jpg "")
分别对图像的 X 方向和 Y 方向进行 Sobel 滤波后我们就得到了图像的梯度信息。但是这个梯度信息是(X, Y)形式的。有时我们需要用到 (ρ,θ) 形式。这时可以用 cv::cartToPolar 函数来进行转换。下面是个例子:
~~~
cv::Sobel(image, resultX, CV_32F, 1, 0, 3);
cv::Sobel(image, resultY, CV_32F, 0, 1, 3);
cv::Mat norm, dir;
cv::cartToPolar(resultX, resultY, norm, dir);
~~~
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db3df740.jpg "")
### Scharr 滤波器
Scharr 算子在图像的梯度方向的计算方面比 Sobel 算子更准确一些。用法和 Sobel 算子是类似的。
具体的理论也可以参考:
[https://en.wikipedia.org/wiki/Sobel_operator](https://en.wikipedia.org/wiki/Sobel_operator)
下面是 Scharr 算子的函数原型。
~~~
void Scharr( InputArray src, OutputArray dst, int ddepth,
int dx, int dy, double scale=1, double delta=0,
int borderType=BORDER_DEFAULT );
~~~
简单的说:
~~~
Scharr(src, dst, ddepth, dx, dy, scale, delta, borderType);
~~~
等效于:
~~~
Sobel(src, dst, ddepth, dx, dy, CV_SCHARR, scale, delta, borderType);
~~~
OpenCV 学习(几种基本的低通滤波)
最后更新于:2022-04-01 11:25:19
## OpenCV 学习(几种基本的低通滤波)
对图像进行滤波处理是图像处理中最常见的一种操作类型。而这其中低通滤波(也可以叫做平滑)有事各种滤波处理中最常用的。这里就简单写写 OpenCV 中提供的几种低通滤波方法。
### 均值滤波
这种滤波方法就是取一个像素的邻域内各像素的平均值作为滤波结果。比如下面这个例子:
~~~
cv::blur(image, result, cv::Size(7, 7), cv::Point(-1, -1), cv::BORDER_DEFAULT);
~~~
原始图像是 image, 滤波后的图像是 result ,邻域大小为 5 * 5。 cv::Point(-1, -1) 表明邻域的零位就是邻域的中心,这个是默认值,如果不改变的话可以不填。
cv::BORDER_DEFAULT 是对边界的处理办法,这个一般也不需要改变的。
下面是一副图像滤波前后的对比。
![原始图片](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1da7517af.jpg "")
![滤波后](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db22dfba.jpg "")
与 blur 函数相关的还有个 boxFilter 函数。这个滤波器的核的各个元素都为 1。normalize = false 时相当于邻域内各像素的数值求和。 normalize = true 时,计算结果等效于 blur 函数。
~~~
cv::boxFilter ( InputArray src,
OutputArray dst,
int ddepth,
Size ksize,
Point anchor = Point(-1,-1),
bool normalize = true,
int borderType = BORDER_DEFAULT
)
~~~
### 高斯滤波
均值滤波对邻域内各个像素采用统一的权值,这种方式对大多数应用来说不是最佳的。高斯滤波采取邻域内越靠近的值提供越大的权重的方式计算平均值。权重的选取采用高斯函数的形式。高斯函数有个非常好的特点,就是无论在时域还是频域都是钟形的。通过控制 σ 可以控制低通滤波的截止频率。函数原型如下:
~~~
void GaussianBlur( InputArray src,
OutputArray dst, Size ksize,
double sigmaX, double sigmaY=0,
int borderType=BORDER_DEFAULT );
~~~
使用起来很方便,下面是个例子:
~~~
cv::GaussianBlur(image, result, cv::Size(7, 7), 1.5);
~~~
参数image为输入图像,result为输出图像,Size(5,5)定义了核的大小,最后一个参数是高斯滤波的 σ 。从函数原型上可以看到有 sigmaX 和 sigmaY 两个参数。通常情况下 sigmaY 取与 sigmaX 相同的值,这时可以不写出来。也就是用它的默认值 0.
还是刚才的图像,高斯滤波后的结果如下:
![高斯滤波后的结果](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db26e7be.jpg "")
高斯滤波器的大小和 σ 可以只指定一个。另一个会自动选择合适的值。
比如说我们希望 σ=1.5。那么可以直接写为:
~~~
cv::GaussianBlur(image, result, cv::Size(0, 0), 1.5);
~~~
或者要求核的大小为 9 * 9 个点。可以这样写:
~~~
cv::GaussianBlur(image, result, cv::Size(9, 9), 0);
~~~
如果加大 σ 会滤波的更平滑些,但是也会损失更多的细节,当 σ=4 时的滤波结果如下。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db2af429.jpg "")
相关的函数还有个 getGaussianKernel。 这个函数可以计算高斯滤波器的系数,但是它计算的是 1 维滤波器的系数。对于高斯滤波器来说, 2 维系数其实就是横向和竖向两个 1 维滤波器的系数的乘积。这种性质有个专有名词,叫做 seperable filter。
下面是这个函数的一个用例:
~~~
cv::Mat kernel = cv::getGaussianKernel(7, 1.5, CV_32F);
~~~
### 中值滤波器
中值滤波是一种非线性滤波器。它是取邻域内各点的统计中值作为输出。这种滤波器可以有效的去除椒盐噪声。还能保持图像中各物体的边界不被模糊掉。是一种最常用的非线性滤波器。这种滤波器只能使用正方形的邻域。下面是个例子:
~~~
cv::medianBlur(image, result, 7);
~~~
下面是原始图像。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db2e3832.jpg "")
中值滤波的结果如下。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db33146c.jpg "")
同样大小滤波核高斯滤波的结果如下。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db35eeef.jpg "")
对比很明显,中值滤波对于去除这些细线更有效。
### 通用滤波器 filter2D
利用这个函数我们可以自定义滤波器的核。
比如下面的代码:
~~~
cv::Mat kernel = (cv::Mat_<float>(3, 3) << 1/9.0, 1/9.0, 1/9.0,
1/9.0, 1/9.0, 1/9.0,
1/9.0, 1/9.0, 1/9.0);
cv::filter2D(image, result, image.depth(), kernel);
~~~
就相当于一个 3 * 3 的均值滤波器。
~~~
cv::blur(image, result, cv::Size(3, 3), cv::Point(-1, -1), cv::BORDER_DEFAULT);
~~~
当然,如果只是搞个均值滤波器,不需要这么麻烦,直接用 blur 函数就可以了。但是如果我们要设计个很特殊的滤波器时,filter2D 就派上用场了。
### 可分离滤波器 sepFilter2D
一个 2 维滤波器,如果可以分离为x 方向和 y 方向两个独立的 1 维滤波器。那么这个 2 维滤波器就称为 可分离滤波器。比如我们上面介绍的高斯滤波器就是一个典型的可分离滤波器。具有这种性质的滤波器有快速算法,可以比不具有这个性质的普通的滤波器更高效的计算。
这个函数的接口如下:
~~~
void sepFilter2D( InputArray src, OutputArray dst, int ddepth,
InputArray kernelX, InputArray kernelY,
Point anchor=Point(-1,-1),
double delta=0, int borderType=BORDER_DEFAULT );
~~~
与其他滤波器最大的区别就是需要传进 2 个滤波器核,kernelX 和 kernelY。下面举个例子:
~~~
cv::Mat kernel = cv::getGaussianKernel(7, 1.5, CV_32F);
cv::sepFilter2D(image, result, -1, kernel, kernel);
~~~
相当于对 image 进行了一次高斯滤波,也就是说与下面的代码等效。
~~~
cv::GaussianBlur(image, result, cv::Size(7, 7), 1.5);
~~~
大家可以试试,两种方法得到的结果完全相同。
QImage 与 cv::Mat 之间的相互转换
最后更新于:2022-04-01 11:25:17
最近做图像处理方面的项目比较多,很多算法自己从头写的话太浪费时间,而且自己写的也不一定完善,早就听说OpenCV在图像处理算法方面功能很强大,一直没时间学习,这次正好项目用到了,临时抱佛脚学习些OpenCV入门知识。因为我的程序界面都是用Qt写的,因此也花了点时间研究了如何将OpenCV 和Qt 融合在一起,协同工作。
Qt 中处理图像主要用的是QImage类,OpenCV中主要用的是cv::Mat类。下面的两个函数可以用来实现这两个类相互转换。
~~~
QImage cvMat2QImage(const cv::Mat& mat)
{
// 8-bits unsigned, NO. OF CHANNELS = 1
if(mat.type() == CV_8UC1)
{
QImage image(mat.cols, mat.rows, QImage::Format_Indexed8);
// Set the color table (used to translate colour indexes to qRgb values)
image.setColorCount(256);
for(int i = 0; i < 256; i++)
{
image.setColor(i, qRgb(i, i, i));
}
// Copy input Mat
uchar *pSrc = mat.data;
for(int row = 0; row < mat.rows; row ++)
{
uchar *pDest = image.scanLine(row);
memcpy(pDest, pSrc, mat.cols);
pSrc += mat.step;
}
return image;
}
// 8-bits unsigned, NO. OF CHANNELS = 3
else if(mat.type() == CV_8UC3)
{
// Copy input Mat
const uchar *pSrc = (const uchar*)mat.data;
// Create QImage with same dimensions as input Mat
QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_RGB888);
return image.rgbSwapped();
}
else if(mat.type() == CV_8UC4)
{
qDebug() << "CV_8UC4";
// Copy input Mat
const uchar *pSrc = (const uchar*)mat.data;
// Create QImage with same dimensions as input Mat
QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_ARGB32);
return image.copy();
}
else
{
qDebug() << "ERROR: Mat could not be converted to QImage.";
return QImage();
}
}
cv::Mat QImage2cvMat(QImage image)
{
cv::Mat mat;
qDebug() << image.format();
switch(image.format())
{
case QImage::Format_ARGB32:
case QImage::Format_RGB32:
case QImage::Format_ARGB32_Premultiplied:
mat = cv::Mat(image.height(), image.width(), CV_8UC4, (void*)image.constBits(), image.bytesPerLine());
break;
case QImage::Format_RGB888:
mat = cv::Mat(image.height(), image.width(), CV_8UC3, (void*)image.constBits(), image.bytesPerLine());
cv::cvtColor(mat, mat, CV_BGR2RGB);
break;
case QImage::Format_Indexed8:
mat = cv::Mat(image.height(), image.width(), CV_8UC1, (void*)image.constBits(), image.bytesPerLine());
break;
}
return mat;
}
~~~
程序比较简单,就不多说明了。唯一需要注意的是cvMat 和QImage 对于RGBA 这四个分量的排列顺序是不相同的。转换的时候需要调换一下。但是Qt 的文档中说的很明确,QImage内部数据存储的方式不能保证以后永远不变。所以这个代码也不能保证一直是可用的。
下面是五个测试用例。基本上把各种常见情况都覆盖了。
~~~
void test1()
{
cv::Mat mat = cv::imread("Q:\\Koala.jpg", cv::IMREAD_UNCHANGED);
cv::cvtColor(mat, mat, CV_BGR2BGRA);
QImage image = cvMat2QImage(mat);
qDebug() << (mat.type() == CV_8UC4);
cvNamedWindow("cvMat2QImage RGB32", CV_WINDOW_AUTOSIZE);
imshow("cvMat2QImage RGB32", mat);
QLabel label;
label.setPixmap(QPixmap::fromImage(image));
label.show();
cv::waitKey(10000);
}
void test2()
{
cv::Mat mat = cv::imread("Q:\\Koala.jpg", cv::IMREAD_UNCHANGED);
cv::cvtColor(mat, mat, CV_BGR2GRAY);
QImage image = cvMat2QImage(mat);
cvNamedWindow("cvMat2QImage gray", CV_WINDOW_AUTOSIZE);
imshow("cvMat2QImage gray", mat);
QLabel label;
label.setPixmap(QPixmap::fromImage(image));
label.show();
cv::waitKey(10000);
}
void test3()
{
QImage image("Q:\\Koala.jpg");
image = image.convertToFormat(QImage::Format_RGB32);
cv::Mat mat = QImage2cvMat(image);
//cv::cvtColor(mat, mat, CV_BGR2RGB);
imshow("QImage2cvMat RGB32", mat);
cv::waitKey(10000);
}
void test4()
{
QImage image("Q:\\Koala.jpg");
image = image.convertToFormat(QImage::Format_RGB888);
cv::Mat mat = QImage2cvMat(image);
imshow("QImage2cvMat RGB24", mat);
cv::waitKey(10000);
}
void test5()
{
QImage image("Q:\\Koala.jpg");
image = image.convertToFormat(QImage::Format_Indexed8);
cv::Mat mat = QImage2cvMat(image);
imshow("QImage2cvMat Indexed8", mat);
cv::waitKey(10000);
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//test1();
//test2();
//test3();
//test4();
//test5();
test1();
return a.exec();
}
~~~
前言
最后更新于:2022-04-01 11:25:14
> 原文出处:[OpenCV 应用笔记](http://blog.csdn.net/column/details/usingopencv.html)
作者:[李渊](http://blog.csdn.net/liyuanbhu)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# OpenCV 应用笔记
> 机器视觉各种常见算法原理介绍,及OpenCV 实现。OpenCV 的各种应用技巧。利用 OpenCV 实现的各种机器视觉应用实例。