OpenCV4 常用技巧

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
    max=MAX(r,g,b)min=MIN(r,g,b)H={00,if max = min600×gbmaxmin+00if max == r and g >= b600×gbmaxmin+3600if max == r and g < b600×brmaxmin+1200if max == g600×rgmaxmin+2400if max == bS=(maxmin)/max/255V=max/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}
  • 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=(x1,y1),b=(x2,y2)a=(x_1, y_1), b=(x_2, y_2)
    cos(θ)=x1x2+y1y2x12+y12x22+y22cos(\theta) = \frac{x_1x_2 + y_1y_2}{\sqrt{x_1^2+y_1^2} \cdot \sqrt {x_2^2 + y_2^2}}

1.2.2 三維向量

  • 三維向量:a=(x1,y1,z1),b=(x2,y2,z2)a=(x_1, y_1, z_1), b=(x_2, y_2, z_2)
    cos(θ)=x1x2+y1y2+z1z2x12+y12+z12x22+y22+z22cos(\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}}

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示例代碼

#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。
參數 說明
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
平滑/模糊圖像,但會引入噪聲, 取加權值
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
  • boxFilter算法
    K=α[111111111111]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}
    α={1ksize.width×ksize.height,when normalize = true, 相當於blur1,otherwise \alpha = \begin{cases} \frac{1}{ksize.width \times ksize.height}, & \text{when normalize = true, 相當於blur} \\ 1, & \text{otherwise} \end{cases}
  • 中值濾波器與均值濾波器比較
    • 優勢
      • 在均值濾波器中,由於噪聲成分被放入平均計算中,所以輸出受到了噪聲的影響
      • 在中值濾波器中,由於噪聲成分很難選上,所以幾乎不會影響到輸出
      • 同樣用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覆蓋的區域的像素點的最大值,並把這個最大值賦值給參考點指定的像素;這樣就會使圖像中的高亮區域逐漸增長。
    • 公式
      dst(x,y)=max(x,y):element(x,y)0src(x+x,y+y)dst(x,y) = \max_{(x',y'):element(x',y') \ne 0} src(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);
  • 腐蝕工作原理 (濾波:消除亮的噪點)
    • 是與膨脹相反的一個操作,腐蝕就是求局部最小值的操作
      dst(x,y)=min(x,y):element(x,y)0src(x+x,y+y)dst(x,y) = \min_{(x',y'):element(x',y') \ne 0} src(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):其實就是先腐蝕後膨脹的過程,其公式如下:
    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 使用了滯後閾值,滯後閾值需要兩個閾值(高閾值和低閾值)
        • 如果某一像素位置的幅值超過閾值, 該像素被保留爲邊緣像素。
        • 如果某一像素位置的幅值小於閾值, 該像素被排除
        • 如果某一像素位置的幅值在兩個閾值之間,該像素僅僅在連接到一個高於高閾值的像素時被保留

2.10 幾何變換

2.10.1 圖像重映射remap

  • 重映射:就是把一幅圖像中某位置的像素放置到另一個圖像中指定位置的過程
  • 說明:在重映射過程中,圖像的大小可以發生變化,此時像素與像素之間的關係就不是一一對就關係,因此在重映射過程中,可能涉及到像素值的插值計算
  • 公式
    dst(x,y)=src(mapx(x,y),mapy(x,y))dst(x,y) = src(map_x(x,y), map_y(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=[fx0cx0fycy001]A=\begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix}
distCoeffs 畸變係數向量:(k1,k2,p1,p2,[,k3[,k4,k5,k6[,s1,s2,s3,s4[,τ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]]]])
參數個數可爲:4,5,8,12或14
newCameraMatrix 失真圖像的相機矩陣
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章