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