1. 程式人生 > >OpenCV從入門到放棄(五):畫素!

OpenCV從入門到放棄(五):畫素!

一.概念

1.影象本質上面是由數值組成的矩陣。矩陣中的一個元素對應一個畫素。
2.對於灰度影象(黑白影象),畫素是8位無符號數(CV_8U)。0表示黑色,255表示白色。對於彩色影象,是用三原色資料合成彩色。3個8位(CV_8UC3)的數值組成矩陣的一個元素。而且順序是BGR
3.一般來說8位的通道夠用了。但是有些特殊的需要16位。
4.經驗之談:矩陣可以有很多種型別,但是大部分操作可以使用任何型別的矩陣來完成。但是還是有一些操作必須使用特性的型別或者特定的通道數量。有時候留個心積累那些圖用什麼矩陣來處理。

下面廢話不多說,先來一個啟發性的例子(可以暫時不用知道其中全部的細節,稍後會講到這些東西)
程式碼:
這裡寫圖片描述


結果:
在原來的圖片上面生成了很多很多的點。
這裡寫圖片描述
上面這段程式碼的原理很簡單:就是隨機生成座標值,然後把這些座標處的灰度值改為255(白色).
大概有這些就已經算是一個訪問畫素的完整過程了.下面就詳細講一些訪問畫素的細節.

二.訪問畫素的三種方法

1.at方法(cv::Mat::at(…..)).

at方法顧名思義,就是在某個位置.其實用at可以直接訪問到某個位置的畫素.在OpenCV中,at方法為一個模板方法且有很多的變種,下面只講最基本常用的兩種方法.(分別是傳入座標傳入點的方法)
at方法是一個模板函式,在官方文件中抽取最簡單的寫法:

at <型別
>
(行,列) [通道(如果有通道的話)]

at<型別>(行,列)就能夠訪問到一幅圖片中的一個畫素了,每個畫素的chanel用[]來提取.是不是很簡單.
因為這是模板方法,選擇型別成了重要的一步,而且型別的選擇是與圖片元素的型別要對應起來,at方法不負責轉化型別.下面給出一個詳細的型別對應表.(要是現在不知道什麼是影象元素的型別.那麼點選下面的連結轉到之前的core元件,有詳細的型別介紹.)
http://blog.csdn.net/xierhacker/article/details/52457907
首先,OpenCV中有一個基本的向量模板類,一些基本的”N個元素”向量可以由這個模板類來定義,簡單地可以寫為cv::Vec

uchar型別(分別為2元素,3元素,4元素):
typedef Vec<uchar, 2> cv::Vec2b     typedef Vec<uchar, 3> cv::Vec3b typedef Vec<uchar, 4> cv::Vec4b

Short型別
typedef Vec<short, 2> cv::Vec2s     typedef Vec<ushort, 2> cv::Vec2w
typedef Vec<short, 3> cv::Vec3s     typedef Vec<ushort, 3> cv::Vec3w
typedef Vec<short, 4> cv::Vec4s     typedef Vec<ushort, 4> cv::Vec4w

Int型別(同上):
typedef Vec<int, 2> cv::Vec2i       typedef Vec<int, 3> cv::Vec3i
typedef Vec<int, 4> cv::Vec4i       typedef Vec<int, 6> cv::Vec6i
typedef Vec<int, 8> cv::Vec8i

float型別:
typedef Vec<float, 2> cv::Vec2f     typedef Vec<float, 3> cv::Vec3f typedef Vec<float, 4> cv::Vec4f     typedef Vec<float, 6> cv::Vec6f


double型別:
typedef Vec<double, 2> cv::Vec2d        typedef Vec<double, 3> cv::Vec3d
typedef Vec<double, 4> cv::Vec4d        typedef Vec<double, 6> cv::Vec6d

由此,可以得到一個常用的訪問畫素的時候模板中放型別的表:
畫素型別(模板傳入關鍵字):

CV_8U(uchar)  
CV_8UC1 (uchar)  CV_8UC2 (Vec2b)  CV_8UC3 (Vec3b)  CV_8UC4(Vec4b) 

CV_8S(char)   
CV_8SC1 (1通道)  CV_8SC2 (2通道)   CV_8SC3 (3通道)  CV_8SC4 (4通道)   

CV_16U  (ushort)
CV_16UC1 (ushort)  CV_16UC2 (Vec2w)   CV_16UC3 (Vec3w)   CV_16UC4 (Vec4w)   

CV_16S  (short)
CV_16SC1(short)   CV_16SC2(Vec2s)   CV_16SC3(Vec3s)   CV_16SC4(Vec4s)   

CV_32S (int)
CV_32SC1(int)   CV_32SC2(Vec2i)    CV_32SC3(Vec3i)  CV_32SC4(Vec4i)   

CV_32F (float)
CV_32FC1(float)   CV_32FC2(Vec2f)  CV_32FC3(Vec3f)   CV_32FC4(Vec4f)  

CV_64F(double)  
CV_64FC1(double)   CV_64FC2(Vec2d)  CV_64FC3(Vec3d)  CV_64FC4(Vec4d) 

現在再來看上面的那個加入白色噪點的例子,是不是豁然開朗,知道是怎麼用的了.
一個改變某點畫素來畫線的例子:
程式碼:

#include <iostream>
#include <cmath>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

int main()
{
    cv::Mat pic1(300, 300, CV_8UC3, cv::Scalar(255, 0, 0));
    cv::Mat pic2(10, 3, CV_32F, 20.3);

    //訪問畫素
    //pic1上面畫出一條直線
    for (int i = 0; i <300; i++)
    {
        int j = i; 
        //訪問畫素改變顏色CV_8UC3對應的就是Vec3b.
        pic1.at<cv::Vec3b>(i, j)[0] = 0;
        pic1.at<cv::Vec3b>(i, j)[2] = 255;

    }
    cv::imshow("test", pic1);
    cv::waitKey(0);
    return 0;
}

結果:
這裡寫圖片描述
程式碼的意義很容易懂,就是建立一個80*80的圖片,初始化為藍色,然後根據一個直線方程把某點的顏色改為紅色,那麼最終就得到了一條紅色的直線.

2.指標(cv::Mat::ptr(….))

_Tp* cv::Mat::ptr (int  i0 = 0)     
    返回mat的某行的一個地址,地址的型別與你之前在mat中選擇的型別有關(模板函式),因此,要非常注意選擇正確的返回以及模板引數的型別.
    i0代表0軸,或者通俗一點理解就是矩陣的一行.(索引是從0開始,要小心)

定義二:

_Tp* cv::Mat::ptr ( int  i0,int  i1 )
    返回mat某個位置元素的地址,還是老話,地址的型別與你之前在mat中選擇的型別有關(模板函式),因此,要非常注意選擇正確的返回以及模板引數的型別.
    i0代表0軸,或者通俗一點理解就是矩陣的一行.(索引是從0開始,要小心)
    I1代表1軸,或者通俗一點理解就是矩陣的一列,(索引是從0開始,要小心)

例一(只有一個通道):
程式碼:

#include <iostream>
#include <cmath>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

int main()
{
    cv::Mat pic2(10, 3, CV_32F, 20.3);
    //每行元素數量
    int numOfRow = pic2.cols;

    //訪問畫素
    for (int row = 0; row < pic2.rows; row++)
    {
        //獲得該行的地址
        float *data = pic2.ptr<float>(row);
        //訪問該行元素
        for (int col = 0; col < numOfRow; col++)
        {
            std::cout << data[col] << " ";
        }

        std::cout << std::endl;
    }
    cv::imshow("test", pic2);
    cv::waitKey(0);
    return 0;
}

結果:
這裡寫圖片描述
這裡的Mat中的資料型別選擇的是CV_32F,是float的單通道型別.選擇這種型別就是要展示接下來的指標的模板中應該選擇的引數.

例二(多個通道):
程式碼:

#include <iostream>
#include <cmath>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

int main()
{
    cv::Mat pic1(10, 3, CV_8UC3, cv::Scalar(255, 0, 0));
//  cv::Mat pic2(10, 3, CV_32F, 20.3);
    //每行元素數量
    int numOfRow = pic1.cols;

    //訪問畫素
    for (int row = 0; row < pic1.rows; row++)
    {
        //獲得該行的地址
        cv::Vec3b *data = pic1.ptr<cv::Vec3b>(row);
        //訪問該行元素
        for (int col = 0; col < numOfRow; col++)
        {
            data[col][0] = 0;
            data[col][2] = 255;
        }

        //std::cout << std::endl;
    }
    std::cout << cv::format(pic1, cv::Formatter::FMT_PYTHON) << std::endl;
    cv::imshow("test", pic1);
    cv::waitKey(0);
    return 0;
}

結果:
指標的方式改變畫素通道的值

例三(兩個索引):
程式碼:

#include <iostream>
#include <cmath>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

int main()
{
    cv::Mat pic1(10, 3, CV_8UC3, cv::Scalar(255, 0, 0));
//  cv::Mat pic2(10, 3, CV_32F, 20.3);
    //每行元素數量
    int numOfRow = pic1.cols;

    //返回13列的地址(索引從0開始)
    cv::Vec3b *pixel = pic1.ptr<cv::Vec3b>(0, 2);
    //0通道改為128
    pixel[0] = 128;
    //輸出
    std::cout << *pixel << std::endl;
    cv::imshow("test", pic1);
    cv::waitKey(0);
    return 0;
}

結果:
這裡寫圖片描述
也就是說,可以同時使用兩個索引來得到一個具體位置的地址.

三.感興趣區域(Region Of Interest,ROI)

有時候我們並不想在一整張圖片上面做文章..我們只想選擇某一個小區域上面完成一些操作.
OpenCV能夠讓我們僅僅選擇一些子區域,並且把這個子區域當做普通的影象來操作.這就引出了感興趣區域(ROI)這個話題.
使用ROI通常可以減少處理時間,增加精度,因此是一個必須要掌握的”技能”.
定義ROI區域方式:

方式一:使用矩形(Rect)類

Mat  ROI;
ROI=image(Rect_ (_Tp _x, _Tp _y, _Tp _width, _Tp _height));
    x,y這兩個引數就是矩形區域左上角的座標
    width,height這兩個引數就是矩形局域的寬和高

方式二:手動指定感興趣的行和列的範圍

Mat  ROI;
ROI=image(Rect_ (range(row_start,row_end),range(col_strat,col_end)));
    range(row_start,row_end):行的開始和結束
    range(col_strat,col_end):列的開始和結束

說了這麼多,就要講具體怎麼用了,下面影象運算的第一個例項影象疊加就使用了ROI的概念,可以看下例項是怎麼用的.

四.簡單影象運算

首先,標題是簡單影象運算,是因為接下來的例子都是很簡單很基礎的.但是也是很綜合的,綜合使用了之前接觸到的一些程式設計以及理論知識.
這部分有必要消化,因為這些例項中包含了一些很基本的概念,這些概念會在這些例子中很形象的展示出來.比死記硬背一些理論好多了.

Ⅰ影象疊加

影象疊加是一個很基本的例子,通過這個例子可以聯絡ROI的使用.
我們這裡有兩個圖片,一個是主圖片如下
這裡寫圖片描述
一個是很小的logo如下.
這裡寫圖片描述
任務就是要將logo疊加到主圖片上面去.不囉嗦了,上程式碼

程式碼:

#include <iostream>
#include <cmath>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

int main()
{
    //讀主圖片
    cv::Mat image = cv::imread("1.jpg");
    //讀logo
    cv::Mat logo = cv::imread("logo.png");
    //在主圖片上面定義"感興趣"區域
    cv::Mat ROI = image(cv::Rect(200, 200, logo.cols, logo.rows));
    //logo複製到感興趣區域
    logo.copyTo(ROI);

    cv::imshow("test", image);
    cv::waitKey(0);
    return 0;
}

效果:
這裡寫圖片描述
上面的程式碼很簡單,所以這裡不給出解釋,看註釋完完全全能夠懂.

還有內容,稍後更新……