目錄
一、前言
圖像操作其實就是對像素進行操作,這些操作不僅僅是像前面那些基礎操作一樣簡單,只有獲取值啊,簡單賦值啊之類的。但是像素操作可不止有這麼簡單。
從今天這節內容開始,我們來一起聊一些更高級的像素操作,我們會講一些原理,並講解對應的API,通過實戰讓大家能夠對內容有更深刻的認識。
二、溫故知新——像素基本操作
前面我們講了幾個像素基本操作:
獲取像素指針:用於後續讀取某像素的信息及修改像素。通過像素指針來訪問像素。
控制像素範圍:將求得的像素值規範到0-255之間。
讀寫像素,利用像素指針獲取像素值及修改像素值。
1、獲取像素指針
獲取像素指針是可以獲得一個指向像素的指針,我們可以使用指針來訪問像素值,修改像素值。包括獲取灰度圖像像素指針和彩色圖像像素指針。
獲取方式如下:
//灰度圖像
src_gray.at<uchar>(y, x); //行在前,列在後,y表示行,x表示列
src_gray.at<uchar>(Point(x, y));
//彩色圖像
Vec3b BGR = src.at<Vec3b>(row, col);
int B = BGR.val[0];
int G = BGR.val[1];
int R = BGR.val[2];
Scalar BGR1 = src.at<Vec3b>(Point(col, row));
2、像素範圍處理
像素範圍處理功能會將所有的輸入值都控制在0-255之間,小於0的返回0,大於255的返回255,其他不變。具體API如下:
saturate_cast<uchar>(number);
3、讀寫像素
讀寫像素是最基本的像素操作,一個是用於獲取像素值,一個是用於修改某個位置的像素值,上面獲取像素指針的代碼同時也讀取了像素,寫像素代碼如下:
//灰度圖像
image.at<uchar>(y, x) = 123;
//彩色圖像
image.at<Vec3b>(y,x)[0]=0; // blue
image.at<Vec3b>(y,x)[1]=0; // green
image.at<Vec3b>(y,x)[2]=255; // red
三、圖像掩膜操作
1、怎麼理解掩膜Mask
首先我們先來看一下定義:
掩膜操作是指根據掩膜矩陣(也稱作核kernel)重新計算圖像中每個像素的值。
我們舉個例子來說明一下,然後我們再來解釋:
比如上面的這個圖中,我們左邊是圖像,右邊是我們的掩膜矩陣,定義說,我們要根據掩膜矩陣重新計算圖像中每個像素的值。計算方式如下:
從左邊圖中找到黃色背景區域(從原圖中找到和掩膜矩陣大小相同的區域),對應位置相乘,然後求和:
5 * 0 + 5 * (-1) + 5 * 0 + 5 * (-1) + 5 * 5 + 5 * (-1) + 5 * 0 + 5 * (-1) + 5 * 0 = 5
通過計算,我們就會得到右面的圖像,左邊的3×3的像素,經過計算,會得到右邊1×1的像素。然後,我們計算
然後,我們計算下一個像素點,也就是左邊的區域往後移動一個區域,一直到最後一個區域結束:
我們會發現,計算過後,圖像變小了(最外面有一圈沒有計算)。這個問題,我們先留在這裏,以後再說。
由於圖像所有都是5,五個中心減去四個相鄰,最後結果還是5,所以上面這個經過掩膜操作並沒有什麼太明顯的變化。
如果換個矩陣就不一樣啦:
通過上面的介紹,我想,我們可以對掩膜有個更加深刻的瞭解了,掩膜可以通過鄰近像素來修改自身像素值。
2、掩膜實現
瞭解了具體的原理,我們接下來通過代碼來自己實現一下掩膜操作吧!
核心代碼就是使用掩膜中心位置的值的五倍,減去上下左右四個位置的值:
for (int i = 1; i < src_gray.rows-1; i++)
{
for (int j = 1; j < src_gray.cols-1; j++)
{
src_new.at<uchar>(i, j) =
src_gray.at<uchar>(i, j) * 5 - //中心
src_gray.at<uchar>(i - 1, j) - //上
src_gray.at<uchar>(i + 1, j) - //下
src_gray.at<uchar>(i, j - 1) - //左
src_gray.at<uchar>(i, j + 1); //右
}
}
全部代碼如下:
#include<iostream>
#include<opencv2\opencv.hpp>
using namespace std;
using namespace cv;
int main() {
Mat src = imread("./image/sign.png");
if (!src.data)
{
cout << "ERROR : could not load image.\n";
return -1;
}
Mat src_gray;
cvtColor(src, src_gray, COLOR_BGR2GRAY);
imshow("src_gray", src_gray);
Mat src_new = Mat(Size(src_gray.cols,src_gray.rows),CV_8UC1);
for (int i = 1; i < src_gray.rows-1; i++)
{
for (int j = 1; j < src_gray.cols-1; j++)
{
src_new.at<uchar>(i, j) =
src_gray.at<uchar>(i, j) * 5 - //中心
src_gray.at<uchar>(i - 1, j) - //上
src_gray.at<uchar>(i + 1, j) - //下
src_gray.at<uchar>(i, j - 1) - //左
src_gray.at<uchar>(i, j + 1); //右
}
}
imshow("new src_gray", src_new);
waitKey(0);
return 0;
}
執行結果如下:
大家就能發現,圖像中文字邊界的位置的發生了明顯變化。
3、API-filter2D
我們自己實現了我們的掩膜操作,在opencv中,我們提供了專門的API來實現掩膜操作:
void filter2D(
InputArray src,
OutputArray dst,
int ddepth,
InputArray kernel,
Point anchor = Point(-1,-1),
double delta = 0,
int borderType = BORDER_DEFAULT
);
函數參數含義如下:
(1)InputArray類型的src ,輸入圖像。
(2)OutputArray類型的dst ,輸出圖像,圖像的大小、通道數和輸入圖像相同。
(3)int類型的ddepth,目標圖像的所需深度。
(4)InputArray類型的kernel,卷積核(或者更確切地說是相關核)是一種單通道浮點矩陣;如果要將不同的核應用於不同的通道,請使用split將圖像分割成不同的顏色平面,並分別對其進行處理。。
(5)Point類型的anchor,表示錨點(即被平滑的那個點),注意他有默認值Point(-1,-1)。如果這個點座標是負值的話,就表示取核的中心爲錨點,所以默認值Point(-1,-1)表示這個錨點在覈的中心。。
(6)double類型的delta,在將篩選的像素存儲到dst中之前添加到這些像素的可選值。
(7)int類型的borderType,用於推斷圖像外部像素的某種邊界模式。有默認值BORDER_DEFAULT。
在一般使用中,我們只涉及到前面四個參數,
對於第五個參數,我們都是默認中心爲錨點,
對於第六個參數,一般來說我們很少會設置一個額外的值去調整像素值,所以也是默認爲0的。
對於第七個參數,因爲是剛開始,我們先不展開說明,因爲後續我們還會講到,在這裏,我們先採用默認,讓我們把重心放在前四個參數上面。
如下面這個例子:
filter2D( src, dst, src.depth(), kernel );
我們來使用一個完整的例子來說明一下:
執行結果如下:
我們能夠發現,我們的文字出現了白色的邊界。
如果我們再調整一下kernel,我們就可以得到很多類型的圖像:
大家也可以自己嘗試設置自己的kernel,一般來說有個原則就是儘量核所有數值加起來不要太大。一般都是讓求得的值爲1。
四、執行時間
我們留意到一個比較重要的內容,就是我們操作像素,是要遍歷所有像素的,這是很耗時間的操作。特別是瞭解深度學習的同學,當做深度學習圖像檢測要遍歷圖像做訓練時,運算量是極大的,所以我們需要獲取執行時間,來分析算法的優劣,進行算法效率比較。
在opencv中,我們提供了專門的API來獲取執行時間,全部功能代碼如下:
double t = (double)getTickCount();
// do something ...
t = ((double)getTickCount() - t) / getTickFrequency();
cout << "消耗的時間爲: " << t << endl;
這其中涉及到兩個API:
getTickCount();
getTickFrequency();
第一個是:函數返回特定事件(例如,當機器打開時)後的刻度數。它可用於初始化RNG或通過讀取函數調用前後的計時計數來測量函數執行時間。
第二個是:函數返回每秒的刻度數。
我們通過第一個執行得到運行前和運行後的刻度數,相減後得到運行過程中的刻度時長,然後除以每秒的刻度數,就能得到代碼以秒爲單位的運行時長了。
舉個例子:
double t = (double)getTickCount();
// do something ...
Mat src_new;
Mat kernel = (Mat_<char>(3, 3) << 1, -4, 1, -1, 7, -1, 1, -4, 1);
filter2D(src, src_new, src.depth(), kernel);
imshow("src_new: 1, -4, 1, -1, 7, -1, 1, -4, 1", src_new);
t = ((double)getTickCount() - t) / getTickFrequency();
cout << "消耗的時間爲: " << t << endl;
執行結果如下:
到這裏我們就說完啦,大家多做練習哦!