###本文目录
#####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