1. 常用知识
1.1 HSV
- H: Hue (色度)
- 表示是什么颜色,即R、G、B等
- 它使用度来表示,0度:红色,120度:绿色, 240 度:蓝色
- S: Saturation (饱和度)
- 颜色有多深,(0-100%)
- V: Value (色调)
- 颜色有多亮,(0-100%)
- 三者关系:
- 当S=1 V=1时,H所代表的任何颜色被称为纯色;
- 当S=0时,即饱和度为0,颜色最浅,最浅被描述为灰色(灰色也有亮度,黑色和白色也属于灰色),灰色的亮度由V决定,此时H无意义;
- 当V=0时,颜色最暗,最暗被描述为黑色,因此此时H(无论什么颜色最暗都为黑色)和S(无论什么深浅的颜色最暗都为黑色)均无意义。
- RGB转换为HSV
m a x = M A X ( r , g , b ) m i n = M I N ( r , g , b ) H = { 0 0 , if max = min 6 0 0 × g − b m a x − m i n + 0 0 if max == r and g >= b 6 0 0 × g − b m a x − m i n + 36 0 0 if max == r and g < b 6 0 0 × b − r m a x − m i n + 12 0 0 if max == g 6 0 0 × r − g m a x − m i n + 24 0 0 if max == b S = ( m a x − m i n ) / m a x / 255 V = m a x / 255 \begin{aligned} max &= MAX(r, g, b) \\ min &= MIN(r, g, b) \\ H &= \begin{cases} 0^0, &\text{if max = min} \\ 60^0 \times \frac{g-b}{max-min} + 0^0 &\text{if max == r and g >= b} \\ 60^0 \times \frac{g-b}{max-min} + 360^0 &\text{if max == r and g < b} \\ 60^0 \times \frac{b-r}{max-min} + 120^0 &\text{if max == g} \\ 60^0 \times \frac{r-g}{max-min} + 240^0 &\text{if max == b} \end{cases} \\ S &= (max - min) / max / 255 \\ V &= max / 255 \end{aligned} maxminHSV=MAX(r,g,b)=MIN(r,g,b)=⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧00,600×max−ming−b+00600×max−ming−b+3600600×max−minb−r+1200600×max−minr−g+2400if max = minif max == r and g >= bif max == r and g < bif max == gif max == b=(max−min)/max/255=max/255 - OpenCV中的HSV
- H: [0, 180)
- S: [0., 255)
- V: [0, 255)
- 若要区分两种颜色,就找H、S、V中没有重叠的这一个分量
- 示例代码
Mat src, src_hsv, src_h, src_v, src_s;
src = imread("1.jpg");
cvtColor(src,src_hsv,COLOR_BGR2HSV); // convert to HSV
vector<Mat> rgb_planes;
split(src_hsv, rgb_planes ); // split to 3 mat
src_h = rgb_planes[0]; // H plane (there is no shadow infomation)
src_s = rgb_planes[1]; // S plane
src_v = rgb_planes[2]; // V plane
1.2 向量间夹角
1.2.1 二维向量
- 二维向量:
a
=
(
x
1
,
y
1
)
,
b
=
(
x
2
,
y
2
)
a=(x_1, y_1), b=(x_2, y_2)
a=(x1,y1),b=(x2,y2)
c o s ( θ ) = x 1 x 2 + y 1 y 2 x 1 2 + y 1 2 ⋅ x 2 2 + y 2 2 cos(\theta) = \frac{x_1x_2 + y_1y_2}{\sqrt{x_1^2+y_1^2} \cdot \sqrt {x_2^2 + y_2^2}} cos(θ)=x12+y12⋅x22+y22x1x2+y1y2
1.2.2 三维向量
- 三维向量:
a
=
(
x
1
,
y
1
,
z
1
)
,
b
=
(
x
2
,
y
2
,
z
2
)
a=(x_1, y_1, z_1), b=(x_2, y_2, z_2)
a=(x1,y1,z1),b=(x2,y2,z2)
c o s ( θ ) = x 1 x 2 + y 1 y 2 + z 1 z 2 x 1 2 + y 1 2 + z 1 2 ⋅ x 2 2 + y 2 2 + z 2 2 cos(\theta) = \frac{x_1x_2 + y_1y_2 + z_1z_2}{\sqrt{x_1^2+y_1^2 + z_1^2} \cdot \sqrt {x_2^2 + y_2^2 + z_2^2}} cos(θ)=x12+y12+z12⋅x22+y22+z22x1x2+y1y2+z1z2
2. 常用函数
2.1 阈值化操作
2.1.1 直接阈值化
- 函数定义
double cv::threshold(
cv::InputArray src, // 输入图像
cv::OutputArray dst, // 输出图像
double thresh, // 阈值
double maxValue, // 向上最大值
int thresholdType // 阈值化操作的类型
);
- 功能:给定一个输入数组和一个阈值,数组中的每个元素与阈值进行比较,然后把对应的结果写入输出数组中
- 参数说明
thresholdType | 说明 | 注意 |
---|---|---|
cv::THRESH_BINARY | DSTi = (SRCi > thresh) ? maxValue: 0 | |
cv::THRESH_BINARY_INV | DSTi = (SRCi > thresh) ? 0 : maxValue | |
cv::THRESH_TRUNC | DSTi = (SRCi > thresh) ? THRESH : SRCi | |
cv::THRESH_TOZERO | DSTi = (SRCi > thresh) ? SRCi : 0 | |
cv::THRESH_TOZERO_INV | DSTi = (SRCi > thresh) ? 0 : SRCi | |
cv::THRESH_OTSU | 1) flag, 使用OSTU算法选择最优的阈值 2) 此阈值把像素分为A、B两类,且使A、B类间方差最大 3) 与自适应阈值化的差别,它只有一个阈值,而自适应阈值化每个像素有一个阈值 4) 需与其它thresholdType一起使用,此时thresh参数无效 (THRESH_BINARY | THRESH_OSTU) | 不支持32位 |
cv::THRESH_TRIANGLE | 1) flag, 使用Triangle算法选择最优的阈值 | 不支持32位 |
2.1.2 自适应阈值化
- 函数定义
void cv::adaptiveThreshold(
cv::InputArray src, // 输入图像
cv::OutputArray dst, // 输出图像
double maxValue, // 向上最大值
int adaptiveMethod, // 自适应方法,平均或高斯
int thresholdType, // 阈值化类型
int blockSize, // 块大小
double C // 常量
);
- 功能 :
- 自适应阈值根据图像不同区域亮度分布的,改变阈值
- 使用函数adaptiveThreshold的关键是确定blockSize和C的值)
- 参数说明
- blockSize:计算单位是像素的邻域块大小,必须为奇数
- adaptiveMethod:在一个邻域(由blockSize决定)内计算阈值所采用的算法
adaptiveMethod | 说明 |
---|---|
cv::ADAPTIVE_THRESH_MEAN_C | 计算出邻域的平均值再减去第七个参数double C的值, 即NTi |
cv::ADAPTIVE_THRESH_GAUSSIAN_C | 计算出邻域的高斯均值再减去第七个参数double C的值,即NTi |
thresholdType | 说明 |
---|---|
cv::THRESH_BINARY | DSTi = (SRCi > NTi) ? maxValue: 0 |
cv::THRESH_BINARY_INV | DSTi = (SRCi > NTi) ? 0 : maxValue |
- 示例代码
Mat src = imread("1.jpg");
Mat dst1, dst2, dst3;
cvtColor(src, src, COLOR_BGR2GRAY); // 灰度化
medianBlur(src,src,5);//中值滤波
threshold(src,dst1, 127, 255, THRESH_BINARY);//阈值分割, 最差
adaptiveThreshold(src,dst2,255,ADAPTIVE_THRESH_MEAN_C,THRESH_BINARY,11,2);//自动阈值分割,邻域均值, 较好
adaptiveThreshold(src,dst3,255,ADAPTIVE_THRESH_GAUSSIAN_C,THRESH_BINARY,11,2);//自动阈值分割,高斯邻域, 最好
2.2 图像旋转
2.2.1 图像翻转
- 函数定义
void cv::flip(
cv::InputArray src, // 输入图像
cv::OutputArray dst, // 输出
int flipCode = 0 // >0: 沿y-轴翻转, 0: 沿x-轴翻转, <0: x、y轴同时翻转
);
2.3 分割背景和前景
- MOG的第一张图片是关键的参考图片,否则会带来很多问题
版本 | 支持的类 |
---|---|
OpenCV2.x | BackgroundSubtractorMOG2 BackgroundSubtractorMOG |
OpenCV3.x | BackgroundSubtractorMOG2 |
- MOG2比MOG的优势:
- 增加阴影检测功能
- 算法效率有较大提升
2.3.1 OpenCV2.x示例代码
int main()
{
VideoCapture video("../video.avi");
Mat frame, mask, thresholdImage, output;
int frameNum = 1;
if (!video.isOpened()) {
cout << "fail to open!" << endl;
return -1;
}
long totalFrameNumber = video.get(CV_CAP_PROP_FRAME_COUNT);
video>>frame;
BackgroundSubtractorMOG bgSubtractor(20, 10, 0.5, false);
while (true){
if (totalFrameNumber == frameNum)
break;
video >> frame;
++frameNum;
bgSubtractor(frame, mask, 0.001);
imshow("mask",mask);
waitKey(10);
}
return 0;
}
-
构造函数:
BackgroundSubtractorMOG2::BackgroundSubtractorMOG2(int history,
float varThreshold,
bool bShadowDetection=true)
- 执行函数:
void BackgroundSubtractorMOG2::operator()(InputArray image,
OutputArray fgmask, doublelearningRate=-1)
- 执行函数参数说明:
- image:为待处理的图像
- fgmask:得到的前景图像(二值化的)
- learningRate:配置背景更新方法
- 0:表示不更新
- 1:表示根据最后一帧更新
- <0:负数表示自动更新
- (0~1):数字越大,背景更新越快。
#include"opencv2/opencv.hpp"
#include"opencv2/core/core.hpp"
#include"opencv2/highgui/highgui.hpp"
#include"opencv2/imgproc/imgproc.hpp"
#include"opencv2/video/background_segm.hpp"
#include<iostream>
using namespace cv;
using namespace std;
int main()
{
VideoCapture capture(0);
BackgroundSubtractorMOG2 bg_model;//(100, 3, 0.3, 5);
Mat image, fgimage, fgmask;
bool update_bg_model = true;
while (1)
{
capture >> image;
if (!image.data)
{
cerr << "picture error!";
return -1;
}
if (fgimage.empty())
fgimage.create(image.size(), image.type());
bg_model(image, fgmask, update_bg_model ? -1 : 0);
fgimage = Scalar::all(0);
image.copyTo(fgimage, fgmask);
Mat bgimage;
bg_model.getBackgroundImage(bgimage);
imshow("image", image);
imshow("fgimage", fgimage);
imshow("fgmask", fgmask);
if (!bgimage.empty())
imshow("bgimage", bgimage);
waitKey(30);
}
return 0;
2.3.2 OpenCV3.x示例代码
- BackgroundSubtractorMOG2
- createBackgroundSubtractorMOG2() : 创建一个MOG2
- 参考代码
- 函数定义:
#include <opencv2/video/background_segm.hpp>
Ptr<BackgroundSubtractorMOG2> cv::createBackgroundSubtractorMOG2
( int history = 500,
double varThreshold = 16,
bool detectShadows = true
)
参数 | 说明 |
---|---|
history | 历史帧的长度 |
varThreshold | 像素与模型之间的Mahalanobis距离平方的阈值,以确定背景模型是否很好地描述了像素。 此参数不影响背景更新 |
detectShadows | 如果为true,则算法将检测阴影并对其进行标记。 它会稍微降低速度,因此,如果不需要此功能,请将参数设置为false。 |
- BackgroundSubtractorMOG2
- 计算一个前景mask
- 参考代码
- void apply (InputArray image, OutputArray fgmask, double learningRate=-1)
- 参数说明:
- 计算一个前景mask
参数 | 说明 |
---|---|
image | 下一个视频帧,输入图像 |
fgmask | 输出前景mask, 一个8位二值图像 |
learningRate | (0,1):0到1之间的值,表示学习背景模型的速度 < 0:使算法使用一些自动选择的学习率 0: 从不更新背景模型 1:从最后一帧完全重新初始化背景模型 |
#include"opencv2/opencv.hpp"
#include"opencv2/core/core.hpp"
#include"opencv2/highgui/highgui.hpp"
#include"opencv2/imgproc/imgproc.hpp"
#include"opencv2/video/background_segm.hpp"
#include<iostream>
using namespace cv;
using namespace std;
int main()
{
VideoCapture capture(0);
Ptr<BackgroundSubtractorMOG2> bg_model=createBackgroundSubtractorMOG2();
bg_model->setVarThreshold(20);
Mat image, fgimage, fgmask;
bool update_bg_model = true;
while (1)
{
capture >> image;
if (!image.data)
{
cerr << "picture error!";
return -1;
}
if (fgimage.empty())
fgimage.create(image.size(), image.type());
bg_model->apply(image, fgmask, update_bg_model ? -1 : 0);
fgimage = Scalar::all(0);
image.copyTo(fgimage, fgmask);
Mat bgimage;
bg_model->getBackgroundImage(bgimage);
imshow("image", image);
imshow("fgimage", fgimage);
imshow("fgmask", fgmask);
if (!bgimage.empty())
imshow("bgimage", bgimage);
waitKey(30);
}
return 0;
}
2.3 数据格式转换(>= OpenCV3.0)
2.3.1 IplImage转Mat
IplImage* image = cvLoadImage( "lena.jpg");
Mat mat=cv::cvarrToMat(image);
2.3.2 Mat转IplImage
IplImage img = IplImage(mat);
2.3.3 CvMat转Mat
CvMat* points = cvCreateMat(10, 3, CV_32F);
cv::Mat target(points->rows, points->cols, CV_32F, points->data.fl);
2.3.4 Mat高效访问
- 方法一:对每一行,取其行首地址
#include <highgui.h>
using namespace std ;
using namespace cv ;
int main()
{
Mat image = imread("forest.jpg") ;
imshow("image" , image) ;
//单通道多通道都适用
int nRows = image.rows ;
int nCols = image.cols * image.channels() ;
if(image.isContinuous())
{
nCols = nRows * nCols ;
nRows = 1 ;
}
for(int h = 0 ; h < nRows ; ++ h)
{
uchar *ptr = image.ptr<uchar>(h) ;
for(int w = 0 ; w < nCols ; ++ w)
{
//ptr[w] = 128 ;
*ptr ++ = 128 ;
}
}
imshow("high" , image) ;
waitKey(0) ;
return 0 ;
}
- 使用mat.data和mat.step的方式获取行首地址
void colorReduce4(cv::Mat &image, int div=64) {
int nr= image.rows; // number of rows
int nc= image.cols * image.channels(); // total number of elements per line
int n= static_cast<int>(log(static_cast<double>(div))/log(2.0));
int step= image.step; // effective width, that is: the number of bytes of a line
// mask used to round the pixel value
uchar mask= 0xFF<<n; // e.g. for div=16, mask= 0xF0
// get the pointer to the image buffer
uchar *data= image.data;
for (int j=0; j<nr; j++) {
for (int i=0; i<nc; i++) {
*(data+i)= *data&mask + div/2;
} // end of row
data+= step; // next line
}
}
2.4 近似多边形
- 函数原型
void cv::approxPolyDP (InputArray curve,
OutputArray approxCurve,
double epsilon,
bool closed )
- 功能:把一个连续光滑曲线折线化
- 参数说明
参数 | 说明 |
---|---|
curve | 存储在std::vector或Mat中的2D Point数组 |
approxCurve | 近似多边形的2D Point数组,数据类型与curve一致 |
epsilon | 指定近似精度的参数,即原始曲线与其近似边之间的最大距离 |
closed | 如果为真,则近似多边形是闭合的(其第一个顶点和最后一个顶点是连接的)。否则,它不会关闭。 |
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <math.h>
#include <iostream>
using namespace cv;
using namespace std;
static void help(char** argv)
{
cout
<< "\nThis program illustrates the use of findContours and drawContours\n"
<< "The original image is put up along with the image of drawn contours\n"
<< "Usage:\n";
cout
<< argv[0]
<< "\nA trackbar is put up which controls the contour level from -3 to 3\n"
<< endl;
}
const int w = 500;
int levels = 3;
vector<vector<Point> > contours;
vector<Vec4i> hierarchy;
static void on_trackbar(int, void*)
{
Mat cnt_img = Mat::zeros(w, w, CV_8UC3);
int _levels = levels - 3;
drawContours( cnt_img, contours, _levels <= 0 ? 3 : -1, Scalar(128,255,255),
3, LINE_AA, hierarchy, std::abs(_levels) );
imshow("contours", cnt_img);
}
int main( int argc, char** argv)
{
cv::CommandLineParser parser(argc, argv, "{help h||}");
if (parser.has("help"))
{
help(argv);
return 0;
}
Mat img = Mat::zeros(w, w, CV_8UC1);
//Draw 6 faces
for( int i = 0; i < 6; i++ )
{
int dx = (i%2)*250 - 30;
int dy = (i/2)*150;
const Scalar white = Scalar(255);
const Scalar black = Scalar(0);
if( i == 0 )
{
for( int j = 0; j <= 10; j++ )
{
double angle = (j+5)*CV_PI/21;
line(img, Point(cvRound(dx+100+j*10-80*cos(angle)),
cvRound(dy+100-90*sin(angle))),
Point(cvRound(dx+100+j*10-30*cos(angle)),
cvRound(dy+100-30*sin(angle))), white, 1, 8, 0);
}
}
ellipse( img, Point(dx+150, dy+100), Size(100,70), 0, 0, 360, white, -1, 8, 0 );
ellipse( img, Point(dx+115, dy+70), Size(30,20), 0, 0, 360, black, -1, 8, 0 );
ellipse( img, Point(dx+185, dy+70), Size(30,20), 0, 0, 360, black, -1, 8, 0 );
ellipse( img, Point(dx+115, dy+70), Size(15,15), 0, 0, 360, white, -1, 8, 0 );
ellipse( img, Point(dx+185, dy+70), Size(15,15), 0, 0, 360, white, -1, 8, 0 );
ellipse( img, Point(dx+115, dy+70), Size(5,5), 0, 0, 360, black, -1, 8, 0 );
ellipse( img, Point(dx+185, dy+70), Size(5,5), 0, 0, 360, black, -1, 8, 0 );
ellipse( img, Point(dx+150, dy+100), Size(10,5), 0, 0, 360, black, -1, 8, 0 );
ellipse( img, Point(dx+150, dy+150), Size(40,10), 0, 0, 360, black, -1, 8, 0 );
ellipse( img, Point(dx+27, dy+100), Size(20,35), 0, 0, 360, white, -1, 8, 0 );
ellipse( img, Point(dx+273, dy+100), Size(20,35), 0, 0, 360, white, -1, 8, 0 );
}
//show the faces
namedWindow( "image", 1 );
imshow( "image", img );
//Extract the contours so that
vector<vector<Point> > contours0;
findContours( img, contours0, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);
contours.resize(contours0.size());
for( size_t k = 0; k < contours0.size(); k++ )
approxPolyDP(Mat(contours0[k]), contours[k], 3, true);
namedWindow( "contours", 1 );
createTrackbar( "levels+3", "contours", &levels, 7, on_trackbar );
on_trackbar(0,0);
waitKey();
return 0;
}
2.5 查找图形中的矩形
#include "opencv2/core.hpp"
#include "opencv2/core/ocl.hpp"
#include "opencv2/core/utility.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;
int thresh = 50, N = 11;
const char* wndname = "Square Detection Demo";
// helper function:
// finds a cosine of angle between vectors
// from pt0->pt1 and from pt0->pt2
static double angle( Point pt1, Point pt2, Point pt0 )
{
double dx1 = pt1.x - pt0.x;
double dy1 = pt1.y - pt0.y;
double dx2 = pt2.x - pt0.x;
double dy2 = pt2.y - pt0.y;
return (dx1*dx2 + dy1*dy2)/sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10);
}
// returns sequence of squares detected on the image.
static void findSquares( const UMat& image, vector<vector<Point> >& squares )
{
squares.clear();
UMat pyr, timg, gray0(image.size(), CV_8U), gray;
// down-scale and upscale the image to filter out the noise
pyrDown(image, pyr, Size(image.cols/2, image.rows/2));
pyrUp(pyr, timg, image.size());
vector<vector<Point> > contours;
// find squares in every color plane of the image
for( int c = 0; c < 3; c++ )
{
int ch[] = {c, 0};
mixChannels(timg, gray0, ch, 1);
// try several threshold levels
for( int l = 0; l < N; l++ )
{
// hack: use Canny instead of zero threshold level.
// Canny helps to catch squares with gradient shading
if( l == 0 )
{
// apply Canny. Take the upper threshold from slider
// and set the lower to 0 (which forces edges merging)
Canny(gray0, gray, 0, thresh, 5);
// dilate canny output to remove potential
// holes between edge segments
dilate(gray, gray, UMat(), Point(-1,-1));
}
else
{
// apply threshold if l!=0:
// tgray(x,y) = gray(x,y) < (l+1)*255/N ? 255 : 0
threshold(gray0, gray, (l+1)*255/N, 255, THRESH_BINARY);
}
// find contours and store them all as a list
findContours(gray, contours, RETR_LIST, CHAIN_APPROX_SIMPLE);
vector<Point> approx;
// test each contour
for( size_t i = 0; i < contours.size(); i++ )
{
// approximate contour with accuracy proportional
// to the contour perimeter
approxPolyDP(contours[i], approx, arcLength(contours[i], true)*0.02, true);
// square contours should have 4 vertices after approximation
// relatively large area (to filter out noisy contours)
// and be convex.
// Note: absolute value of an area is used because
// area may be positive or negative - in accordance with the
// contour orientation
if( approx.size() == 4 &&
fabs(contourArea(approx)) > 1000 &&
isContourConvex(approx) )
{
double maxCosine = 0;
for( int j = 2; j < 5; j++ )
{
// find the maximum cosine of the angle between joint edges
double cosine = fabs(angle(approx[j%4], approx[j-2], approx[j-1]));
maxCosine = MAX(maxCosine, cosine);
}
// if cosines of all angles are small
// (all angles are ~90 degree) then write quandrange
// vertices to resultant sequence
if( maxCosine < 0.3 )
squares.push_back(approx);
}
}
}
}
}
// the function draws all the squares in the image
static void drawSquares( UMat& _image, const vector<vector<Point> >& squares )
{
Mat image = _image.getMat(ACCESS_WRITE);
for( size_t i = 0; i < squares.size(); i++ )
{
const Point* p = &squares[i][0];
int n = (int)squares[i].size();
polylines(image, &p, &n, 1, true, Scalar(0,255,0), 3, LINE_AA);
}
}
2.6 排序
2.6.1 从左到右且从上到下排序contours
vector<vector<Point>> contours(4);
contours[0].push_back(Point(3,111));
contours[0].push_back(Point(3,121));
contours[1].push_back(Point(81,13));
contours[1].push_back(Point(84,14));
contours[2].push_back(Point(33,55));
contours[2].push_back(Point(36,57));
contours[3].push_back(Point(133,25));
contours[3].push_back(Point(136,27));
for ( int i=0; i<contours.size(); i++ )
cerr << Mat(contours[i]) << endl;
struct contour_sorter // 'less' for contours
{
bool operator ()( const vector<Point>& a, const vector<Point> & b )
{
Rect ra(boundingRect(a));
Rect rb(boundingRect(b));
// scale factor for y should be larger than img.width
return ( (ra.x + 1000*ra.y) < (rb.x + 1000*rb.y) );
}
};
// apply it to the contours:
std::sort(contours.begin(), contours.end(), contour_sorter());
for ( int i=0; i<contours.size(); i++ )
cerr << Mat(contours[i]) << endl;
[3, 111; 3, 121]
[81, 13; 84, 14]
[33, 55; 36, 57]
[133, 25; 136, 27]
[81, 13; 84, 14]
[133, 25; 136, 27]
[33, 55; 36, 57]
[3, 111; 3, 121]
2.7 图像滤波
- 概念
- 图像滤波的原理:在尽量保留图像细节特征的条件下对目标图像的噪声进行抑制,通常是数字图像处理中不可缺少的操作,其处理效果的好坏将直接影响到后续运算和分析的效果。
- 滤波:是将信号中特定频率滤除的操作
- 图像滤波的目的:是在图像中提取出人类感兴趣的特征。
- 滤波器:可以将原始信号的有用信息通过各种组合来凸显出来
- 当我们观察一幅图像时,有两种处理方法:
- 观察不同的灰度(或彩色值)在图像中的分布情况,即空间分布。
- 观察图像中的灰度(或彩色值)的变化情况,这涉及到频率方面的问题。
- 图像滤波分为频域和空域滤波:
- 空 间域:指用图像的灰度值来描述一幅图像;
- 频率域:指用图像的灰度值的变化来描述一幅图像。
- 频域滤波
- 低通滤波器和高通滤波器的概念就是在频域中产生的。
- 低通滤波器:旨在去除图像中的高频成分(变化量较大的部分)
- 高通滤波器:旨在去除了图像中的低频成分(变化量较小的部分)
- 高频与低频
- 高频部分:是指图像中像素值落差很大的部分
- 低频部分:是指像素值与旁边的像素值相差不大甚至相同
- 而图像的一些细节的部分往往由高频信息来展现,图像中掺杂的噪声往往也处于高频段,这就造成了一些细节信息被噪声淹没
- 可以根据不同的噪声类型用不同的滤波器进行处理
- 低通滤波器
- 消除图像中的噪声成分叫作图像的平滑或低通滤波
- 信号或图像的能量大部分集中在幅度谱的低频和中频段是很常见的,而在较高频段,感兴趣的信息经常被噪声淹没
- 图像滤波的目的:
- 一是抽出对象的特征作为图像识别的特征模式;
- 另一个是为适应图像处理的要求,消除图像数字化时所混入的噪声;
- 在设计低通滤波器时,要考虑到滤波对图像造成的细节丢失等问题。
- 平滑滤波
- 是低频增强的空间域滤波技术。
- 目的:
- 一类是图像模糊;
- 另一类是滤除图像噪声。
- 空间域的平滑滤波一般采用简单平均法进行,就是求邻近像素点的平均灰度值或亮度值。邻域的大小与平滑的效果直接相关,邻域越大平滑的效果越好,但邻域过大,平滑会使边缘信息损失的越大,从而使输出的图像变得模糊,因此需合理选择邻域的大小。
- 线性滤波器与非线性滤波器的区别
- 线性滤波器 (算术运算)
- 是原始数据与滤波结果是一种算术运算,即用加减乘除等运算实现
- 如均值滤波器(模板内像素灰度值的平均值)、高斯滤波器(高斯加权平均值)等
- 由于线性滤波器是算术运算,有固定的模板,因此滤波器的转移函数是可以确定并且是唯一的(转移函数即模板的傅里叶变换)
- 线性滤波器经常用于剔除输入信号中不想要的频率或者从许多频率中选择一个想要的频率
- 非线性滤波器 (逻辑运算)
- 是原始数据与滤波结果是一种逻辑运算,即用逻辑运算实现,
- 如最大值滤波器、最小值滤波器、中值滤波器等,是通过比较一定邻域内的灰度值大小来实现的
- 没有固定的模板,因而也就没有特定的转移函数(因为没有模板作傅里叶变换)
- 膨胀和腐蚀也是通过最大值、最小值滤波器实现的
- 线性滤波器 (算术运算)
- OpenCV提供的平滑滤波器
- 低通:就是模糊
- 高通:就是锐化
序号 | 名称 | 函数名 | 线性 | 高低通 | 说明 |
---|---|---|---|---|---|
1 | 均值滤波 | blur | 线性 算术运算 | 低通 | blur(image,result,cv::Size(5,5)); 与boxFilter(src, boxFilterDst, -1, cv::Size(5, 5));效果一样 平滑/模糊图像(损失细节),但会引入噪声, 取平均值 |
2 | 方框滤波 | boxFilter | 线性 算术运算 | 低通 | boxFilter(image,result, -1, cv::Size(5,5),Point(-1,-1), true/false); boxFilter(src, boxFilterDst, -1, cv::Size(5, 5)); 当true/false设置为true时,等效于blur Point(-1,-1):表示锚点(即被平滑的那个点) |
3 | 高斯滤波 | GaussianBlur | 线性 算术运算 | 低通/高通 | GaussianBlur(image,result,cv::Size(5,5),1.5, 1.5); 1.5, 1.5分别为 σ x 、 σ y \sigma_x、\sigma_y σx、σy 平滑/模糊图像,但会引入噪声, 取加权值 |
4 | 中值滤波 | medianBlur | 非线性 逻辑运算 | 低通 | medianBlur(image,result,5); 用于去除脉冲噪声、椒盐噪声的同时又能保留图像边缘细节 |
5 | 双边滤波 | bilateralFilter | 非线性 逻辑运算 | 低通 | bilateralFilter( image, result, 25, 25*2, 25/2 ); bilateralFilter(image, result, int d, double sigmaColor, double sigmaSpace) 同时考虑空域信息和灰度相似性,达到保边去噪的目的 双边滤波器的好处是可以做边缘保存(edge preserving) 它比高斯滤波多了一个高斯方差 σ d \sigma_d σd |
- boxFilter算法
K = α [ 1 1 ⋯ 1 1 1 1 ⋯ 1 1 ⋮ ⋮ ⋱ ⋮ ⋮ 1 1 ⋯ 1 1 ] K = \alpha \begin{bmatrix} 1 & 1 & \cdots & 1 & 1 \\ 1 & 1 & \cdots & 1 & 1 \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ 1 & 1 & \cdots & 1 & 1 \\ \end{bmatrix} K=α⎣⎢⎢⎢⎡11⋮111⋮1⋯⋯⋱⋯11⋮111⋮1⎦⎥⎥⎥⎤
α = { 1 k s i z e . w i d t h × k s i z e . h e i g h t , when normalize = true, 相当于blur 1 , otherwise \alpha = \begin{cases} \frac{1}{ksize.width \times ksize.height}, & \text{when normalize = true, 相当于blur} \\ 1, & \text{otherwise} \end{cases} α={ksize.width×ksize.height1,1,when normalize = true, 相当于blurotherwise - 中值滤波器与均值滤波器比较
- 优势:
- 在均值滤波器中,由于噪声成分被放入平均计算中,所以输出受到了噪声的影响
- 在中值滤波器中,由于噪声成分很难选上,所以几乎不会影响到输出
- 同样用3x3区域进行处理,中值滤波消除的噪声能力更胜一筹
- 中值滤波无论是在消除噪声还是保存边缘方面都是一个不错的方法
- 劣势:
- 中值滤波花费的时间是均值滤波的5倍以上
- 优势:
- 参考代码
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
using namespace cv;
using namespace std;
Mat g_srcImage, g_dstImage1, g_dstImage2, g_dstImage3, g_dstImage4, g_dstImage5;
int g_nBoxFilterValue = 6; //方框滤波的内核值
int g_nMeanBlurValue = 10; //均值滤波的内核值
int g_nGaussianBlurValue = 6; //高斯滤波内核值
int g_nMedianBlurBlurValue = 10; //中值滤波参数
int g_nBilateralFiterValue = 50; //双边滤波参数值
//声明滚动条回调函数
static void on_BoxFilter(int, void*); //方框
static void on_MeanBulr(int, void*); //均值
static void on_GaussianBulr(int, void*); //高斯
static void on_MedianBlur(int, void*); //中值
static void on_BilateralFiter(int, void*); //双边
int main()
{
g_srcImage = imread("1.jpg");
if (!g_srcImage.data)
{
printf("图片载入失败!\n");
return -1;
}
g_dstImage1 = g_srcImage.clone();
g_dstImage2 = g_srcImage.clone();
g_dstImage3 = g_srcImage.clone();
g_dstImage4 = g_srcImage.clone();
g_dstImage5 = g_srcImage.clone();
//显示原图
namedWindow("原图");
imshow("原图", g_srcImage);
//方框滤波
namedWindow("方框滤波");
//创建滚动条
createTrackbar("内核值:", "方框滤波", &g_nBoxFilterValue, 50, on_BoxFilter);
on_BoxFilter(g_nBoxFilterValue, 0);
imshow("方框滤波", g_dstImage1);
//均值滤波
namedWindow("均值滤波");
createTrackbar("内核值:", "均值滤波", &g_nMeanBlurValue, 50, on_MeanBulr);
on_MeanBulr(g_nMeanBlurValue, 0);
imshow("均值滤波", g_dstImage2);
//高斯
namedWindow("高斯滤波");
createTrackbar("内核值:", "高斯滤波", &g_nGaussianBlurValue, 50, on_GaussianBulr);
on_GaussianBulr(g_nGaussianBlurValue, 0);
imshow("高斯滤波", g_dstImage3);
//中值
namedWindow("中值滤波");
createTrackbar("内核值:", "中值滤波", &g_nMedianBlurBlurValue, 50, on_MedianBlur);
on_MedianBlur(g_nMedianBlurBlurValue, 0);
imshow("中值滤波", g_dstImage4);
//双边
namedWindow("双边滤波");
createTrackbar("内核值:", "双边滤波", &g_nBilateralFiterValue, 50, on_BilateralFiter);
on_BilateralFiter(g_nBilateralFiterValue, 0);
imshow("双边滤波", g_dstImage5);
waitKey(0);
return 0;
}
//回调函数
static void on_BoxFilter(int, void*)
{
//方框滤波操作
boxFilter(g_srcImage, g_dstImage1, -1, Size(g_nBoxFilterValue + 1, g_nBoxFilterValue + 1));
//显示
imshow("方框滤波", g_dstImage1);
}
static void on_MeanBulr(int, void*)
{
blur(g_srcImage, g_dstImage2, Size(g_nMeanBlurValue + 1, g_nMeanBlurValue + 1));
imshow("均值滤波", g_dstImage2);
}
static void on_GaussianBulr(int, void*)
{
GaussianBlur(g_srcImage, g_dstImage3, Size(g_nGaussianBlurValue * 2 + 1, g_nGaussianBlurValue * 2 + 1),0,0); //大于1的奇数
imshow("高斯滤波", g_dstImage3);
}
static void on_MedianBlur(int, void*)
{
medianBlur(g_srcImage, g_dstImage4, g_nMedianBlurBlurValue * 2 + 1);
imshow("中值滤波", g_dstImage4);
}
static void on_BilateralFiter(int, void*)
{
bilateralFilter(g_srcImage, g_dstImage5, g_nBilateralFiterValue, g_nBilateralFiterValue * 2, g_nBilateralFiterValue / 2);
imshow("双边滤波", g_dstImage5);
}
2.8 图像形态学
- 形态学操作就是基于形状的一系列图像处理操作。OpenCV为进行图像的形态学变换提供了快捷、方便的函数。最基本的形态学操作有二种,他们是:膨胀与腐蚀(Dilation与Erosion)。
2.8.1 膨胀腐蚀
- 作用域
- 腐蚀和膨胀:是对白色部分(高亮部分)而言的,不是黑色部分。
- 膨胀:就是图像中的高亮部分进行膨胀,“领域扩张”,效果图拥有比原图更大的高亮区域。
- 腐蚀:就是原图中的高亮部分被腐蚀,“领域被蚕食”,效果图拥有比原图更小的高亮区域。
- 膨胀腐蚀主要功能
- 消除噪声
- 分割(isolate)出独立的图像元素,在图像中连接(join)相邻的元素
- 寻找图像中的明显的极大值区域或极小值区域
- 求出图像的梯度
- 膨胀工作原理(补洞:消除黑色的洞)
- 膨胀就是求局部最大值的操作,核B与图形卷积,即计算核B覆盖的区域的像素点的最大值,并把这个最大值赋值给参考点指定的像素;这样就会使图像中的高亮区域逐渐增长。
- 公式
d s t ( x , y ) = max ( x ′ , y ′ ) : e l e m e n t ( x ′ , y ′ ) ≠ 0 s r c ( x + x ′ , y + y ′ ) dst(x,y) = \max_{(x',y'):element(x',y') \ne 0} src(x+x', y+y') dst(x,y)=(x′,y′):element(x′,y′)=0maxsrc(x+x′,y+y′) - 函数定义
int g_nStructElementSize = 3; //结构元素(内核矩阵)的尺寸
//获取自定义核
Mat element = getStructuringElement(MORPH_RECT,
Size(2*g_nStructElementSize+1,2*g_nStructElementSize+1),
Point( g_nStructElementSize, g_nStructElementSize ));
- 示例代码
Mat img = imread("1.jpg");
// shape: 矩形: MORPH_RECT, 交叉形: MORPH_CROSS, 椭圆形: MORPH_ELLIPSE
Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
Mat out;
dilate(img, out, element);
- 腐蚀工作原理 (滤波:消除亮的噪点)
- 是与膨胀相反的一个操作,腐蚀就是求局部最小值的操作
d s t ( x , y ) = min ( x ′ , y ′ ) : e l e m e n t ( x ′ , y ′ ) ≠ 0 s r c ( x + x ′ , y + y ′ ) dst(x,y) = \min_{(x',y'):element(x',y') \ne 0} src(x+x', y+y') dst(x,y)=(x′,y′):element(x′,y′)=0minsrc(x+x′,y+y′) - 函数定义
- 是与膨胀相反的一个操作,腐蚀就是求局部最小值的操作
C++: void erode(
InputArray src,
OutputArray dst,
InputArray kernel,
Point anchor=Point(-1,-1),
int iterations=1,
int borderType=BORDER_CONSTANT,
const Scalar& borderValue=morphologyDefaultBorderValue()
);
- 示例代码
Mat img = imread("1.jpg");
// shape: 矩形: MORPH_RECT, 交叉形: MORPH_CROSS, 椭圆形: MORPH_ELLIPSE
Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
Mat out;
erode(img, out, element);
2.8.2 开运算(Opening Operation)
- 函数定义
C++: void morphologyEx(
InputArray src,
OutputArray dst,
int op, // MORPH_OPEN, MORPH_CLOSE, MORPH_GRADIENT, MORPH_TOPHAT, MORPH_BLACKHAT
InputArraykernel,
Pointanchor=Point(-1,-1),
intiterations=1,
intborderType=BORDER_CONSTANT,
constScalar& borderValue=morphologyDefaultBorderValue() );
- 开运算(Opening Operation):其实就是先腐蚀后膨胀的过程,其公式如下:
d s t = o p e n ( s r c , e l e m e n t ) = d i l a t e ( e r o d e ( s r c , e l e m e n t ) ) dst = open(src, element) = dilate(erode(src,element)) dst=open(src,element)=dilate(erode(src,element)) - 用途
- 消除小物体
- 在纤细点处分离物体
- 平滑较大物体的边界的同时并不明显改变其面积
- 示例代码
Mat image = imread("1.jpg"); //工程目录下应该有一张名为1.jpg的素材图
//定义核
Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
//进行形态学操作
morphologyEx(image,image, MORPH_OPEN, element);
2.8.3 闭运算(Closing Operation)
- 闭运算:先膨胀后腐蚀的过程,其公式如下:
- 用途:闭运算能够排除小型黑洞(黑色区域)
- 示例代码
Mat image = imread("1.jpg"); //工程目录下应该有一张名为1.jpg的素材图
//定义核
Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
//进行形态学操作
morphologyEx(image,image, MORPH_CLOSE, element);
2.9 边缘检测
2.9.1边缘检测的步骤
- 滤波:
- 边缘检测的算法主要是基于图像强度的一阶和二阶导数
- 导数通常对噪声很敏感,因此必须采用滤波器来改善与噪声有关的边缘检测器的性能。
- 常见的滤波方法主要有高斯滤波,即采用离散化的高斯函数产生一组归一化的高斯核,然后基于高斯核函数对图像灰度矩阵的每一点进行加权求和
- 增强
- 增强边缘的基础是确定图像各点邻域强度的变化值。增强算法可以将图像灰度点邻域强度值有显著变化的点凸显出来
- 实际工程中,可通过计算梯度幅值来确定
- 检测
- 经过增强的图像,往往邻域中有很多点的梯度值比较大,而在特定的应用中,这些点并不是我们要找的边缘点,所以应该采用某种方法来对这些点进行取舍
- 实际工程中,常用的方法是通过阈值化方法来检测。
2.9.2 最优边缘检测的三个主要评价标准
- 低错误率: 标识出尽可能多的实际边缘,同时尽可能的减少噪声产生的误报。
- 高定位性: 标识出的边缘要与图像中的实际边缘尽可能接近。
- 最小响应: 图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。
2.9.3 边缘检测算法
序号 | 名称 | 函数名 | 线性 | 高低通 | 说明 |
---|---|---|---|---|---|
1 | Canny | Canny | 线性 | 高通 | Canny(image, edges, 3, 9, 3); |
2 | |||||
3 | |||||
6 | 拉普拉斯滤波 | 线性 | 高通 |
- Canny
- 函数定义: Canny(InputArray image,OutputArray edges, double threshold1, double threshold2, int apertureSize=3,bool L2gradient=false )
- image:输入图像,填Mat类的对象即可,且需为单通道8位图像
- threshold1:第一个滞后性阈值
- threshold2:第二个滞后性阈值
- apertureSize:表示应用Sobel算子的孔径大小,其有默认值3
- threshold2与threshold1:这二个阈值中当中的小阈值用来控制边缘连接,大的阈值用来控制强边缘的初始分割即如果一个像素的梯度大与上限值,则被认为是边缘像素,如果小于下限阈值,则被抛弃。如果该点的梯度在两者之间则当这个点与高于上限值的像素点连接时我们才保留
- 实现步骤:
- 消除噪声:一般情况下,使用高斯平滑滤波器卷积降噪
- 计算梯度幅值和方向:按照Sobel滤波器的步骤
- 非极大值抑制:排除非边缘像素, 仅仅保留了一些细线条(候选边缘)
- 滞后阈值:Canny 使用了滞后阈值,滞后阈值需要两个阈值(高阈值和低阈值)
- 如果某一像素位置的幅值超过高阈值, 该像素被保留为边缘像素。
- 如果某一像素位置的幅值小于低阈值, 该像素被排除
- 如果某一像素位置的幅值在两个阈值之间,该像素仅仅在连接到一个高于高阈值的像素时被保留
- 函数定义: Canny(InputArray image,OutputArray edges, double threshold1, double threshold2, int apertureSize=3,bool L2gradient=false )
2.10 几何变换
2.10.1 图像重映射remap
- 重映射:就是把一幅图像中某位置的像素放置到另一个图像中指定位置的过程
- 说明:在重映射过程中,图像的大小可以发生变化,此时像素与像素之间的关系就不是一一对就关系,因此在重映射过程中,可能涉及到像素值的插值计算
- 公式:
d s t ( x , y ) = s r c ( m a p x ( x , y ) , m a p y ( x , y ) ) dst(x,y) = src(map_x(x,y), map_y(x,y)) dst(x,y)=src(mapx(x,y),mapy(x,y)) - 函数定义
void cv::remap (InputArray src,
OutputArray dst,
InputArray map1,
InputArray map2,
int interpolation,
int borderMode = BORDER_CONSTANT,
const Scalar & borderValue = Scalar()
)
- 参数说明
参数 | 描述 |
---|---|
src | 源图像 |
dst | 目标图像 |
map1 | 第一个映射,或者为(x,y)点的映射,或为x值的映射 数据类型:CV_16SC2 , CV_32FC1, or CV_32FC2 |
map2 | 第二个映射,或者为空(当map1为(x,y)点),或者为y值的映射 数据类型: CV_16UC1, CV_32FC1 |
interpolation | 插值方法: INTER_NEAREST : 最近邻插值 INTER_LINEAR :双线性插值 INTER_CUBIC:双三次插值 |
borderMode | 像素外推法: BORDER_CONSTANT : 常量 iiiiii|abcdefgh|iiiiiii (i的值由最后一个参数Scalar()确定,如Scalar::all(0) BORDER_REPLICATE :边界复制 aaaaaa|abcdefgh|hhhhhhh BORDER_REFLECT :边界反射 fedcba|abcdefgh|hgfedcb BORDER_WRAP :边界包裹 cdefgh|abcdefgh|abcdefg BORDER_REFLECT_101 :gfedcb|abcdefgh|gfedcba BORDER_TRANSPARENT:边界透明 uvwxyz|abcdefgh|ijklmno |
borderValue | 常量边界所使用的值,默认值为 0 |
2.10.2 畸变校正(undistort)
- 功能:变换图像以补偿镜头失真,即变换图像以补偿径向和切向透镜畸变。
- 与remap的关系:
- undistort是initUndistortRectifyMap()和remap()的简单组合,效果是一样的
- 当你有很多畸变图像需要较正时,用undistort()函数的缺点就暴露了。因为畸变坐标映射矩阵mapx和mapy只需要计算一次就足够了,而重复调用undistort()只会重复计算mapx和mapy,严重影响程序效率。
- 因此当有多张图片要畸变校正时,建议使用一次initUndistortRectifyMap(),获取畸变坐标映射矩阵mapx和mapy后,作为remap函数的输入,多次调用remap函数进行畸变校正。
- 函数定义
void cv::undistort (InputArray src,
OutputArray dst,
InputArray cameraMatrix,
InputArray distCoeffs,
InputArray newCameraMatrix = noArray()
)
- 参数说明
参数 | 说明 |
---|---|
src | 源图像(失真图像) |
dst | 目标图像(校正后的图像),尺寸与src的一样 |
cameraMatrix | 内参: A = [ f x 0 c x 0 f y c y 0 0 1 ] A=\begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix} A=⎣⎡fx000fy0cxcy1⎦⎤ |
distCoeffs | 畸变系数向量:
(
k
1
,
k
2
,
p
1
,
p
2
,
[
,
k
3
[
,
k
4
,
k
5
,
k
6
[
,
s
1
,
s
2
,
s
3
,
s
4
[
,
τ
x
,
τ
y
]
]
]
]
)
(k_1, k_2, p_1, p_2, [,k_3[,k_4,k_5,k_6[,s_1,s_2,s_3,s_4[,τ_x,τ_y]]]])
(k1,k2,p1,p2,[,k3[,k4,k5,k6[,s1,s2,s3,s4[,τx,τy]]]]) 参数个数可为:4,5,8,12或14 |
newCameraMatrix | 失真图像的相机矩阵 |