本文目錄
1. 像素讀寫
2. 圖像通道與均值方差計(jì)算
3. 算術(shù)操作與調(diào)整圖像的亮度和對(duì)比度
4. 基于權(quán)重的圖像疊加
5. Mat的其他各種像素操作
1. 像素讀寫
- Mat作為
圖像容器
泻仙,其數(shù)據(jù)部分存儲(chǔ)了圖像的像素?cái)?shù)據(jù)
,我們可以通過相關(guān)的API來獲取圖像數(shù)據(jù)部分
量没; - 在獲取圖像數(shù)據(jù)的時(shí)候玉转,知道
Mat的類型
與通道數(shù)目
關(guān)重要,
根據(jù)Mat的類型
與通道數(shù)目
允蜈,開辟適當(dāng)大小的內(nèi)存空間
冤吨,
然后通過get方法
就可以循環(huán)實(shí)現(xiàn)每個(gè)像素點(diǎn)值的讀取蒿柳、修改
,
然后再通過put方法修改與Mat對(duì)應(yīng)的數(shù)據(jù)部分
漩蟆。
常見的Mat的像素讀寫get與put方法支持
如下表:
- 默認(rèn)情況下垒探,
imread
方式將Mat對(duì)象類型
加載為CV_8UC3
,
本系列筆記跟隨原著默認(rèn)
提到的加載圖像文件均為Mat對(duì)象怠李、類型均為CV_8UC3
圾叼、通道順序均為BGR
。 - 上表中所列舉的是當(dāng)前OpenCV支持的讀取圖像的方法捺癞;
使用時(shí)若需要將像素值寫入到Mat對(duì)象
中夷蚊,使用與每個(gè)get
方法相對(duì)應(yīng)的put
方法即可。 - 根據(jù)
開辟
的緩存區(qū)域data數(shù)組的大小
髓介,
讀寫像素既可以每次從Mat中讀取一個(gè)
像素點(diǎn)數(shù)據(jù)惕鼓,
或者可以每次從Mat中讀取一行
像素?cái)?shù)據(jù),
還可以一次從Mat中讀取全部
像素?cái)?shù)據(jù)唐础。
下面演示對(duì)Mat對(duì)象中的每個(gè)像素點(diǎn)的值都進(jìn)行取反操作
箱歧,并且分別用這三種方法實(shí)現(xiàn)像素操作
。
- 首先要將圖像
加載為Mat對(duì)象
一膨,
然后獲取圖像的寬呀邢、高以及通道數(shù)channels(特別注意這三個(gè)值,接下來一直用到豹绪,尤其channels)
:
Mat src = Imgcodecs.imread(fileUri.getPath());
if(src.empty()){
return;
}
int channels = src.channels();
int width = src.cols();
int height = src.rows();
接下來便可以通過方才所述三種方式讀取像素?cái)?shù)據(jù)价淌、修改、寫入
并比較它們的執(zhí)行時(shí)間
瞒津。
1.1.從Mat中每次讀取一個(gè)像素點(diǎn)數(shù)據(jù)
對(duì)于CV_8UC3
的Mat類型
來說蝉衣,對(duì)應(yīng)的數(shù)據(jù)類型
是byte
;
則先初始化byte數(shù)組data
仲智,用來存取每次讀取出來的一個(gè)像素點(diǎn)的所有通道值
买乃,
數(shù)組的長度
取決于圖像通道數(shù)目
。
完整代碼如下:
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);//!!!!!!!!!!!!!!!!!!!!!!!讀取一個(gè)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);
}
}
補(bǔ)充詮釋:
- 一個(gè)px有多個(gè)通道钓辆;
- 一個(gè)通道配給它一個(gè)數(shù)組元素剪验;
- 1.2中逐行讀取時(shí)的一個(gè)列(某行中的某個(gè)列其實(shí)就是一個(gè)數(shù)組元素而已)不是px,
而只是某個(gè)px的一個(gè)channel而已前联;- 1.3 同理
- 即1.2 以及1.3 中功戚,data的一個(gè)元素,不是px似嗤,而只是某個(gè)px的一個(gè)channel而已啸臀;
1.2 從Mat中每次讀取一行像素?cái)?shù)據(jù)
首先需要定義每一行像素?cái)?shù)據(jù)數(shù)組的長度
,這里為圖像寬度
乘以每個(gè)像素的通道數(shù)目
。
接著循環(huán)修改每一行的數(shù)據(jù)
乘粒;
這里get方法
的第二個(gè)參數(shù) col = 0
的意思是從每一行的第一列開始獲取像素?cái)?shù)據(jù)
豌注。
完整代碼如下:
// each row data
byte[] data = new byte[channels*width];//channels 是一個(gè)px的通道數(shù);width是一個(gè)行的px的個(gè)數(shù)灯萍;
// 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數(shù)據(jù)轧铁,存進(jìn)data;形象地說旦棉,是以 位置是(row齿风, 0)的第一個(gè)px的第一個(gè)channel為起始元素,獲取一個(gè)data長度的數(shù)據(jù)绑洛;
數(shù)據(jù)一個(gè)元素(channel)一個(gè)元素(channel)地存進(jìn)數(shù)組data救斑, 每個(gè)元素是某個(gè)px的一個(gè)channel;*/
for(int col=0; col<data.length; col++) {//行中循環(huán)列真屯,處理內(nèi)容:修改一整行的數(shù)據(jù)
// 讀取
pv = data[col]&0xff;
// 修改
pv = 255 - pv;
data[col] = (byte)pv;
}
// 至此脸候,data蓄滿一行修改好的px(channel)數(shù)據(jù)
// 寫入
src.put(row, 0, data);
}
關(guān)于代碼的補(bǔ)充詮釋:
byte[] data = new byte[channels*width];
中:
channels 是一個(gè)px的通道數(shù);
width是一個(gè)行的px的個(gè)數(shù)绑蔫;for(int row=0; row<height; row++)
:外層 for 循環(huán)行纪他;src.get(row, 0, data);
get一整行的px數(shù)據(jù),存進(jìn)data晾匠;
形象地說,
是以 位置是(row梯刚, 0)
即第一個(gè)px
的第一個(gè)channel
為起始元素
凉馆,
獲取一個(gè)data長度
的數(shù)據(jù);
數(shù)據(jù)一個(gè)元素(channel)
一個(gè)元素(channel)
地存進(jìn)數(shù)組data
亡资,
每個(gè)元素
是某個(gè)px
的一個(gè)channel
澜共;for(int col=0; col<data.length; col++)
次層 for ,
行中循環(huán)列锥腻,處理內(nèi)容:修改一整行的數(shù)據(jù)嗦董;- 次層for執(zhí)行完畢,data蓄滿一行修改好的px(channel)數(shù)據(jù)瘦黑;
src.put(row, 0, data)
:數(shù)組對(duì)象引用賦給行首京革,交付整行數(shù)據(jù);
形象地說幸斥,
是以 位置是(row匹摇, 0)
的第一個(gè)px
的第一個(gè)channel
為起始元素
,
提交一個(gè)data長度
的數(shù)據(jù)甲葬,即一整行廊勃;
1.3 從Mat中一次讀取全部像素?cái)?shù)據(jù)
- 首先定義
數(shù)組長度
,這里為圖像寬度×圖像高度×通道數(shù)目
经窖,
然后一次性獲取全部
像素?cái)?shù)據(jù)坡垫,
即get
的前面兩個(gè)參數(shù)row=0梭灿、col=0
,表示從第一個(gè)像素的第一個(gè)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);
關(guān)于代碼的補(bǔ)充詮釋(參考1.2的補(bǔ)充堡妒,不難理解):
src.get(0, 0, data);
get全部的px數(shù)據(jù),存進(jìn)data屿脐;
形象地說涕蚤,
是以 位置是(0, 0)
即第一個(gè)px
的第一個(gè)channel
為起始元素
的诵,
獲取一個(gè)data長度
的數(shù)據(jù)万栅;
數(shù)據(jù)一個(gè)元素(channel)
一個(gè)元素(channel)
地存進(jìn)數(shù)組data
,
每個(gè)元素
是某個(gè)px
的一個(gè)channel
西疤;src.put(0, 0, data)
:數(shù)組對(duì)象引用賦給行首烦粒,交付全部數(shù)據(jù);
形象地說代赁,
是以 位置是(0扰她, 0)
的第一個(gè)px
的第一個(gè)channel
為起始元素
,
提交一個(gè)data長度
的數(shù)據(jù)芭碍,即全部px的全部channel
徒役;
上述三種方法:
- 第一種方法因?yàn)轭l繁訪問
JNI調(diào)用(*!=押尽忧勿!* |get())
而效率低下,但是內(nèi)存(*U胺怼T!* |局部變量data的長度)
需求最兴儆隆晌砾; - 第二種方法每次讀取一行,相比第一種方法
速度有所提高
烦磁,但是內(nèi)存使用增加
养匈; - 第三種方法一次讀取Mat中的全部像素?cái)?shù)據(jù),在內(nèi)存中循環(huán)
修改速度最快
都伪,通過JNI調(diào)用OpenCV底層C++方法次數(shù)最少
乖寒,因而效率
也是最高
的,但是對(duì)于高分辨率圖像院溺,這種方式顯然內(nèi)存消耗過多
楣嘁,容易導(dǎo)致OOM問題
。
所以Android開發(fā)者在使用OpenCV的時(shí)候,
需要注意應(yīng)根據(jù)項(xiàng)目需求
逐虚,
選擇第二種
或者第三種方法
實(shí)現(xiàn)像素讀寫
聋溜,
第一種方法
只適用于隨機(jī)少量像素讀寫
的場合。
三種方法在實(shí)例項(xiàng)目中調(diào)試時(shí):
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 是一個(gè)px的通道數(shù)叭爱;width是一個(gè)行的px的個(gè)數(shù)撮躁;
// // 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數(shù)據(jù),存進(jìn)data买雾;形象地說把曼,是以 位置是(row, 0)的第一個(gè)px的第一個(gè)channel為起始元素漓穿,獲取一個(gè)data長度的數(shù)據(jù)嗤军;
// 數(shù)據(jù)一個(gè)元素(channel)一個(gè)元素(channel)地存進(jìn)數(shù)組data, 每個(gè)元素是某個(gè)px的一個(gè)channel晃危;*/
// for(int col=0; col<data.length; col++) {//行中循環(huán)列叙赚,處理內(nèi)容:修改一整行的數(shù)據(jù)
// // 讀取
// pv = data[col]&0xff;
// // 修改
// pv = 255 - pv;
// data[col] = (byte)pv;
// }
// // 至此,data蓄滿一行修改好的px(channel)數(shù)據(jù)
// // 寫入
// 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. 圖像通道與均值方差計(jì)算
-
圖像中通道數(shù)目的多少
可以通過Mat對(duì)象channels()
進(jìn)行查詢獲取
僚饭。 - 對(duì)于
多通道
的圖像震叮,Mat提供的API方法可以把它分為多個(gè)單通道
的圖像;
同樣對(duì)于多個(gè)單通道
的圖像鳍鸵,也可以組合
成一個(gè)多通道
的圖像苇瓣。 - OpenCV還提供了
計(jì)算
圖像每個(gè)通道像素平均值
與標(biāo)準(zhǔn)方差
的API方法,
通過它們可以計(jì)算得到圖像的像素平均值與方差
偿乖,
根據(jù)平均值
可以實(shí)現(xiàn)基于平均值的二值圖像分割
钓简,
根據(jù)標(biāo)準(zhǔn)方差
可以找到空白圖像
或者無效圖像
。
2.1 圖像通道分離與合并
-
圖像通道數(shù)
通過Mat的channels()
獲取之后汹想,
如果通道數(shù)目大于1,
那么根據(jù)需要調(diào)用split方法
就可以實(shí)現(xiàn)通道分離
撤蚊,
通過merge方法
就可以實(shí)現(xiàn)通道合并
古掏,
這兩個(gè)方法的詳細(xì)解釋具體如下:
split(Mat m, List<Mat> mv) // 通道分離
m
:表示輸入多通道
圖像。
mv
:表示分離
之后個(gè)單通道
圖像侦啸,mv的長度與m的通道數(shù)目一致槽唾。merge(List<Mat> mv, Mat dst) // 通道合并
mv
:表示多個(gè)待合并
的單通道
圖像。
dst
:表示合并之后
生成的多通道
圖像光涂。
上面兩個(gè)方法都來自Core模塊
庞萍,Core模塊
主要包含一些Mat操作
與基礎(chǔ)矩陣數(shù)學(xué)功能
。
一個(gè)簡單的多通道的Mat對(duì)象其分離與合并的代碼演示如下:
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,畢竟都調(diào)用了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();
}
上面的代碼實(shí)現(xiàn)了對(duì)多通道圖像
分離之后取反
忘闻,
然后再合并
钝计,
最后通過Android ImageView組件顯示結(jié)果
,
如此便是圖像通道分離與合并
的基本用法
;
2.2 .均值與標(biāo)準(zhǔn)方差計(jì)算與應(yīng)用
接下來的內(nèi)容是關(guān)于圖像Mat像素?cái)?shù)據(jù)的簡單統(tǒng)計(jì)
私恬,計(jì)算均值與方差
债沮。
-
對(duì)給定的一組數(shù)據(jù)計(jì)算其
均值μ
與標(biāo)準(zhǔn)方差stddev
的公式如下:n
表示數(shù)組的長度
本鸣、xi
表示數(shù)組第i個(gè)元素的值
疫衩。
n
表示數(shù)組長度
荣德、μ
表示均值
闷煤、1
表示自由度
。 根據(jù)上述公式涮瞻,
可以讀取每個(gè)像素點(diǎn)的值
鲤拿,
計(jì)算
每個(gè)通道像素的均值與標(biāo)準(zhǔn)方差
,
OpenCV Core模塊中已經(jīng)實(shí)現(xiàn)了這類API
饲宛,具體解釋如下:
meanStdDev(Mat src, MatOfDouble mean, MatOfDouble stddev)
src
:表示輸入Mat圖像皆愉。
mean
:表示計(jì)算出各個(gè)通道的均值
,數(shù)組長度與通道數(shù)目一致艇抠。
stddev
:表示計(jì)算出各個(gè)通道的標(biāo)準(zhǔn)方差
幕庐,數(shù)組長度與通道數(shù)目一致。meanStdDev(Mat src, MatOfDouble mean, MatOfDouble stddev, Mat mask)
本方法實(shí)現(xiàn)的功能同上家淤,
不同的是這里多了一個(gè)Mat型參數(shù) mask
异剥;
表示只有當(dāng)mask中對(duì)應(yīng)位置的像素值不等于零
時(shí),src中相同位置的像素點(diǎn)
才參與計(jì)算均值與標(biāo)準(zhǔn)方差
絮重。
完整的基于均值實(shí)現(xiàn)圖像二值分割的代碼如下:
public void meanAndDev() {
// 加載圖像
Mat src = Imgcodecs.imread(fileUri.getPath());
if(src.empty()){
return;
}
// 轉(zhuǎn)為灰度圖像
Mat gray = new Mat();
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
// 計(jì)算均值與標(biāo)準(zhǔn)方差
MatOfDouble means = new MatOfDouble();
MatOfDouble stddevs = new MatOfDouble();
Core.meanStdDev(gray, means, stddevs);
// 顯示均值與標(biāo)準(zhǔn)方差
double[] mean = means.toArray();
double[] stddev = stddevs.toArray();
Log.i(TAG, "gray image means : " + mean[0]);
Log.i(TAG, "gray image stddev : " + stddev[0]);
// 讀取像素?cái)?shù)組
int width = gray.cols();
int height = gray.rows();
byte[] data = new byte[width*height];
gray.get(0, 0, data);
int pv = 0;
// 根據(jù)均值冤寿,二值分割
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
就是二值圖像
,轉(zhuǎn)換為Bitmap對(duì)象
之后青伤,通過ImageView顯示
即可督怜。
- 另外,
關(guān)于計(jì)算得到的標(biāo)準(zhǔn)方差
狠角,如上面的代碼中假設(shè)stddev[0]的值小于5
号杠,那么基本上圖像可以看成是無效圖像
或者空白圖像
,
因?yàn)?code>標(biāo)準(zhǔn)方差越小則說明圖像各個(gè)像素的差異越小
丰歌,圖像本身攜帶的有效信息越少
姨蟋;- 在圖像處理中,可以利用這個(gè)結(jié)論來
提取和過濾質(zhì)量不高的掃描或者打印圖像
立帖。
3. 算術(shù)操作與調(diào)整圖像的亮度和對(duì)比度
- OpenCV的
Core
模塊支持Mat對(duì)象的加眼溶、減、乘晓勇、除算術(shù)運(yùn)算
堂飞,
這些算術(shù)運(yùn)算都處于Mat對(duì)象層次
灌旧,
可以在任意兩個(gè)Mat之間
實(shí)現(xiàn)上述算術(shù)操作,以得到結(jié)果
酝静。
3.1 算術(shù)操作API的介紹
OpenCV中
Mat的加节榜、減、乘别智、除運(yùn)算
宗苍,
既可以在兩個(gè)Mat對(duì)象之間
,
也可以在Mat對(duì)象與Scalar之間
進(jìn)行薄榛。Mat對(duì)象之間的加讳窟、減、乘敞恋、除運(yùn)算最常用的方法
如下:
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)
上述方法的參數(shù)個(gè)數(shù)與意義相同丽啡,具體解釋如下;
src1
:表示輸入的第一個(gè)
Mat圖像對(duì)象。
src2
:表示輸入的第二個(gè)
Mat圖像對(duì)象硬猫。
dst
:表示算術(shù)操作輸出
的Mat對(duì)象补箍。此外,
src2
的類型還可以是Scalar
類型啸蜜,
這個(gè)時(shí)候表示圖像的每個(gè)像素點(diǎn)
都與Scalar中的每個(gè)向量
完成指定的算術(shù)運(yùn)算
坑雅。注意
在使用算術(shù)
運(yùn)算時(shí)候,
當(dāng)src1衬横、src2
均為Mat對(duì)象
的時(shí)候裹粤,
它們的大小與類型
必須一致
,
默認(rèn)的輸出
圖像類型與輸入
圖像類型一致
蜂林。
下面是一個(gè)簡單的算術(shù)運(yùn)算的例子遥诉,使用加法,將兩個(gè)Mat對(duì)象的疊加結(jié)果輸出:
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);
// 加法運(yùn)算
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 調(diào)整圖像的亮度和對(duì)比度
圖像的
亮度和對(duì)比度
是圖像
的兩個(gè)基本屬性
噪叙,
對(duì)RGB色彩圖像
來說矮锈,
亮度越高
,像素點(diǎn)對(duì)應(yīng)的RGB值
應(yīng)該越大
睁蕾,越接近255
苞笨;
反之亮度越低
,其像素點(diǎn)對(duì)應(yīng)的RGB值
應(yīng)該越小
惫霸,越接近0
。
所以在RGB色彩空間
中葱弟,調(diào)整圖像亮度
可以簡單地通過對(duì)圖像進(jìn)行加法與減法操作
來實(shí)現(xiàn)壹店。圖像對(duì)比度
主要是用來描述圖像顏色與亮度之間的差異感知,
對(duì)比度越大
芝加,圖像的每個(gè)像素與周圍的差異性
也就越大
硅卢,整個(gè)圖像的細(xì)節(jié)
就越顯著
射窒;
反之亦然。
通過對(duì)圖像進(jìn)行乘法或者除法操作
來擴(kuò)大或者縮小圖像像素之間的差值
将塑,便可調(diào)整圖像對(duì)比度
脉顿。
加減法
只能使各個(gè)通道值保持差值(差距)去變大變小点寥;
而乘除法
能放大縮小差值艾疟;
基于Mat與Scalar
的算術(shù)
操作,實(shí)現(xiàn)圖像亮度
或者對(duì)比度調(diào)整
的代碼實(shí)現(xiàn)如下:
public void adjustBrightAndContrast(int b, float c) {
// 輸入圖像src1
Mat src = Imgcodecs.imread(fileUri.getPath());
if(src.empty()){
return;
}
// 調(diào)整亮度
Mat dst1 = new Mat();
Core.add(src, new Scalar(b, b, b), dst1);
// 調(diào)整對(duì)比度
Mat dst2 = new Mat();
Core.multiply(dst1, new Scalar(c, c, c), dst2);
//至dst2敢辩,圖像的兩個(gè)度已經(jīng)調(diào)整完畢蔽莱,就差個(gè)轉(zhuǎn)化類型而已
// 轉(zhuǎn)換為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
表示亮度參數(shù)
盗冷,c
表示對(duì)比度參數(shù)
;
其中同廉,
b
的取值為負(fù)數(shù)
時(shí)仪糖,表示調(diào)低亮度
;為正數(shù)
時(shí)迫肖,表示調(diào)高亮度
锅劝;
c
的取值是浮點(diǎn)數(shù)
,使用經(jīng)驗(yàn)值范圍
一般為0~3.0
咒程,
c
的取值小于1
時(shí)鸠天,表示降低對(duì)比度
,大于1
時(shí)表示提升對(duì)比度
帐姻。
4. 基于權(quán)重的圖像疊加
- 對(duì)圖像進(jìn)行
簡單的相加
方法有時(shí)候并不能滿足需要
稠集,
這時(shí)可以通過參數(shù)
來調(diào)整輸入圖像
在最終疊加之后的圖像中所占的權(quán)重比
,
以實(shí)現(xiàn)基于權(quán)重方式的饥瓷、更加靈活的圖像調(diào)整方法
剥纷。
Core模塊中已經(jīng)實(shí)現(xiàn)了這樣的API函數(shù),方法名稱與各個(gè)參數(shù)的解釋具體如下:
addWeighted(Mat src1, double alpha, Mat src2, double beta, double gamma, Mat dst)
src1
:表示輸入
的第一個(gè)
Mat對(duì)象呢铆。
alpha
:表示混合時(shí)候第一個(gè)
Mat對(duì)象所占的權(quán)重
大小晦鞋。
src2
:表示輸入
的第二個(gè)
Mat對(duì)象。
beta
:表示混合時(shí)候第二個(gè)
Mat對(duì)象所占的權(quán)重
大小棺克。
gamma
:表示混合之后是否進(jìn)行亮度校正
(提升或降低)悠垛。
dst
:表示輸出
權(quán)重疊加之后的Mat對(duì)象。最常見
的情況下娜谊,
在進(jìn)行兩個(gè)圖像疊加的時(shí)候
确买,權(quán)重調(diào)整需要滿足的條件為alpha + beta = 1.0
,通常alpha = beta = 0.5
纱皆,
表示混合疊加后的圖像中原來兩副圖像的像素比值各占一半
湾趾,這些都是對(duì)于正常圖像
來說的芭商。假設(shè)
src2
是全黑色背景圖像
,
那么這種疊
加效果就是讓圖像src1
變得更加暗
搀缠,對(duì)比度
變得更加低
铛楣;
在src2為黑色背景圖像時(shí),我們把alpha
值調(diào)整為1.5
艺普,beta
值為-0.5
簸州,
這樣最終的疊加結(jié)果就是圖像的對(duì)比度得到了提升
;
當(dāng)alpha=1
時(shí)候衷敌,則輸出原圖
勿侯。如果
gamma
不是默認(rèn)值0
,而是一個(gè)正整數(shù)
的時(shí)候缴罗,那么這時(shí)就會(huì)提升圖像的亮度
助琐,
所以這種方式就成為調(diào)整圖像亮度與對(duì)比度
的另外一種方式
,而且它比上一節(jié)中提到的方法更簡潔面氓、實(shí)用
兵钮,只需一次調(diào)用
就可以得到圖像亮度與對(duì)比度調(diào)整后輸出的圖像
。
這種方法的公式化描述如下:
dst=src1*alpha+src2*beta+gamma
其中舌界,
如果src2
是純黑色的背景圖像掘譬,
則gamma
大小決定了圖像的亮度
,
alpha
大小決定了圖像的對(duì)比度
(因?yàn)閟rc2純黑色背景則基本無對(duì)比度呻拌,所以該由src1決定得多)戒洼,
alpha+beta=1
狂秦。
基于權(quán)重疊加的圖像亮度與對(duì)比度調(diào)整
的完整代碼實(shí)現(xiàn)如下:
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();
// 像素混合 - 基于權(quán)重
Core.addWeighted(src, alpha, black, 1.0-alpha, gamma, dst);
// 轉(zhuǎn)換為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);
}
其中,
兩個(gè)參數(shù)alpha和gamma
分別表示對(duì)比度與亮度調(diào)整的幅度
污它,這里的默認(rèn)值分別為1.5
和30
绎速。
完整代碼可以參考文末作者的GitHub虾啦;
5. Mat的其他各種像素操作
OpenCV除了支持圖像的算術(shù)操作
之外锰扶,還支持圖像的邏輯操作、平方初家、取LOG偎窘、歸一化值范圍
等操作,
這些操作在處理復(fù)雜場景的圖像
與二值
或者灰度圖像分析
的時(shí)候非常有用溜在。
圖像邏輯操作相關(guān)的API與參數(shù)說明具體如下:
bitwise_not(Mat src, Mat dst) // 取反操作
src
:輸入圖像陌知。
dst
:取反之后的圖像掖肋。
取反操作
對(duì)二值圖像
來說是一個(gè)常見操作
,
有時(shí)候我們需要先進(jìn)行取反操作
浙芙,然后再對(duì)圖像進(jìn)行更好地分析
籽腕。bitwise_and(Mat src1, Mat src2, Mat dst) // 與操作
src
:輸入圖像一。
src2
:輸入圖像二南窗。
dst
:與操作結(jié)果万伤。
與操作對(duì)兩張圖像混合之后的輸出圖像
有降低混合圖像亮度
的效果呜袁,
會(huì)讓輸出的像素小于等于對(duì)應(yīng)位置的任意一張輸入圖像的像素值
敌买。
(因唯兩個(gè)高值像素相與得高值像素,
高值與低值阶界、低值與低值的結(jié)果都是低值虹钮,
于是三分之二的運(yùn)算都是降低亮度的操作)
-
bitwise_or(Mat src1, Mat src2, Mat dst) // 或操作
src1
:輸入圖像一。
src2
:輸入圖像二膘融。
dst
:或操作結(jié)果芙粱。
或操作對(duì)兩張圖像混合之后的輸出圖像
有強(qiáng)化混合圖像亮度
的效果,
會(huì)讓輸出的像素大于等于對(duì)應(yīng)位置的任意一張輸入圖像的像素值氧映。
(其理解同與操作相反)
-
bitwise_xor(Mat src1, Mat src2, Mat dst) // 異或操作
src1
:輸入圖像一春畔。
src2
:輸入圖像二。
dst
:或操作結(jié)果岛都。
異或操作
可以看作是對(duì)輸入圖像的疊加取反效果
律姨。
下面創(chuàng)建兩個(gè)Mat對(duì)象,
然后對(duì)它們完成位運(yùn)算——邏輯與疗绣、或线召、非,
得到的結(jié)果將拼接
為一張大Mat對(duì)象顯示多矮,
完整的代碼演示如下:
// 創(chuàng)建圖像
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區(qū)域定義
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);
// 邏輯運(yùn)算
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);
// 輸出結(jié)果
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));
// 釋放內(nèi)存
dst1.release();
dst2.release();
dst3.release();
// 轉(zhuǎn)換為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);
如上代碼前文字所述,
三個(gè)輸出圖像分別以x = 0, 400, 800為Mat矩陣左上角點(diǎn)拼接
到結(jié)果Mat矩陣dst中:
- 除了邏輯操作之外塔逃,
還有兩個(gè)重要且常見的像素操作是歸一化
與線性絕對(duì)值放縮變換
讯壶,
其中歸一化
是把數(shù)據(jù)re-scale到指定的范圍內(nèi),
線性絕對(duì)值放縮
是把任意范圍的像素值變化到0~255的CV_8U的圖像像素值湾盗。
相關(guān)API解釋如下:
-
convertScaleAbs(Mat src, Mat dst) //線性絕對(duì)值放縮變換
src
:表示輸入圖像伏蚊。
dst
:表示輸出圖像躏吊。
默認(rèn)情況下會(huì)對(duì)輸入Mat對(duì)象數(shù)據(jù)求得絕對(duì)值胜卤,并將其轉(zhuǎn)換為CV_8UC1類型
的輸出數(shù)據(jù)dst
葛躏。
-
normalize(Mat src, Mat dst, double alpha, double beta, int norm_type, int dtype, Mat mask)
src
:表示輸入圖像。
dst
:表示輸出圖像摩窃。
alpha
:表示歸一化到指定范圍的低值。
beta
:表示歸一化到指定范圍的高值匪蟀。
dtype
:表示輸出的dst圖像類型材彪,默認(rèn)為-1,表示類型與輸入圖像src相同显熏。
mask
:表示遮罩層喘蟆,默認(rèn)為Mat類型。 -
歸一化
在圖像處理中是經(jīng)常需要用到的方法橙弱,
比如對(duì)浮點(diǎn)數(shù)進(jìn)行計(jì)算
得到輸出數(shù)據(jù)
斜筐,
將數(shù)據(jù)歸一化到0~255后就可以作為彩色圖像輸出
,得到輸出結(jié)果。
(數(shù)據(jù) 只要經(jīng)過 歸一化 就可以變成 彩色圖像 輸出俘闯,劃重點(diǎn)U胬省!F炱恕M畏馈!V略铩)
下面簡單演示一下如何創(chuàng)建一個(gè)0~1的浮點(diǎn)數(shù)圖像嫌蚤,
然后將其歸一化到0~255搬葬,
代碼實(shí)現(xiàn)如下:
public void normAndAbs() {
// 創(chuàng)建隨機(jī)浮點(diǎn)數(shù)圖像
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());
// 類型轉(zhuǎn)換
Mat dst8u = new Mat();
dst.convertTo(dst8u, CvType.CV_8UC3);
// 轉(zhuǎn)換為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);
}
上述代碼將創(chuàng)建一張大小為400×400
的高斯噪聲圖像
疾忍,
其中歸一化方法
選擇的是最小與最大值歸一化方法(NORM_MINMAX=32)
一罩,
這種方法的數(shù)學(xué)表示如下:
- 圖解:
如圖所示四瘫,( x - min / max - min )
必然是一個(gè)[0,1]
的實(shí)數(shù)找蜜!
- 另外弓叛,
你會(huì)發(fā)現(xiàn)公式中撰筷,不加alpha對(duì)( 0 , 255 )
這個(gè)范圍的歸一(即以上題境)沒有什么影響,
這是因?yàn)?code>( x - min / max - min )光乘以(beta - alpha)
不加最后的alpha
只能歸一到范圍( 0 影钉, beta )
平委;
加上 最后的alpha
才能歸一到( alpha , beta )
蜡塌;
其中馏艾,
x
表示src
的像素值,
min房资、max
表示src
中像素的最小值與最大值
岖沛,
對(duì) src 各個(gè)通道
完成上述計(jì)算即可得到最終的歸一化結(jié)果
婴削。
若計(jì)算圖像的結(jié)果有正負(fù)值
期升,那么在顯示之前
會(huì)調(diào)用convertScaleAbs()
來對(duì)負(fù)值求取絕對(duì)值圖像
播赁,
在后面的圖像濾波與梯度計(jì)算中會(huì)用到該方法乓序。
此外,Core中圖像常見的操作還有對(duì)Mat做平方與取對(duì)數(shù)
陨献,這些操作都與實(shí)際應(yīng)用場合有一定的關(guān)系,而且使用與參數(shù)都比較簡單龄捡,書中這里沒再做過多的說明聘殖。
關(guān)于相關(guān)API的更多說明,我們可以查看對(duì)應(yīng)的OpenCV幫助文檔坠宴。
參考資料
- 《OpenCV Android 開發(fā)實(shí)戰(zhàn)》(賈志剛 著)
- 關(guān)于本書作者的GitHub項(xiàng)目
- 基于作者GitHub維護(hù)的APP