計算機視覺 OpenCV Android | Mat畫素操作(影象畫素的讀寫、均值方差、算術、邏輯等運算、權重疊加、歸一化等操作)...
本文目錄
1. 畫素讀寫
2. 影象通道與均值方差計算
3. 算術操作與調整影象的亮度和對比度
4. 基於權重的影象疊加
5. Mat的其他各種畫素操作
1. 畫素讀寫
- Mat作為
影象容器
,其資料部分儲存了影象的畫素資料
,我們可以通過相關的API來獲取影象資料部分
; - 在獲取影象資料的時候,知道
Mat的型別
與通道數目
關重要,
根據Mat的型別
與通道數目
,開闢適當大小的記憶體空間
,
然後通過get方法
就可以迴圈實現每個畫素點值的讀取、修改
,
然後再通過put方法修改與Mat對應的資料部分
。
常見的Mat的畫素讀寫get與put方法支援
如下表:
- 預設情況下,
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_8UC3
的Mat型別
來說,對應的資料型別
是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
的公式如下:n
表示陣列的長度
、xi
表示陣列第i個元素的值
。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色彩空間
中,調整影象亮度
可以簡單地通過對影象進行加法與減法操作
來實現。影象對比度
主要是用來描述影象顏色與亮度之間的差異感知,對比度越大
,影象的每個畫素與周圍的差異性
也就越大
,整個影象的細節
就越顯著
;
反之亦然。
通過對影象進行乘法或者除法操作
來擴大或者縮小影象畫素之間的差值
,便可調整影象對比度
。
加減法
只能使各個通道值保持差值(差距)去變大變小;
而乘除法
能放大縮小差值;
基於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.5
,beta
值為-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.5
和30
。
完整程式碼可以參考文末作者的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中:
- 除了邏輯操作之外,
還有兩個重要且常見的畫素操作是歸一化
與線性絕對值放縮變換
,
其中歸一化
是把資料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)
,
這種方法的數學表示如下:
- 圖解:
如圖所示,( 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幫助文件。
參考資料
- 《OpenCV Android 開發實戰》(賈志剛 著)
- 關於本書作者的GitHub專案
- 基於作者GitHub維護的APP
相關文章
- OpenCV計算機視覺學習(2)——影像算術運算 & 掩膜mask操作(數值計算,影像融合,邊界填充)OpenCV計算機視覺
- 什麼是物理畫素和邏輯畫素?
- 一道多媒體畫素計算題
- 掌握web開發基礎系列--物理畫素、邏輯畫素、css畫素WebCSS
- 移動前端適配—邏輯畫素和物理畫素前端
- Android粒子篇之Bitmap畫素級操作Android
- opencv 修改畫素OpenCV
- 計算機視覺—影象特效(3)計算機視覺特效
- 計算資料集均值方差
- opencv 梯度運算、禮貌操作OpenCV梯度
- 影象中的畫素處理
- [Python影象處理] 二.OpenCV+Numpy庫讀取與修改畫素PythonOpenCV
- OpenCV計算機視覺學習(15)——淺談影像處理的飽和運算和取模運算OpenCV計算機視覺
- canvas畫素點操作 —— 視訊綠幕摳圖Canvas
- 計算素數【Java】Java
- 常用的畫素操作演算法:影像加法、畫素混合、提取影像中的ROI演算法
- MATLAB 寫log file自動輸出計算資料等操作Matlab
- 計算機最基礎的部分:運算邏輯電路(ALU)計算機
- 計算機影象與視覺入門必備計算機視覺
- Python邏輯運算Python
- day14.邏輯運算,位運算
- 機器學習之邏輯迴歸:計算概率機器學習邏輯迴歸
- 機器學習之邏輯迴歸:計算機率機器學習邏輯迴歸計算機
- 常用的畫素操作演算法:Resize、Flip、Rotate演算法
- 計算 1-100 的素數
- 計算機視覺 OpenCV Android | 基本特徵檢測之 霍夫圓檢計算機視覺OpenCVAndroid特徵
- opencv 開運算、閉運算OpenCV
- 6-2 計算素數和
- 計算機視覺技術專利分析計算機視覺
- canvas畫素畫板Canvas
- Python 影像處理 OpenCV (2):畫素處理與 Numpy 操作以及 Matplotlib 顯示影像PythonOpenCV
- OpenCV計算機視覺學習(1)——影像基本操作(影像視訊讀取,ROI區域擷取,常用cv函式解釋)OpenCV計算機視覺函式
- 修改畫素
- [Python影象處理] 九.形態學之影象開運算、閉運算、梯度運算Python梯度
- 計算機導論微機操作計算機
- iOS計算機視覺—ARKitiOS計算機視覺
- 計算機視覺論文集計算機視覺
- 畫素畫裡的孤獨