图像容器
,其数据部分存储了图像的像素数据
,我们可以通过相关的API来获取图像数据部分
;Mat的类型
与通道数目
关重要,
根据Mat的类型
与通道数目
,开辟适当大小的内存空间
,
然后通过get方法
就可以循环实现每个像素点值的读取、修改
,
然后再通过put方法修改与Mat对应的数据部分
。常见的Mat的像素读写get与put方法支持
如下表:
imread
方式将Mat对象类型
加载为CV_8UC3
,
本系列笔记跟随原著默认
提到的加载图像文件均为Mat对象、类型均为CV_8UC3
、通道顺序均为BGR
。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();
接下来便可以通过方才所述三种方式读取像素数据、修改、写入
并比较它们的执行时间
。
对于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);
}
}
补充诠释:
首先需要定义每一行像素数据数组的长度
,这里为图像宽度
乘以每个像素的通道数目
。
接着循环修改每一行的数据
;
这里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 ,
行中循环列,处理内容:修改一整行的数据;src.put(row, 0, data)
:数组对象引用赋给行首,交付整行数据;
形象地说,
是以 位置是(row, 0)
的第一个px
的第一个channel
为起始元素
,
提交一个data长度
的数据,即一整行;数组长度
,这里为图像宽度×图像高度×通道数目
,
然后一次性获取全部
像素数据,
即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的长度)
需求最小;速度有所提高
,但是内存使用增加
;修改速度最快
,通过JNI调用OpenCV底层C++方法次数最少
,因而效率
也是最高
的,但是对于高分辨率图像,这种方式显然内存消耗过多
,容易导致OOM问题
。所以Android开发者在使用OpenCV的时候, 需要
注意应根据项目需求
, 选择第二种
或者第三种方法
实现像素读写
,第一种方法
只适用于随机少量像素读写
的场合。
图像中通道数目的多少
可以通过Mat对象channels()
进行查询获取
。多通道
的图像,Mat提供的API方法可以把它分为多个单通道
的图像;
同样对于多个单通道
的图像,也可以组合
成一个多通道
的图像。计算
图像每个通道像素平均值
与标准方差
的API方法,
通过它们可以计算得到图像的像素平均值与方差
,
根据平均值
可以实现基于平均值的二值图像分割
,
根据标准方差
可以找到空白图像
或者无效图像
。图像通道数
通过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组件显示结果
, 如此便是图像通道分离与合并
的基本用法
;
接下来的内容是关于图像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)
该功能与第一个方法所实现的功能完全一致,唯一不同的是多了一个参数mask,表示只有当mask中对应位置的像素值不等于零的时候,src中相同位置的像素点才参与计算均值与标准方差。完整的基于均值实现图像二值分割的代码如下:
// 加载图像
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);
最终得到的gray就是二值图像,转换为Bitmap对象之后,通过ImageView显示即可。此外,可根据计算得到标准方差,上面的代码中假设stddev[0]的值小于5,那么基本上图像可以看成是无效图像或者空白图像,因为标准方差越小则说明图像各个像素的差异越小,图像本身携带的有效信息越少。在图像处理中,我们可以利用上述结论来提取和过滤质量不高的扫描或者打印图像。