影像讀取儲存(imread、imshow、imwrite)

這邊示範一個簡短的OpenCV的程式,用imread()讀取圖片,並將資料寫入Mat,imwrite()將Mat儲存在硬碟中,imshow()將Mat展示在螢幕上。

標頭檔

當我們使用OpenCV函式時,要先include此函式的模組,例如要用到Core模組時,我們須加入標頭檔:

#include <opencv2/core/core.hpp>

所有用到的模組都需加入,這邊為了方便,我們直接include所有模組,所以在cpp檔表頭,加入標頭檔:

#include <opencv2/opencv.hpp>

命名空間

為了使用上的安全,OpenCV有加上命名空間cv,所以我們使用此OpenCV的函式或類別時,前面需要加上cv::,為了使用上的方便,所以在cpp檔我們加上:

using namespace cv;

影像讀取

Mat imread(const string& filename, int flags=1)

  • filename:要載入的檔案名稱。
  • flags:影像標誌,flag分成三種模式,分別為彩色、灰階、原影像格式。
    flag值備註
    CV_LOAD_IMAGE_UNCHANGED<0讀入格式不變
    CV_LOAD_IMAGE_GRAYSCALE0以灰階讀入
    CV_LOAD_IMAGE_COLOR>0以BGR格式讀入

OpenCV支援的影像格式有BMP、PBM、DIB、JPEG、JPE、PNG、TIF等,常見的格式幾乎都有支援,以下的程式碼讀入"lena.jpg",並賦值給img:

Mat img = imread("lena.jpg",CV_LOAD_IMAGE_GRAYSCALE);

創建視窗

void namedWindow(const string& winname, int flags=WINDOW_AUTOSIZE)

  • winname:視窗標題。
  • flags:視窗標誌,flag常用的有兩種,分別為WINDOW_NORMAL和WINDOW_AUTOSIZE,當使用WINDOW_NORMAL時,可變更視窗的大小,當使用WINDOW_AUTOSIZE時,會產生剛好包含Mat大小的視窗,無法改變視窗大小。

秀出影像

void imshow(const string& winname, InputArray mat)

  • winname:要秀出此影像的視窗。
  • mat:要秀出的影像,當Mat為8位元時直接秀出,16位元時數值除以256後秀出,為浮點數時乘上255,使得三種格式,顯現範圍都為0~255。

我們先創建視窗,接著將Mat秀在此視窗內,當我們使用imshow這個函式時,我們需要加上waitKey(0),阻斷程式進行,這樣才可以看到視窗,以及視窗內的圖像,直到按下任一鍵後程式才繼續進行。如果流程完全不阻斷,main函式隨之進行完畢,此時視窗和影像都消失,我們會看不到想要的結果,以下為秀出影像的程式碼:

namedWindow("Display window", WINDOW_AUTOSIZE); 
imshow("Display window", img);  
waitKey(0);

等待按鍵輸入

int waitKey(int delay=0)
  • delay:等待時間,這邊主要分兩大類使用方式,當delay<=0時,程式靜止,當delay>0時,函式會等待參數時間(ms)後,返回按鍵的ASCII碼,如果這段時間沒有按鍵按下,會返回-1。
  • waitKey()只有搭配OpenCV視窗才有效果,沒有視窗的話無此暫停程式功能。
  • 可以使用這個特性搭配while迴圈,讓圖片不斷顯示,直到按下某特定按鍵後才停止。

由於esc鍵的ASCII碼為27,所以以下程式只有按下esc鍵,程式才會跳出迴圈:

while(true){ 
    imshow("Display window", img);  
    if(cvWaitKey(10)==27){  
        break; 
    } 
}

圖片儲存

bool imwrite(const string& filename, InputArray img, const vector& params=vector())

  • filename:儲存的檔案名稱。
  • img:要儲存的影像。
  • params:影像參數,以(id,value)的順序輸入,因為有些格式有參數可輸入,像JPEG壓縮品質的值從0到100,預設為95,我們可將CV_IMWRITE_JPEG_QUALITY設定值,進而改變儲存的JPEG影像品質。
  • 如果是儲存3通道彩色圖的話,要注意此時的色彩空間需為BGR,否則後續讀取強度會錯誤,B和R的強度會對調。

OpenCV支援的影像格式有BMP、PBM、DIB、JPEG、JPE、PNG、TIF等,常見的格式幾乎都有支援,以下為圖片儲存的程式碼:

imwrite ("output1.jpg ", img);
vector<int> quality;
quality.push_back(CV_IMWRITE_JPEG_QUALITY);
quality.push_back(90);
imwrite("output2.jpg", img, quality);

程式範例1

以下程式碼從硬碟讀取檔案,將影像設定成灰階圖,把影像儲存在硬碟,並將影像秀在螢幕上,當使用者按下任一鍵,或是關閉視窗後程式終止:

#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;

int main(){
    Mat img = imread("lena.jpg",CV_LOAD_IMAGE_GRAYSCALE);
    imwrite("output.jpg",img);  

    namedWindow("Display window", WINDOW_AUTOSIZE);  
    imshow("Display window", img);        
    waitKey(0);                          

    return 0;
}

程式範例2

以下程式碼從硬碟讀取檔案,秀在螢幕上,當使用者按下esc鍵時程式終止:

#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;

int main(){
    Mat img = imread("lena.jpg",CV_LOAD_IMAGE_GRAYSCALE);
    while(true){
        imshow("Display window", img);     
        if(cvWaitKey(10)==27){                      
            break;
        }
    }
    return 0;
}

imshow

繼續閱讀 影像讀取儲存(imread、imshow、imwrite)

像素巡訪(at、ptr)

當我們進行影像處理時,可能有操作是要查訪所有像素,比如說我們想要改變一張影像的灰階值,讓所有的像素值加20,這時我們就需掃過影像所有的像素,這邊介紹OpenCV的at()和ptr()函式以及迭代器,來查訪Mat所有像素。

at()可用來讀取和修改某個像素值,通常用來對隨機位置的像素進行讀寫,就效率考量,並不適合用在循序查訪影像所有像素,以下用at()來讀取img的所有像素,並讓所有像素值加20:

int widthLimit = img.channels() * img.cols;
for(int height=0; height<img.rows; height++){
    for(int width=0; width<widthLimit; width++){
        img.at<uchar>(height, width) += 20;
    }
}

ptr()函式返回指標,指向影像指定列的首像素,使用時須輸入像素位元深度和第幾列,對於一個深度8位元的圖,我們可用img.ptr(j)指到第j列的第一個像素,接著逐列查訪,最後可查訪影像所有像素,這種方法運行速度較at()快,在解析度大或是重視效率的地方,是比較好的方法,以下用ptr()來讀取img的所有像素,並讓所有像素值加20:

int widthLimit = img.channels() * img.cols;
for(int height=0; height<img.rows; height++){
    uchar *data = img.ptr<uchar>(height);
    for(int width=0; width<img.widthLimit ; width++){
        data[width] += 20;
    }
}

OpenCV有為Mat提供了與STL迭代器兼容的迭代器,使用時須指定影像數據類型,以下用迭代器來讀取img的所有像素,並讓所有像素值加20:

if(img.channels()==1){
    Mat_<uchar>::iterator it = img.begin<uchar>();
    Mat_<uchar>::iterator itend = img.end<uchar>();
    for(;it!=itend;it++){
        (*it) = (*it) + 20;
    }
}
if(img.channels()==3){
    Mat_<Vec3b>::iterator it = img.begin<Vec3b>();
    Mat_<Vec3b>::iterator itend = img.end<Vec3b>();
    for(;it!=itend;it++){
        (*it)[0] = (*it)[0] + 20;
        (*it)[1] = (*it)[1] + 20;
        (*it)[2] = (*it)[2] + 20;
    }
}

繼續閱讀 像素巡訪(at、ptr)

影像卷積(Convolution)

濾波(filtering)是影像處理的一個基本操作,目的在選擇性的提取重要訊息,用在影像銳化、去除雜訊或提取感興趣的視覺特徵,這類處理需尋訪影像每個像素,從此像素和相鄰像素得到新的像素值。


核心(kernel):基本上核心是一個固定大小,其中心為錨點(anchor point)的二維矩陣,以下為一高斯濾波的核心,大小和矩陣數值依需求而變。

Kernel


卷積(convolution):是核心與圖的每個重疊像素間的運算,目的是要計算影像中某個位置的結果,主要有以下4個步驟:

  1. 將核心的錨點放在輸入圖的某個像素上。
  2. 各鄰近像素乘上相對應的核心係數後加總。
  3. 將計算結果放在輸出圖的錨點像素上。
  4. 對輸入圖的每個像素重複進行以上動作。

我們用以上的核心和卷積的概念,對一張灰階8位元影像進行銳化,我們將輸出圖和來源圖的第一行、第一列、最後一行、最後一列不做處理,所以尋訪像素時留了1像素的寬度,使用ptr()函式讀取指定列的第一個像素,主要有3個步驟:

  1. 先複製圖案,讓不處理的第一行、第一列、最後一行、最後一列,輸出圖和輸入圖像素值相同。
  2. 接著使用四個指標,前三個指標指向來源圖上中下列,第四個指向輸出圖目前處理的列數,之後逐列逐列進行濾波計算。
  3. 用saturate_cast將每次的計算結果限定在合理範圍,以本例來說就是從0到255,超過255會設定成255,小於0會設定成0。

OpenCV 限定合理範圍:template< … > _Tp saturate_cast(_Tp2 v)

  • v:輸入參數,會讓此值在合理範圍。
  • saturate_cast使用模板,所以呼叫時要指定像素深度。

影像卷積

#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;

void sharpen(const Mat &src, Mat &dst);

int main(){
    Mat src = imread("lena.jpg",CV_LOAD_IMAGE_UNCHANGED);
    Mat dst;
    sharpen(src, dst);

    namedWindow("window1");
    imshow("window1", src);
    imshow("window2", dst);
    waitKey(0); 

    return 0;
}

void sharpen(const Mat &src, Mat &dst){
    src.copyTo(dst);
    const int nChannels = src.channels();
    int heightLimit = src.rows - 1;
    int widthLimit = nChannels * (src.cols-1);
    for(int iH=1; iH<heightLimit; iH++){
        const uchar *prePtr = src.ptr<const uchar>(iH-1);
        const uchar *curPtr = src.ptr<const uchar>(iH);
        const uchar *nextPtr = src.ptr<const uchar>(iH+1);
        uchar *dstPtr = dst.ptr<uchar>(iH);
        for(int iW=nChannels; iW<widthLimit; iW++){
            dstPtr[iW] = saturate_cast<uchar>(5*curPtr[iW]-curPtr[iW-nChannels]-curPtr[iW+nChannels]-prePtr[iW]-nextPtr[iW]);
        }
    }
}

sharpen

sharpen


掃描影像

OpenCV提供filter2D()函式方便濾波計算,我們依濾波值定義一個核矩陣,之後呼叫filter2D函式時輸入此核矩陣當參數。

void filter2D(InputArray src, OutputArray dst, int ddepth, InputArray kernel, Point anchor=Point(-1,-1), double delta=0, intborderType=BORDER_DEFAULT)

  • src:輸入圖。
  • dst:輸出圖,和輸入圖的尺寸、通道數相同。
  • ddepth:輸出圖深度。
  • kernel:使用的核心。
  • anchor:錨點,預設為核心中央。

上面的卷積函式可改寫成以下函式,兩者結果相同:

#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;

void sharpen2(const Mat &src, Mat &dst);

int main(){
    Mat src = imread("lena.jpg",CV_LOAD_IMAGE_UNCHANGED);
    Mat dst;
    sharpen2(src, dst);

    namedWindow("window1");
    imshow("window1", src);
    imshow("window2", dst);
    waitKey(0); 

    return 0;
}

void sharpen2(const Mat &src, Mat &dst){
    Mat kernel(3,3,CV_32F,Scalar(0));
    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;
    filter2D(src,dst,src.depth(),kernel);
}

sharpen

sharpen

繼續閱讀 影像卷積(Convolution)

影像相加(add、addWeighted)

影像可以用不同的方式組合,就像是矩陣運算,各個相對像素做加減乘除,這邊介紹如何使用addWeighted()和add()函式,將輸入影像進行混和,addWeighted()和add()只能處理相同大小的輸入圖,這邊另外介紹如何合併大小不同的影像,類似把一個小Logo加到原本影像上。

OpenCV影像相加:void add(InputArray src1, InputArray src2, OutputArray dst, InputArray mask=noArray(), int dtype=-1)

  • src1 :輸入圖或強度值。
  • src2 :輸入圖或強度值。
  • dst:輸出圖,輸出圖和輸入圖有相同的尺寸和通道數。
  • mask:可有可無的遮罩,8位元單通道圖,指定那些像素要計算。
  • dtype:可有可無的輸出圖深度。

OpenCV影像相加:void addWeighted(InputArray src1, double alpha, InputArray src2, double beta, double gamma, OutputArray dst, int dtype=-1)

  • src1:輸入圖。
  • alpha:src1的權重。
  • src2:輸入圖,和src1的尺寸和通道數相同。
  • beta:src2的權重。
  • gamma:兩圖相加後再增加的值。
  • dst:輸出圖,輸出矩陣和輸入矩陣有相同的尺寸和通道數。
  • dtype:可有可無的輸出圖深度。

以下為函式概述,imgA、imgB、img皆為Mat,a[i]、b[i]、c[i]分別為此Mat的某個像素。

add(imgA, imgB, imgC);            //c[i] = a[i] + b[i]
add(imgA, imgB, imgC);            //c[i] = a[i] + b[i]
add(imgA, Scalar(20), imgC);      //c[i] = a[i] + 20
addWeighted(imgA, 0.8, imgB, 0.5, 10, imgC);   //c[i] = 0.8*a[i] + 0.5*b[i] + 10
scaleAdd(imgA, 1.2, imgB, imgC);  //c[i] = 1.2*a[i] + b[i]
add(imgA, imgB, imgC, mask);      //if(mask[i]) c[i] = a[i] + b[i]

這些運算內部都使用saturate_cast,來限制像素值在合理範圍內,以8位元影像來說,就是用saturate_cast限制範圍在0~255,而且參與運算的影像必須有相同的大小和深度,輸出圖像如果大小或格式不同,會重新分配空間。

有時我們只想處理部分區域,這時要使用遮罩(mask),這時我們可呼叫函式add(imgA, imgB, imgC, mask),這樣會在遮罩像素不為NULL的地方才相加,而遮罩必須是單通道的。

除了加法運算之外,OpenCV也提供像subtract、absdiff、multiply等多種矩陣運算,詳細用法可參考OpenCV文件。


以下範例為用兩個輸入圖,各佔0.5的比例混和產生新的圖:

#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;

int main(){
    Mat src1 = imread("input1.jpg",CV_LOAD_IMAGE_UNCHANGED);
    Mat src2 = imread("input2.jpg",CV_LOAD_IMAGE_UNCHANGED);
    Mat dst;
    addWeighted(src1,0.5,src2,0.5,0,dst);

    namedWindow("window1");
    namedWindow("window2");
    namedWindow("window3");
    imshow("window1", src1);
    imshow("window2", src2);
    imshow("window3", dst);
    waitKey(0); 

    return 0;
}

addWeighted

addWeighted

addWeighted


有時我們想合併大小不同的影像,類似把一個小Logo加到原本影像上,且能夠指定Logo的位置,由於add()函式要求輸入的影像格式、尺寸相同,所以不能直接使用add(),在使用之前要先定義感興趣區域(ROI),ROI大小和Logo圖相同,ROI位置決定Logo圖插入位置。我們使用addWeighted()調整背景和logo圖的強度比例,以下的imageROI和logo這兩個Mat,需要大小、尺寸相同,imageROI和img指向相同的數據結構,因此當我們改變imgROI時,原始圖img也隨之更改,達到插入Logo圖到原始圖的目的,以下為程式碼:

Mat img = imread("background.jpg");
Mat logo = imread("logo.jpg");
Mat imgROI = img(Rect(30,30,logo.cols,logo.rows));  //指定插入的大小和位置
addWeighted(imgROI,0.5,logo,0.5,0,imgROI);
  • 我們可以把imgROI想成是一張左上角在(30,30),寬logo.cols,高logo.rows的圖,重疊的像素值和img相同,而兩者指向相同的資料。

我們可用類似方法,來取得影像的某個部分來儲存或顯示,像下面的croppedImage為image這個影像,從(100,100)這個位置開始,寬度150、高度200的圖。

Mat croppedImage = image(Rect(100, 100, 150, 200));

以下示範如何將logo圖,混和添加在原始圖上產生一個新的圖:

#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;

int main(){
    Mat src = imread("background.jpg",CV_LOAD_IMAGE_UNCHANGED);
    Mat logo = imread("logo.jpg",CV_LOAD_IMAGE_UNCHANGED);
    Mat dst = imread("background.jpg",CV_LOAD_IMAGE_UNCHANGED);
    Mat imgROI = dst(Rect(30,30,logo.cols,logo.rows));  //指定插入的大小和位置
    addWeighted(imgROI,0.5,logo,0.5,0,imgROI);

    namedWindow("window1");
    namedWindow("window2");
    namedWindow("window3");
    imshow("window1", src);
    imshow("window2", logo);
    imshow("window3", dst);
    waitKey(0); 

    return 0;
}

addWeighted

addWeighted

addWeighted

繼續閱讀 影像相加(add、addWeighted)

像素強度變換(convertTo)

這邊介紹影像的強度變換,指的是對每個像素依序進行同樣的運算,假設r和s分別為輸入和輸出影像任一點的灰階值,可以定義為:s = T(r),其中T為強度變換,表示輸入和輸出圖間強度的某種映射關係,強度變換可分為線性變換和非線性變換。

最基本的線性變換就是一維線性變換:x’=a*x+b

  • 其中a為斜率,b為在y軸上的截距,x表示輸入影像的灰階值,x’為輸出影像的灰階值。
  • 當a>1時,輸出圖像的對比度增加,當a<1時,輸出圖像的對比度減小。
  • 當a=1且b!=0時,所有像素的灰階值增加或減少,使輸出圖像看起來更亮或更暗。
  • 當a=-1且b=255時,輸出影像的灰階值正好反轉,因為人的視覺特性的關係,這通常用來強調暗色影像中亮度較大的細節部分。

線性強度變換可以解決整體過亮或過暗的問題,但是對細節的改善有限,非線性變換才能對細節強度改善有較明顯的效果。


OpenCV 影像線性變換

void Mat::convertTo(OutputArray m, int rtype, double alpha=1, double beta=0)

  • m:輸出圖,如果和呼叫的Mat尺寸或型態不同,會再重新分配空間。
  • rtype:指定輸出圖型態,如果為負數的話,輸出圖型態會和呼叫的Mat相同。
  • alpha:選擇性的放大倍率,也就是線性變換:x’=a*x+b這個式子裡的a。
  • beta:選擇性的偏移量,也就是線性變換:x’=a*x+b這個式子裡的b。
  • 函式內有呼叫saturate_cast<>,避免發生overflow的現象。

以下示範兩種方法對影像進行線性變換,分別是自己寫的linearTrans()和OpenCV的convertTo(),兩種函式會帶來相同的結果:

#include <cstdio>
#include <opencv2/opencv.hpp>
using namespace cv;

void linearTrans(const Mat &src, Mat &dst);

int main(){
    Mat src = imread("lena.jpg",CV_LOAD_IMAGE_UNCHANGED);
    Mat dst1;
    Mat dst2;
    linearTrans(src, dst1);
    src.convertTo(dst2,-1,1.5,30);

    imshow("window1", src);
    imshow("window2", dst1);
    imshow("window3", dst2);
    waitKey(0); 

    return 0;
}

void linearTrans(const Mat &src, Mat &dst){
    dst.create(src.size(),src.type());
    int widthLimit = src.channels() * src.cols;
    for(int iH=0; iH<src.rows; iH++){
        const uchar *curPtr = src.ptr<const uchar>(iH);
        uchar *dstPtr = dst.ptr<uchar>(iH);
        for(int iW=0; iW<widthLimit; iW++){
            dstPtr[iW] = saturate_cast<uchar>(1.5*curPtr[iW]+30);
        }
    }
}

影像線性變換

影像線性變換

影像線性變換


以下為兩種較常見的非線性強度變換,分別為對數變換和伽瑪變換:

對數變換: x’ = c*log(1 + x)

  • x’為輸出像素值,x為原始像素值,c為比例常數。
  • 此變換可以增強圖像中較暗部分的細節,通常在傅立葉頻譜時,強度範圍非常大,直接顯示頻譜時,顯示設備的範圍往往不能滿足,進而丟失大量暗部細節,這時可以使用對數變換,將範圍進行非線性壓縮,以至於能夠清楚的顯示影像。

伽瑪變換:x’ = (x + esp)r

  • x’與x的範圍皆為0到1,esp為補償係數,r為伽瑪係數。
  • 與對數變換不同,伽瑪變化可以根據r的不同,增強低灰度或高灰度區域的對比度。
  • 當r>1時,高灰度區域對比度增強。
  • 當r<1時,低灰度區域對比度增強。
  • 當r=1時,此變換為線性變換。
  • 伽瑪變化並不僅可以改變影像的對比度,還能增強細節,帶來整體效果的改善。

繼續閱讀 像素強度變換(convertTo)