目錄
預備知識
濾波、核和卷積
濾波器指的是一種由一幅圖像 I(x,y)根據像素點x,y附近的區域計算得到一幅新圖像 I'(x,y)的算法。其中,模板規定了濾波器的形狀以及這個區域內像素的值的組成規律,也稱“濾波器”或“核”。在下面的介紹中多采用的是線性核,即 I'(x,y)的像素的值由 I(x,y)及其周圍的像素的值的加權相加得來的。可由以下方程表示:
(A)5×5盒狀核 | (B)規範化的5×5盒狀核 | (C)3×3的Sobel核 | (D)5×5規範化高斯核 |
注:“錨點”均用粗體表示
邊界外推和邊界處理
自定義邊框
在處理圖像時,只要告訴調用的函數添加虛擬像素的規則,庫函數就會自動創建虛擬像素。cv::copyMakeBorder()就是一個爲圖像創建邊框的函數。
cv::copyMakeBorder(InputArray src, InputArray dst, int top, int bottom, int left, int right, int borderType, const cv::Scalar& value=cv::Scalar())
作用:通過指定兩幅圖像,同時指明填充方法,該函數就會將第一幅圖填補後的結果保存在第二幅圖像中。其中,src是原圖像,dst是填充後的圖像,top、bottom、left、right分別是四個方向上的尺寸,borderType是像素填充的方式,value是常量填充時的值。
取值 | 效果 |
---|---|
cv::BORDER_CONSTANT | 爲每個邊框像素賦予一個相同的值。 |
cv::BORDER_WRAP | 類似於平鋪擴充 |
cv::BORDER_REPLICATE | 複製邊緣的像素擴充 |
cv::BORDER_REFLECT | 通過鏡像複製擴充 |
cv::BORDER_REFLECT_101 | 通過鏡像複製擴充,邊界像素除外 |
cv::BORDER_DEFAULT | cv::BORDER_REFLECT_101 |
自定義外推
int cv::borderInterpolate(int p, int len, int borderType)
作用:計算一個維度上的外推,p爲原圖上一個座標,len是p指維度上的大小,borderType是邊界類型。
例子:混合的邊界條件下計算一個特定像素的值,在一維中使用BORDER_REFLECT_101,在二維中使用BORDER_WRAP:
float val = img.at<float>(
cv::borderInterpolate(100, img.rows, cv::BORDER_REFLECT_101),
cv::borderInterpolate(-5, img.cols, cv::BORDER_WRAP)
)
閾值化操作
閾值化操作的原理是對於數組中每個值,根據其高於或低於某個閾值做出相對應的處理,OpenCV中提供了實現這種功能的方法cv::threshold()。
double cv::threshold(InputArray src, cv::OutputArray dst, double thresh, double maxValue, int thresholdType)
閾值類型 | 操作 |
---|---|
cv::THRESH_BINARY | |
cv::THRESH_BINARY_INV | |
cv::THRESH_TRUNC | |
cv::THRESH_TOZERO | |
cv::THRESH_TOZERO_INV |
例一:將一幅圖像的三個通道相加並將像素值限制在100以內
#include "stdafx.h"
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
void sum_rgb(const cv::Mat& src, cv::Mat& dst) {
// 分通道
vector<cv::Mat> planes;
cv::split(src, planes);
cv::Mat b = planes[0];
cv::Mat g = planes[1];
cv::Mat r = planes[2];
cv::Mat s;
// 加權融合,防止越界
cv::addWeighted(r, 1. / 3, g, 1. / 3, 0.0, s);
cv::addWeighted(s, 1., r, 1. / 3, 0.0, s);
// 閾值化操作
cv::threshold(s, dst, 100, 100, cv::THRESH_TRUNC);
}
void help() {
cout << "Call: ./ch10_ex10_1 faceScene.jpg" << endl;
cout << "Show use of alpha blending (addWeighted) and threshold" << endl;
}
int main()
{
help();
cv::Mat src = cv::imread("D:\\personal-data\\wallpapers\\test.png");
cv::Mat dst;
if (src.empty())
{
cout << "can not load the image" << endl;
return -1;
}
sum_rgb(src, dst);
cv::imshow("img", dst);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
運行結果:
例二:對於浮點型圖像進行三個通道相加並將像素值限制在100以內
#include "stdafx.h"
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
void sum_rgb(const cv::Mat& src, cv::Mat& dst) {
// 分通道
vector<cv::Mat> planes;
cv::split(src, planes);
cv::Mat b = planes[0];
cv::Mat g = planes[1];
cv::Mat r = planes[2];
// 全0初始化s矩陣
cv::Mat s = cv::Mat::zeros(b.size(), CV_32F);
// accumulate可將8位整型的圖像累加到一幅浮點型的圖像中
cv::accumulate(b, s);
cv::accumulate(g, s);
cv::accumulate(r, s);
// 閾值化操作
cv::threshold(s, dst, 100, 100, cv::THRESH_TRUNC);
s.convertTo(dst, b.type());
}
void help() {
cout << "Call: ./ch10_ex10_1 faceScene.jpg" << endl;
cout << "Show use of alpha blending (addWeighted) and threshold" << endl;
}
int main()
{
help();
cv::Mat src = cv::imread("D:\\personal-data\\wallpapers\\test.png");
cv::Mat dst;
if (src.empty())
{
cout << "can not load the image" << endl;
return -1;
}
sum_rgb(src, dst);
cv::imshow("img", dst);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
運行結果:
Otsu算法
函數cv::threshold()也可以自動決定最優的閾值,只需將參數thresh傳遞值cv::THRESH_OTSU即可。
簡而言之,Otsu算法就是遍歷所有可能的閾值,然後對每個閾值結果的兩類像素(低於閾值和高於閾值兩類像素)計算方差,然後計算的值,取其最小的閾值。
式中,和是根據兩類像素的數量計算而來的權重。由於要遍歷所有可能的閾值,所以這並不是一個相對高效的過程。
自適應閾值
自適應閾值方法中閾值在整個過程中自動產生變化,這由OpenCV中的cv::adaptiveThreshold()實現。
void cv::adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int threshType, int blockSize, double C)
該方法是逐個像素地計算自適應閾值T(x, y),具體計算過程是:計算每個像素位置周圍的blockSize×blockSize區域的加權平均值,然後減去常數C。求均值時所用權重和adaptiveMethod有關,若是cv::ADAPTIVE_THRESH_MEAN_C,則權重相等,若是cv::ADAPTIVE_THRESH_GAUSSIAN_C,則權重由高斯方差得到。
注:該方法只適應與單通道8位或浮點型圖像
#include "stdafx.h"
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
int main()
{
// 設置參數
double fixed_threshold = 15;
int threshold_type = 1 ? cv::THRESH_BINARY : cv::THRESH_BINARY_INV;
int adaptive_method = 1 ? cv::ADAPTIVE_THRESH_MEAN_C : cv::ADAPTIVE_THRESH_GAUSSIAN_C;
int block_size = 71;
double offset = 15;
// 以灰度圖形式加載圖片
cv::Mat Igray = cv::imread("D:\\personal-data\\wallpapers\\test.png", cv::IMREAD_GRAYSCALE);
// 判斷圖像是否加載成功
if (Igray.empty()) {
cout << "Can not load " << "D:\\personal-data\\wallpapers\\test.png" << endl;
return -1;
}
// 聲明輸出矩陣
cv::Mat It, Iat;
// 閾值化操作
cv::threshold(
Igray,
It,
fixed_threshold,
255,
threshold_type);
// 自適應閾值
cv::adaptiveThreshold(
Igray,
Iat,
255,
adaptive_method,
threshold_type,
block_size,
offset
);
// 展示結果圖像
cv::imshow("Raw", Igray);
cv::imshow("Threshold", It);
cv::imshow("Adaptive Threshold", Iat);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
運行結果:
當threshold_type=1且adaptive_method=1
原圖 | 閾值化操作 | 自適應閾值化 |
當threshold_type=0且adaptive_method=0
原圖 | 閾值化操作 | 自適應閾值化 |
平滑
平滑也稱“模糊”,是一種簡單而又常用的圖像處理操作。平滑圖像的目的有很多,但通常都是爲了減少噪聲和僞影。在降低圖像分辨率的時候,平滑也十分重要,可以防止圖片出現鋸齒狀。
簡單模糊和方框型濾波器
void cv::blur(InputArray src,OutputArray dst,Size ksize,Point anchor=Point(-1,-1),int borderType=BORDER_DEFAULT)
作用:實現簡單的濾波,目標圖像中的每個值都是原圖像中相應位置一個窗口(核)中像素的平均值,窗口的尺寸由ksize聲明;anchaor指定計算時核與源圖像的對齊方式,默認情況下anchor爲cv::Point(-1, -1),表示核相對於濾波器居中。
void cv::boxFilter(InputArray src, OutputArray dst, cv::Size ksize, cv::Point anchor=cv::Point(-1,-1), bool normalize=true; int borderType=cv::BORDER_DEFAULT)
作用:方框濾波器是一種矩形的並且濾波器中所有值全部相等的濾波器。通常,所有的爲1或者1/A,其中A是濾波器的面積。後一種濾波器稱爲“歸一化方框型濾波器”,下面所示的是一個5×5的模糊濾波器,也稱“歸一化方框型濾波器”。
通過上述介紹,可以發現cv::boxFilter()是一種一般化的形式,而cv::blur()是一種特殊化的形式。但前者可以以非歸一化形式調用,並且輸出圖像深度可以控制,但後者智能以歸一化形式調用,且輸出圖像深度必須和原圖像保持一致。
中值濾波器
中值濾波器(Median Filter)將每個像素替換爲圍繞這個像素的矩形領域內的中值或“中值”像素(相對於平均像素)。通過均值濾波器對噪聲圖像,尤其是有較大孤立的異常值非常敏感,少量具有較大偏差的點也會嚴重影響到均值濾波器。中值濾波器可以採用取其中間點的方式來消除異常值。
void cv::medianBlur(InputArray src, OutputArray dst, Size ksize)
高斯濾波器
關於高斯濾波器,在之前的文章中已做了詳細介紹,可以參考OpenCV高斯濾波GaussianBlur
雙邊濾波器
雙邊濾波器是一種比較大的圖像分析算子,也就是邊緣保持平滑。高斯平滑的模糊過程是減緩像素在空間上的變化,因此與鄰近的關係緊密,而隨機噪聲在像素間的變化幅度又會非常的大(即噪聲不是空間相關的)。基於這種前提高斯平滑很好地減弱了噪聲並且保留了小信號,但是卻破壞了邊緣信息,邊緣也模糊了。
和高斯平滑類似,雙邊濾波對每個像素及其領域內的像素進行了加權平均。其權重由兩部分組成,第一部分同高斯平滑,第二部分也是高斯權重,但是它不是基於空間距離而是色彩強度差計算而來的,在多通道(色彩)圖像上強度差由各分量的加權累加代替。可將其當做高斯平滑,指示相似程度更高的像素權值更高,邊緣更加明顯,對比度更高。
cv::bilateralFilter(InputArray src, OutputArray dst, int d, double sigmaColor, double sigmaSpace, int borderType= cv::BORDER_DEFAULT)
參數:d是像素鄰近的最大距離,處理視頻時一般不大於5,非實時應用時可放大到9;sigmaColor是色彩空間濾波器的sigma值,該值越大,則色彩強度越大,不連續性越強;sigmaSpace是座標空間濾波器的sigma值。
導數和梯度
卷積中最重要也是最基本的部分就是(近似)計算導數。
索貝爾導數
一般來說,用來表示微分的最常用的算子是索貝爾(Sobel)導數算子,可以實現任意階導數和混合偏導數。
void sv::Sobel(InputArray src, OutputArray dst, int ddepth, int xorder, int yorder, cv::Size ksize=3, double scale=1, double delta=0, int borderType=cv::BORDER_DEFAULT)
參數:ddepth指明目標圖像的深度或類型;xorder和yorder是求導順序,其取值可爲0,、1或2,其中0代表在該方向不求導,kszie是一個奇數,表示濾波器核的大小, 目前最大到31;閾值和偏移量在結果存入dst前進行調用,公式如下:
Sobel算子的好處是可以將覈定義爲各種大小,並且可以快速迭代式地構造這些核。大的核可以更好地近似導數,因爲可以消除噪聲影響。其缺點是如果導數在空間上變化劇烈,核太大會使結果發生偏差,並且核比較小時準確度不高。
實際上,由於Sobel算子定義在離散空間上,所以它並不是真正的導數,而是一個多項式,即在x方向上進行Sobel運算表示的並不是二階導數,而是對拋物線函數的局部擬合。
Scharr濾波器
爲了將圖像內的信息聯繫起來,可能需要測量一幅圖像:在處理過程中,通過在目標附近組織一幅梯度直方圖來收集其形狀信息,這些直方圖是許多形狀分類器訓練和使用的基礎。因此,梯度角的誤差會降低分類器識別的效果。
對於3×3的Sobel濾波器,梯度角距離水平或垂直方向越遠,誤差越明顯。在OpenCV中,調用cv::Sobel()時設置ksize爲cv::SCHARR,即可消除3×3這樣小但是快的Sobel導數濾波器所帶來的誤差。Scharr濾波器核Sobel濾波器同樣很快,但是前者精度更高。因此選擇3×3的濾波器時,應當使用Scharr濾波器。
或
拉普拉斯變換
OpenCV中的函數Laplacian實現了對拉普拉斯算子的離散近似:
void sv::Laplacian(InputArray src, OutputArray dst, int ddepth, cv::Size ksize=3, double scale=1, double delta=0, int borderType=cv::BORDER_DEFAULT)
只要ksize不爲1,Laplacian算子的實現就是直接計算Sobel算子響應之和。當ksize=1時的卷積核如下所示:
Laplacian算子可應用於各種場景處理,一種常見的應用就是匹配“斑點”。Laplacian算子就是圖像在x和y軸方向的導數之和,這意味着一個被較大值包圍的點或小斑點(比ksize小)處的值將會變得很大。相反,被較小值包圍的點或小斑點處的值將在負方向上變得很大。
Laplacian算子同樣可以用於邊緣檢測,函數一階導數在原函數變化大的地方,值會相應變大,同樣在圖像邊緣處也同樣變化,所有導數在這些地方將變得很大。因此可以在二階導數爲0的地方搜尋這麼一個極大值,原圖像中的邊緣通過Laplacian算子運算後會變成0。對於有些不是邊緣也變成0的問題,可以通過濾掉Sobel一階導數中較大值的點解決。
圖像形態學
OpenCV提供了一種高效且易用的圖像形態學變換接口。其中有很多形態學方法,但基本上所有的形態學操作都基於兩種原始操作——膨脹與腐蝕。
膨脹和腐蝕
膨脹和腐蝕是最基本的形態學變換,可用於消除噪聲、元素分割和連接等。基於這兩種操作,可以實現更復雜的形態學操作,用來定位強度峯值或孔洞、另一種形式的圖像梯度等。
膨脹是一種卷積操作,它將目標像素的值替換爲卷積核覆蓋區域的局部最大值,擴張了明亮區域,填充凹面。此卷積核是一個非線性核,是一個四邊形或圓形的實心核,其錨點在中心。與膨脹對應,腐蝕是與之相反的操作,腐蝕操作計算的是核覆蓋範圍內的局部最小值縮減了明亮區,消除凸起。
原圖 | 膨脹 | 腐蝕 |
void cv::erode(InputArray src, OutputArray dst, InputArray element, cv::Point anchor=cv::Point(-1, -1), int iterations=1 int borderType=cv::BORDER_CONSTANT, const cv::Scalar& borderValue = cv::morphologyDefaultBorderValue())
void cv::dilate(InputArray src, OutputArray dst, InputArray element, cv::Point anchor=cv::Point(-1, -1), int iterations=1 int borderType=cv::BORDER_CONSTANT, const cv::Scalar& borderValue = cv::morphologyDefaultBorderValue())
以上兩個函數分別是膨脹和腐蝕對應的函數,第三個參數是核,可以傳遞一個未初始化的cv::Mat,會使用默認的錨點在中心的3×3的核。
腐蝕操作通常用於消除圖中斑點一樣的噪聲,原理是斑點經過腐蝕後會消失,而大的可見區域不會受影響。膨脹操作通常用於發生連通分支。
通用形態學函數
當處理的對象是二值圖像時,像素只能是開(>0)或關(=0)的圖像掩膜時,基本的腐蝕和膨脹操作就夠用了。需要對灰度圖或者彩色圖進行處理時,一些其他操作就非常有用了,這些操作可以通過cv::morphologyEx()實現。
void cv::morphologyEx(InputArray src, OutputArray dst, int op, InputArray element, cv::Point anchor=cv::Point(-1, -1), int iterations=1, int borderType=cv::BORDER_CONSTANT, const cv::Scalar& borderValue = cv::morphologyDefaultBorderValue())
操作值 | 形態學操作 | 是否需要臨時圖像 |
---|---|---|
cv::MORPH_OPEN | 開操作 | 否 |
cv::MORPH_CLOSE | 閉操作 | 否 |
cv::MORPH_GRADIENT | 形態學梯度 | 總是需要 |
cv::MORPH_TOPHAT | 頂帽操作 | 就地調用需要(src = dst) |
cv::MORPH_BLACKHAT | 地貌操作 | 就地調用需要(src = dst) |
開操作和閉操作
開操作先將圖像進行腐蝕,然後對腐蝕的結果進行膨脹。開操作常用語對二值圖像中的區域進行計算。
閉操作想將圖像進行膨脹,然後對膨脹的結果進行腐蝕。閉操作用於複雜連通分支算法中減少無用或噪聲驅動的片段。
對於連通分支,通常先進行腐蝕或閉操作消除噪聲,然後通過開操作連接相互靠近的大型區域。
對於一幅非布爾型圖像進行形態學操作時,閉操作最明顯的效果是消除值小於鄰域內的點的孤立異常,而開操作消除的是大於鄰域內點的孤立異常值。
形態學梯度
梯度操作的結果(擴張亮域)減腐蝕操作的結果(縮減亮域)產生了原圖像中的目標邊緣。對於灰度圖像,其結果就是計算明暗變換的趨勢。形態學梯度通常用於顯示明亮區域的邊界,然後便可以將他們看作目標或者目標的部分。用擴張的圖像減去了收縮的圖像便得到完整的邊界。與計算梯度不同,它並不會關注某個物體的周圍。
頂帽和黑帽
頂帽用於顯示與其鄰近相比更亮的部分;黑帽用於顯示與其鄰近相比更暗的部分。
#include "stdafx.h"
#include <opencv2/opencv.hpp>
int main()
{
cv::namedWindow("image", cv::WINDOW_NORMAL);
cv::namedWindow("erosion", cv::WINDOW_NORMAL);
cv::namedWindow("dilation", cv::WINDOW_NORMAL);
cv::namedWindow("opening", cv::WINDOW_NORMAL);
cv::namedWindow("closing", cv::WINDOW_NORMAL);
cv::namedWindow("gradient", cv::WINDOW_NORMAL);
cv::namedWindow("topHat", cv::WINDOW_NORMAL);
cv::namedWindow("blackHat", cv::WINDOW_NORMAL);
cv::Mat img = cv::imread("D:\\personal-data\\wallpapers\\test.png");
cv::Mat erosion, dilation, opening, closing, gradient, topHat, blackHat;
cv::erode(img, erosion, cv::Mat());
cv::dilate(img, dilation, cv::Mat());
cv::morphologyEx(img, opening, cv::MORPH_OPEN, cv::Mat());
cv::morphologyEx(img, closing, cv::MORPH_CLOSE, cv::Mat());
cv::morphologyEx(img, gradient, cv::MORPH_GRADIENT, cv::Mat());
cv::morphologyEx(img, topHat, cv::MORPH_TOPHAT, cv::Mat());
cv::morphologyEx(img, blackHat, cv::MORPH_BLACKHAT, cv::Mat());
cv::imshow("image", img);
cv::imshow("erosion", erosion);
cv::imshow("dilation", dilation);
cv::imshow("opening", opening);
cv::imshow("closing", closing);
cv::imshow("gradient", gradient);
cv::imshow("topHat", topHat);
cv::imshow("blackHat", blackHat);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
自定義核
在形態學上,核常常稱爲“構造元素”,OpenCV提供了創建自定義形態學核的函數cv::getStructuringElement()。
cv::Mat cv::getStructuringElement(int shape, cv::Size ksize, cv::Point anchor=cv::Point(-1, -1))
形狀值 | 元素 | 描述 |
---|---|---|
cv::MOEPH_RECT | 矩形 | |
cv::MOEPH_ELLIPSE | 橢圓形 | 以ksize.width和ksize.height爲兩個半徑做橢圓 |
cv::MOEPH_CROSS | 交叉 |
,當 i == anchor.y或 j == anchor.x |
用任意線性濾波器做卷積
上述兩個核中,左邊的核是可分的,右邊的是不可分的。一個可分核可以理解成兩個一維核,在卷積時先調用x內核,然後再調用y內核。兩個矩陣進行卷積所產生的消耗可以用兩個矩陣的面積之積近似。如此一來,用n×n的核對面積爲A的圖像進行卷積所需要的時間是,但如果分解爲n×1和1×n的兩個核,那麼代價就是。由此可見,分解卷積核可以提高卷積計算的效率。
用cv::filter2D()進行卷積
void cv::filter2D(InputArray src, OutputArray dst, int ddepth, InputArray kernel, cv::Point anchor=cv::Point(-1, -1), double delta=0, int borderType=cv::BORDER_DEFAULT)
注:如果定義了錨點的位置,那麼核的大小可以是偶數,否則必須是奇數。
通過cv::sepFilter2D使用可分核
void cv::sepFilter2D(InputArray src, OutputArray dst, int ddepth, InputArray rowKernel, InputArray columnKernel, cv::Point anchor=cv::Point(-1, -1), double delta=0, int borderType=cv::BORDER_DEFAULT)
注:兩個核的大小應當是n1×1和1×n2,n1和n2不一定相等。
生成卷積核
void cv::getDerivKernel(OutputArray kx, OutputArray ky, int dx, int dy, int ksize, bool normalize=true, int ktype=CV_32F)
作用:生成可分解核,如Sobel和Scharr核。dx和dy是求導順序;ksize是核的大小,可以爲1、3、5、7或cv::SCHARR;normalize指示是否核元素規範化,如果是浮點型圖像,設爲true,反之設爲false;ktype表示濾波器的類型,可以使CV_32F和CV_64F。
cv::Mat cv::getGaussianKernel(int ksize, double sigma, int ktype=CV_32F)
作用:生成高斯核。
α在濾波器需要規範化的時候才起作用。sigma可以爲-1,這樣將自動計算,其中