計算機視覺 OpenCV Android | Mat畫素操作(影象畫素的讀寫、均值方差、算術、邏輯等運算、權重疊加、歸一化等操作)...

weixin_34116110發表於2019-01-29

本文目錄

1. 畫素讀寫
2. 影象通道與均值方差計算
3. 算術操作與調整影象的亮度和對比度
4. 基於權重的影象疊加
5. Mat的其他各種畫素操作





1. 畫素讀寫

  • Mat作為影象容器,其資料部分儲存了影象的畫素資料,我們可以通過相關的API來獲取影象資料部分
  • 在獲取影象資料的時候,知道Mat的型別通道數目關重要,
    根據Mat的型別通道數目,開闢適當大小的記憶體空間
    然後通過get方法就可以迴圈實現每個畫素點值的讀取、修改
    然後再通過put方法修改與Mat對應的資料部分

常見的Mat的畫素讀寫get與put方法支援如下表:

9125154-049956510284f586.png

  • 預設情況下,imread方式將Mat物件型別載入為CV_8UC3
    本系列筆記跟隨原著預設提到的載入影象檔案均為Mat物件、型別均為CV_8UC3、通道順序均為BGR
  • 上表中所列舉的是當前OpenCV支援的讀取影象的方法;
    使用時若需要將畫素值寫入到Mat物件中,使用與每個get方法相對應的put方法即可。
  • 根據開闢快取區域data陣列的大小
    讀寫畫素既可以每次從Mat中讀取一個畫素點資料,
    或者可以每次從Mat中讀取一行畫素資料,
    還可以一次從Mat中讀取全部畫素資料。

下面演示對Mat物件中的每個畫素點的值都進行取反操作,並且分別用這三種方法實現畫素操作

  • 首先要將影象載入為Mat物件
    然後獲取影象的寬、高以及通道數channels(特別注意這三個值,接下來一直用到,尤其channels)
Mat src = Imgcodecs.imread(fileUri.getPath());
if(src.empty()){
  return;
}
int channels = src.channels();
int width = src.cols();
int height = src.rows();

接下來便可以通過方才所述三種方式讀取畫素資料、修改、寫入比較它們的執行時間

1.1.從Mat中每次讀取一個畫素點資料

對於CV_8UC3Mat型別來說,對應的資料型別byte
則先初始化byte陣列data,用來存取每次讀取出來的一個畫素點的所有通道值
陣列的長度取決於影象通道數目

完整程式碼如下:

byte[] data = new byte[channels];
int b=0, g=0, r=0;
for(int row=0; row<height; row++) {
  for(int col=0; col<width; col++) {
      // 讀取
      src.get(row, col, data);//!!!!!!!!!!!!!!!!!!!!!!!讀取一個px
      b = data[0]&0xff;
      g = data[1]&0xff;
      r = data[2]&0xff;
      // 修改
      b = 255 - b;
      g = 255 - g;
      r = 255 - r;
      // 寫入
      data[0] = (byte)b;
      data[1] = (byte)g;
      data[2] = (byte)r;
      src.put(row, col, data);
  }
}

補充詮釋

  • 一個px有多個通道;
  • 一個通道配給它一個陣列元素;
  • 1.2中逐行讀取時的一個列(某行中的某個列其實就是一個陣列元素而已)不是px,
    而只是某個px的一個channel而已;
  • 1.3 同理
  • 即1.2 以及1.3 中,data的一個元素,不是px,而只是某個px的一個channel而已;
1.2 從Mat中每次讀取一行畫素資料

首先需要定義每一行畫素資料陣列的長度,這裡為影象寬度乘以每個畫素的通道數目
接著迴圈修改每一行的資料
這裡get方法第二個引數 col = 0的意思是從每一行的第一列開始獲取畫素資料

完整程式碼如下:

       // each row data
        byte[] data = new byte[channels*width];//channels 是一個px的通道數;width是一個行的px的個數;
        // loop
        int b=0, g=0, r=0;
        int pv = 0;
        for(int row=0; row<height; row++) {
            src.get(row, 0, data);
            /*get一整行的px資料,存進data;形象地說,是以 位置是(row, 0)的第一個px的第一個channel為起始元素,獲取一個data長度的資料;
            資料一個元素(channel)一個元素(channel)地存進陣列data, 每個元素是某個px的一個channel;*/
            for(int col=0; col<data.length; col++) {//行中迴圈列,處理內容:修改一整行的資料
                // 讀取
                pv = data[col]&0xff;
                // 修改
                pv = 255 - pv;
                data[col] = (byte)pv;
            }
            // 至此,data蓄滿一行修改好的px(channel)資料
            // 寫入
            src.put(row, 0, data);
        }

關於程式碼的補充詮釋

  • byte[] data = new byte[channels*width];中:
    channels 是一個px的通道數;
    width是一個行的px的個數;
  • for(int row=0; row<height; row++):外層 for 迴圈行;
  • src.get(row, 0, data);get一整行的px資料,存進data;
    形象地說,
    是以 位置是(row, 0)第一個px第一個channel起始元素
    獲取一個data長度的資料;
    資料一個元素(channel)一個元素(channel)地存進陣列data
    每個元素是某個px的一個channel
  • for(int col=0; col<data.length; col++)次層 for ,
    行中迴圈列,處理內容:修改一整行的資料;
  • 次層for執行完畢,data蓄滿一行修改好的px(channel)資料;
  • src.put(row, 0, data):陣列物件引用賦給行首,交付整行資料;
    形象地說,
    是以 位置是(row, 0)第一個px第一個channel起始元素
    提交一個data長度的資料,即一整行;
1.3 從Mat中一次讀取全部畫素資料
  • 首先定義陣列長度,這裡為影象寬度×影象高度×通道數目
    然後一次性獲取全部畫素資料,
    get的前面兩個引數row=0、col=0,表示從第一個畫素的第一個channel開始讀取。

完整程式碼如下:

// all pixels
int pv = 0;
byte[] data = new byte[channels*width*height];
src.get(0, 0, data);
for(int i=0; i<data.length; i++) {
  pv = data[i]&0xff;
  pv = 255-pv;
  data[i] = (byte)pv;
}
src.put(0, 0, data);

關於程式碼的補充詮釋(參考1.2的補充,不難理解)

  • src.get(0, 0, data);get全部的px資料,存進data;
    形象地說,
    是以 位置是(0, 0)第一個px第一個channel起始元素
    獲取一個data長度的資料;
    資料一個元素(channel)一個元素(channel)地存進陣列data
    每個元素是某個px的一個channel
  • src.put(0, 0, data):陣列物件引用賦給行首,交付全部資料;
    形象地說,
    是以 位置是(0, 0)第一個px第一個channel起始元素
    提交一個data長度的資料,即全部px的全部channel

上述三種方法

  • 第一種方法因為頻繁訪問JNI呼叫(*!!!* |get())而效率低下,但是記憶體(*!!!* |區域性變數data的長度)需求最小;
  • 第二種方法每次讀取一行,相比第一種方法速度有所提高,但是記憶體使用增加
  • 第三種方法一次讀取Mat中的全部畫素資料,在記憶體中迴圈修改速度最快,通過JNI呼叫OpenCV底層C++方法次數最少,因而效率也是最高的,但是對於高解析度影象,這種方式顯然記憶體消耗過多,容易導致OOM問題

所以Android開發者在使用OpenCV的時候,
需要注意應根據專案需求
選擇第二種或者第三種方法實現畫素讀寫
第一種方法只適用於隨機少量畫素讀寫的場合。
三種方法在例項專案中除錯時:

    public void readAndWritePixels() {
        Mat src = Imgcodecs.imread(fileUri.getPath());
        if(src.empty()){
            return;
        }
        int channels = src.channels();
        int width = src.cols();
        int height = src.rows();

////        // each row data
//        byte[] data = new byte[channels*width];//channels 是一個px的通道數;width是一個行的px的個數;
//        // loop
//        int b=0, g=0, r=0;
//        int pv = 0;
//        for(int row=0; row<height; row++) {
//            src.get(row, 0, data);
//            /*get一整行的px資料,存進data;形象地說,是以 位置是(row, 0)的第一個px的第一個channel為起始元素,獲取一個data長度的資料;
//            資料一個元素(channel)一個元素(channel)地存進陣列data, 每個元素是某個px的一個channel;*/
//            for(int col=0; col<data.length; col++) {//行中迴圈列,處理內容:修改一整行的資料
//                // 讀取
//                pv = data[col]&0xff;
//                // 修改
//                pv = 255 - pv;
//                data[col] = (byte)pv;
//            }
//            // 至此,data蓄滿一行修改好的px(channel)資料
//            // 寫入
//            src.put(row, 0, data);
//        }

        // all pixels
        int pv = 0;
        byte[] data = new byte[channels*width*height];
        src.get(0, 0, data);
        for(int i=0; i<data.length; i++) {
            pv = data[i]&0xff;
            pv = 255-pv;
            data[i] = (byte)pv;
        }
        src.put(0, 0, data);

        Bitmap bm = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888);
        Mat dst = new Mat();
        Imgproc.cvtColor(src, dst, Imgproc.COLOR_BGR2RGBA);
        Utils.matToBitmap(dst, bm);
        ImageView iv = (ImageView)this.findViewById(R.id.chapter3_imageView);
        iv.setImageBitmap(bm);
    }





2. 影象通道與均值方差計算

  • 影象中通道數目的多少可以通過Mat物件channels()進行查詢獲取
  • 對於多通道的影象,Mat提供的API方法可以把它分為多個單通道的影象;
    同樣對於多個單通道的影象,也可以組合成一個多通道的影象。
  • OpenCV還提供了計算影象每個通道畫素平均值標準方差的API方法,
    通過它們可以計算得到影象的畫素平均值與方差
    根據平均值可以實現基於平均值的二值影象分割
    根據標準方差可以找到空白影象或者無效影象
2.1 影象通道分離與合併
  • 影象通道數通過Mat的channels()獲取之後,
    如果通道數目大於1,
    那麼根據需要呼叫split方法就可以實現通道分離
    通過merge方法就可以實現通道合併

這兩個方法的詳細解釋具體如下:

  • split(Mat m, List<Mat> mv) // 通道分離
    m:表示輸入多通道影象。
    mv:表示分離之後個單通道影象,mv的長度與m的通道數目一致。

  • merge(List<Mat> mv, Mat dst) // 通道合併
    mv:表示多個待合併單通道影象。
    dst:表示合併之後生成的多通道影象。

上面兩個方法都來自Core模組Core模組主要包含一些Mat操作基礎矩陣數學功能

一個簡單的多通道的Mat物件其分離與合併的程式碼演示如下:

public void channelsAndPixels() {
//        Mat src = Imgcodecs.imread(fileUri.getPath());
//        if(src.empty()){
//            return;
//        }

        //*******
        Bitmap bitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.lena);
        Mat ori = new Mat();
        Mat src = new Mat();
        Utils.bitmapToMat(bitmap, ori);
        Imgproc.cvtColor(ori, src, Imgproc.COLOR_RGBA2BGR);
        //*******

        List<Mat> mv = new ArrayList<>();
        Core.split(src, mv);
        for(Mat m : mv) {
            int pv = 0;
            int channels = m.channels();//channels = 1,畢竟都呼叫了split()了
//            //下面這行用來測試channels的值
//            Toast.makeText(this,"The m.channels is" + channels,Toast.LENGTH_SHORT).show();

            int width = m.cols();
            int height = m.rows();
            byte[] data = new byte[channels*width*height];
            m.get(0, 0, data);
            for(int i=0; i<data.length; i++) {
                pv = data[i]&0xff;
                pv = 255-pv;
                data[i] = (byte)pv;
            }
            m.put(0, 0, data);
        }
        Core.merge(mv, src);

        Bitmap bm = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888);
        Mat dst = new Mat();
        Imgproc.cvtColor(src, dst, Imgproc.COLOR_BGR2RGBA);
        Utils.matToBitmap(dst, bm);

        ImageView iv = (ImageView)this.findViewById(R.id.chapter3_imageView);
        iv.setImageBitmap(bm);

        dst.release();
        src.release();
    }

上面的程式碼實現了對多通道影象分離之後取反
然後再合併
最後通過Android ImageView元件顯示結果
如此便是影象通道分離與合併基本用法

2.2 .均值與標準方差計算與應用

接下來的內容是關於影象Mat畫素資料的簡單統計,計算均值與方差

  • 對給定的一組資料計算其均值μ標準方差stddev的公式如下:

    9125154-8a1aca1942048968.png
    其中,n表示陣列的長度xi表示陣列第i個元素的值
    9125154-6d1a8958b9d308cd.png
    其中,n表示陣列長度μ表示均值1表示自由度

  • 根據上述公式,
    可以讀取每個畫素點的值
    計算每個通道畫素的均值與標準方差

OpenCV Core模組中已經實現了這類API,具體解釋如下:

  • meanStdDev(Mat src, MatOfDouble mean, MatOfDouble stddev)
    src:表示輸入Mat影象。
    mean:表示計算出各個通道的均值,陣列長度與通道數目一致。
    stddev:表示計算出各個通道的標準方差,陣列長度與通道數目一致。

  • meanStdDev(Mat src, MatOfDouble mean, MatOfDouble stddev, Mat mask)
    本方法實現的功能同上,
    不同的是這裡多了一個Mat型引數 mask
    表示只有當mask中對應位置的畫素值不等於零時,src中相同位置的畫素點才參與計算均值與標準方差

完整的基於均值實現影象二值分割的程式碼如下:

    public void meanAndDev() {
        // 載入影象
        Mat src = Imgcodecs.imread(fileUri.getPath());
        if(src.empty()){
            return;
        }
        // 轉為灰度影象
        Mat gray = new Mat();
        Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);

        // 計算均值與標準方差
        MatOfDouble means = new MatOfDouble();
        MatOfDouble stddevs = new MatOfDouble();
        Core.meanStdDev(gray, means, stddevs);

        // 顯示均值與標準方差
        double[] mean = means.toArray();
        double[] stddev = stddevs.toArray();
        Log.i(TAG, "gray image means : " + mean[0]);
        Log.i(TAG, "gray image stddev : " + stddev[0]);

        // 讀取畫素陣列
        int width = gray.cols();
        int height = gray.rows();
        byte[] data = new byte[width*height];
        gray.get(0, 0, data);
        int pv = 0;

        // 根據均值,二值分割
        int t = (int)mean[0];
        for(int i=0; i<data.length; i++) {
            pv = data[i]&0xff;
            if(pv > t) {
                data[i] = (byte)255;
            } else {
                data[i] = (byte)0;
            }
        }
        gray.put(0, 0, data);

        Bitmap bm = Bitmap.createBitmap(gray.cols(), gray.rows(), Bitmap.Config.ARGB_8888);

        Mat dst = new Mat();
        Imgproc.cvtColor(gray, dst, Imgproc.COLOR_GRAY2RGBA);
        Utils.matToBitmap(dst, bm);

        ImageView iv = (ImageView)this.findViewById(R.id.chapter3_imageView);
        iv.setImageBitmap(bm);

        dst.release();
        gray.release();
        src.release();
    }

最終得到的gray就是二值影象,轉換為Bitmap物件之後,通過ImageView顯示即可。

  • 另外,
    關於計算得到的標準方差,如上面的程式碼中假設stddev[0]的值小於5,那麼基本上影象可以看成是無效影象或者空白影象
    因為標準方差越小則說明影象各個畫素的差異越小,影象本身攜帶的有效資訊越少
  • 在影象處理中,可以利用這個結論來提取和過濾質量不高的掃描或者列印影象





3. 算術操作與調整影象的亮度和對比度

  • OpenCV的Core模組支援Mat物件的加、減、乘、除算術運算
    這些算術運算都處於Mat物件層次
    可以在任意兩個Mat之間實現上述算術操作,以得到結果
3.1 算術操作API的介紹
  • OpenCV中Mat的加、減、乘、除運算
    既可以在兩個Mat物件之間
    也可以在Mat物件與Scalar之間進行。

  • Mat物件之間的加、減、乘、除運算最常用的方法如下:
    add(Mat src1, Mat src2, Mat dst)
    subtract(Mat src1, Mat src2, Mat dst)
    multiply(Mat src1, Mat src2, Mat dst)
    divide(Mat src1, Mat src2, Mat dst)

  • 上述方法的引數個數與意義相同,具體解釋如下;
    src1:表示輸入的第一個Mat影象物件。
    src2:表示輸入的第二個Mat影象物件。
    dst:表示算術操作輸出的Mat物件。

  • 此外,src2的型別還可以是Scalar型別,
    這個時候表示影象的每個畫素點都與Scalar中的每個向量完成指定的算術運算

  • 注意在使用算術運算時候,
    src1、src2均為Mat物件的時候,
    它們的大小與型別必須一致
    預設的輸出影象型別與輸入影象型別一致

下面是一個簡單的算術運算的例子,使用加法,將兩個Mat物件的疊加結果輸出:

    public void matArithmeticDemo() {
        // 輸入影象src1
        Mat src = Imgcodecs.imread(fileUri.getPath());
        if(src.empty()){
            return;
        }
        // 輸入影象src2
        Mat moon = Mat.zeros(src.rows(), src.cols(), src.type());
        int cx = src.cols() - 60;
        int cy = 60;
        Imgproc.circle(moon, new Point(cx, cy), 50, new Scalar(90,95,234), -1, 8, 0);

        // 加法運算
        Mat dst = new Mat();
        Core.add(src, moon, dst);

        Bitmap bm = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888);
        Mat result = new Mat();
        Imgproc.cvtColor(dst, result, Imgproc.COLOR_BGR2RGBA);
        Utils.matToBitmap(result, bm);
        ImageView iv = (ImageView)this.findViewById(R.id.chapter3_imageView);
        iv.setImageBitmap(bm);
    }
3.2 調整影象的亮度和對比度
  • 影象的亮度和對比度影象的兩個基本屬性
    RGB色彩影象來說,
    亮度越高畫素點對應的RGB值應該越大,越接近255
    反之亮度越低,其畫素點對應的RGB值應該越小,越接近0
    所以在RGB色彩空間中,調整影象亮度可以簡單地通過對影象進行加法與減法操作來實現。

  • 影象對比度主要是用來描述影象顏色與亮度之間的差異感知,
    對比度越大,影象的每個畫素與周圍的差異性也就越大,整個影象的細節就越顯著
    反之亦然。
    通過對影象進行乘法或者除法操作擴大或者縮小影象畫素之間的差值,便可調整影象對比度

加減法只能使各個通道值保持差值(差距)去變大變小;
乘除法能放大縮小差值;

9125154-008ebfb0c0175674.png

基於Mat與Scalar算術操作,實現影象亮度或者對比度調整的程式碼實現如下:

    public void adjustBrightAndContrast(int b, float c) {
        // 輸入影象src1
        Mat src = Imgcodecs.imread(fileUri.getPath());
        if(src.empty()){
            return;
        }

        // 調整亮度
        Mat dst1 = new Mat();
        Core.add(src, new Scalar(b, b, b), dst1);

        // 調整對比度
        Mat dst2 = new Mat();
        Core.multiply(dst1, new Scalar(c, c, c), dst2);
        //至dst2,影象的兩個度已經調整完畢,就差個轉化型別而已

        // 轉換為Bitmap,顯示
        Bitmap bm = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888);
        Mat result = new Mat();
        Imgproc.cvtColor(dst2, result, Imgproc.COLOR_BGR2RGBA);
        Utils.matToBitmap(result, bm);

        // show
        ImageView iv = (ImageView)this.findViewById(R.id.chapter3_imageView);
        iv.setImageBitmap(bm);
    }
  • 上述程式碼中,b表示亮度引數c表示對比度引數
    其中,
    b的取值為負數時,表示調低亮度;為正數時,表示調高亮度
    c的取值是浮點數,使用經驗值範圍一般為0~3.0
    c的取值小於1時,表示降低對比度大於1時表示提升對比度





4. 基於權重的影象疊加

  • 對影象進行簡單的相加方法有時候並不能滿足需要
    這時可以通過引數調整輸入影象最終疊加之後的影象中所佔的權重比
    以實現基於權重方式的、更加靈活的影象調整方法

Core模組中已經實現了這樣的API函式,方法名稱與各個引數的解釋具體如下:

  • addWeighted(Mat src1, double alpha, Mat src2, double beta, double gamma, Mat dst)
    src1:表示輸入第一個Mat物件。
    alpha:表示混合時候第一個Mat物件所佔的權重大小。
    src2:表示輸入第二個Mat物件。
    beta:表示混合時候第二個Mat物件所佔的權重大小。
    gamma:表示混合之後是否進行亮度校正(提升或降低)。
    dst:表示輸出權重疊加之後的Mat物件。

  • 最常見的情況下,
    在進行兩個影象疊加的時候,權重調整需要滿足的條件為alpha + beta = 1.0,通常alpha = beta = 0.5
    表示混合疊加後的影象中原來兩副影象的畫素比值各佔一半,這些都是對於正常影象來說的。

  • 假設src2全黑色背景影象
    那麼這種疊加效果就是讓影象src1變得更加對比度變得更加
    在src2為黑色背景影象時,我們把alpha值調整為1.5beta值為-0.5
    這樣最終的疊加結果就是影象的對比度得到了提升
    alpha=1時候,則輸出原圖

  • 如果gamma不是預設值0,而是一個正整數的時候,那麼這時就會提升影象的亮度
    所以這種方式就成為調整影象亮度與對比度另外一種方式,而且它比上一節中提到的方法更簡潔、實用,只需一次呼叫就可以得到影象亮度與對比度調整後輸出的影象
    這種方法的公式化描述如下:

dst=src1*alpha+src2*beta+gamma

其中,
如果src2是純黑色的背景影象,
gamma大小決定了影象的亮度
alpha大小決定了影象的對比度(因為src2純黑色背景則基本無對比度,所以該由src1決定得多),
alpha+beta=1

基於權重疊加的影象亮度與對比度調整的完整程式碼實現如下:

    public void blendMat(double alpha, double gamma) {
        // 載入影象
        Mat src = Imgcodecs.imread(fileUri.getPath());
        if(src.empty()){
            return;
        }

        // create black image
        Mat black = Mat.zeros(src.size(), src.type());
        Mat dst = new Mat();

        // 畫素混合 - 基於權重
        Core.addWeighted(src, alpha, black, 1.0-alpha, gamma, dst);

        // 轉換為Bitmap,顯示
        Bitmap bm = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888);
        Mat result = new Mat();
        Imgproc.cvtColor(dst, result, Imgproc.COLOR_BGR2RGBA);
        Utils.matToBitmap(result, bm);

        // show
        ImageView iv = (ImageView)this.findViewById(R.id.chapter3_imageView);
        iv.setImageBitmap(bm);

    }

其中,
兩個引數alpha和gamma分別表示對比度與亮度調整的幅度,這裡的預設值分別為1.530
完整程式碼可以參考文末作者的GitHub;





5. Mat的其他各種畫素操作

OpenCV除了支援影象的算術操作之外,還支援影象的邏輯操作、平方、取LOG、歸一化值範圍等操作,
這些操作在處理複雜場景的影象二值或者灰度影象分析的時候非常有用。

影象邏輯操作相關的API與引數說明具體如下:

  • bitwise_not(Mat src, Mat dst) // 取反操作
    src:輸入影象。
    dst:取反之後的影象。
    取反操作二值影象來說是一個常見操作
    有時候我們需要先進行取反操作,然後再對影象進行更好地分析

  • bitwise_and(Mat src1, Mat src2, Mat dst) // 與操作
    src:輸入影象一。
    src2:輸入影象二。
    dst:與操作結果。
    與操作對兩張影象混合之後的輸出影象降低混合影象亮度的效果,
    會讓輸出的畫素小於等於對應位置的任意一張輸入影象的畫素值

(因唯兩個高值畫素相與得高值畫素,
高值與低值、低值與低值的結果都是低值,
於是三分之二的運算都是降低亮度的操作)

  • bitwise_or(Mat src1, Mat src2, Mat dst) // 或操作
    src1:輸入影象一。
    src2:輸入影象二。
    dst:或操作結果。
    或操作對兩張影象混合之後的輸出影象強化混合影象亮度的效果,
    會讓輸出的畫素大於等於對應位置的任意一張輸入影象的畫素值。

(其理解同與操作相反)

  • bitwise_xor(Mat src1, Mat src2, Mat dst) // 異或操作
    src1:輸入影象一。
    src2:輸入影象二。
    dst:或操作結果。

異或操作可以看作是對輸入影象的疊加取反效果

下面建立兩個Mat物件,
然後對它們完成位運算——邏輯與、或、非,
得到的結果將拼接為一張大Mat物件顯示,
完整的程式碼演示如下:

// 建立影象
Mat src1 = Mat.zeros(400, 400, CvType.CV_8UC3);
Mat src2 = new Mat(400, 400, CvType.CV_8UC3);
src2.setTo(new Scalar(255, 255, 255));

// ROI區域定義
Rect rect = new Rect();
rect.x=100;
rect.y=100;
rect.width = 200;
rect.height = 200;

// 繪製矩形
Imgproc.rectangle(src1, rect.tl(), rect.br(), new Scalar(0, 255, 0), -1);
rect.x=10;
rect.y=10;
Imgproc.rectangle(src2, rect.tl(), rect.br(), new Scalar(255, 255, 0), -1);

// 邏輯運算
Mat dst1 = new Mat();
Mat dst2 = new Mat();
Mat dst3 = new Mat();
Core.bitwise_and(src1, src2, dst1);
Core.bitwise_or(src1, src2, dst2);
Core.bitwise_xor(src1, src2, dst3);

// 輸出結果
Mat dst = Mat.zeros(400, 1200, CvType.CV_8UC3);
rect.x=0;
rect.y=0;
rect.width=400;
rect.height=400;
dst1.copyTo(dst.submat(rect));
rect.x=400;
dst2.copyTo(dst.submat(rect));
rect.x=800;
dst3.copyTo(dst.submat(rect));

// 釋放記憶體
dst1.release();
dst2.release();
dst3.release();

// 轉換為Bitmap,顯示
Bitmap bm = Bitmap.createBitmap(dst.cols(), dst.rows(), Bitmap.Config.ARGB_8888);
Mat result = new Mat();
Imgproc.cvtColor(dst, result, Imgproc.COLOR_BGR2RGBA);
Utils.matToBitmap(result, bm);

// show
ImageView iv = (ImageView)this.findViewById(R.id.chapter3_imageView);
iv.setImageBitmap(bm);

如上程式碼前文字所述,
三個輸出影象分別以x = 0, 400, 800為Mat矩陣左上角點拼接到結果Mat矩陣dst中:

9125154-0ce8accabc988e5a.png

  • 除了邏輯操作之外,
    還有兩個重要且常見的畫素操作是歸一化線性絕對值放縮變換
    其中歸一化是把資料re-scale到指定的範圍內,
    線性絕對值放縮是把任意範圍的畫素值變化到0~255的CV_8U的影象畫素值。

相關API解釋如下:

  • convertScaleAbs(Mat src, Mat dst) //線性絕對值放縮變換
    src:表示輸入影象。
    dst:表示輸出影象。
    預設情況下會對輸入Mat物件資料求得絕對值,並將其轉換為CV_8UC1型別輸出資料dst
  • normalize(Mat src, Mat dst, double alpha, double beta, int norm_type, int dtype, Mat mask)
    src:表示輸入影象。
    dst:表示輸出影象。
    alpha:表示歸一化到指定範圍的低值。
    beta:表示歸一化到指定範圍的高值。
    dtype:表示輸出的dst影象型別,預設為-1,表示型別與輸入影象src相同。
    mask:表示遮罩層,預設為Mat型別。
  • 歸一化在影象處理中是經常需要用到的方法,
    比如對浮點數進行計算得到輸出資料
    將資料歸一化到0~255後就可以作為彩色影象輸出,得到輸出結果。

(資料   只要經過   歸一化   就可以變成   彩色影象  輸出,劃重點!!!!!!)

下面簡單演示一下如何建立一個0~1的浮點數影象,
然後將其歸一化到0~255,
程式碼實現如下:

    public void normAndAbs() {
        // 建立隨機浮點數影象
        Mat src = Mat.zeros(400, 400, CvType.CV_32FC3);
        float[] data = new float[400*400*3];
        Random random = new Random();
        for(int i=0; i<data.length; i++) {
            data[i] = (float)random.nextGaussian();
        }
        src.put(0, 0, data);

        // 歸一化值到0~255之間
        Mat dst = new Mat();
        Core.normalize(src, dst, 0, 255, Core.NORM_MINMAX, -1, new Mat());


        // 型別轉換
        Mat dst8u = new Mat();
        dst.convertTo(dst8u, CvType.CV_8UC3);

        // 轉換為Bitmap,顯示
        Bitmap bm = Bitmap.createBitmap(dst.cols(), dst.rows(), Bitmap.Config.ARGB_8888);
        Mat result = new Mat();
        Imgproc.cvtColor(dst8u, result, Imgproc.COLOR_BGR2RGBA);
        Utils.matToBitmap(result, bm);

        // show
        ImageView iv = (ImageView)this.findViewById(R.id.chapter3_imageView);
        iv.setImageBitmap(bm);
    }

上述程式碼將建立一張大小為400×400高斯噪聲影象
其中歸一化方法選擇的是最小與最大值歸一化方法(NORM_MINMAX=32)
這種方法的數學表示如下:

9125154-778aaaa7417e0335.png

  • 圖解:
    9125154-933188235b7c47d6.png
    如圖所示,( x - min / max - min )必然是一個[0,1]的實數!

  • 另外,
    你會發現公式中,不加alpha對( 0 , 255 )這個範圍的歸一(即以上題境)沒有什麼影響,
    這是因為( x - min / max - min )光乘以(beta - alpha)不加最後的alpha只能歸一到範圍( 0 , beta )
    加上 最後的alpha才能歸一到( alpha , beta )

其中,
x表示src的畫素值,
min、max表示src中畫素的最小值與最大值
src 各個通道完成上述計算即可得到最終的歸一化結果

計算影象的結果有正負值,那麼在顯示之前呼叫convertScaleAbs()對負值求取絕對值影象
在後面的影象濾波與梯度計算中會用到該方法。

此外,Core中影象常見的操作還有對Mat做平方與取對數,這些操作都與實際應用場合有一定的關係,而且使用與引數都比較簡單,書中這裡沒再做過多的說明。

關於相關API的更多說明,我們可以檢視對應的OpenCV幫助文件。

參考資料

相關文章