Android开发程序员Android开发

计算机视觉 OpenCV Android | Mat像素操作

2019-01-29  本文已影响21人  凌川江雪

本文目录

1. 像素读写
2. 图像通道与均值方差计算
3. 算术操作与调整图像的亮度和对比度
4. 基于权重的图像叠加
5. Mat的其他各种像素操作




1. 像素读写

常见的Mat的像素读写get与put方法支持如下表:

下面演示对Mat对象中的每个像素点的值都进行取反操作,并且分别用这三种方法实现像素操作

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_8UC3Mat类型来说,对应的数据类型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中一次读取全部像素数据

完整代码如下:

// 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

上述三种方法

所以Android开发者在使用OpenCV的时候,
需要注意应根据项目需求
选择第二种或者第三种方法实现像素读写
第一种方法只适用于随机少量像素读写的场合。




2. 图像通道与均值方差计算

2.1 图像通道分离与合并

这两个方法的详细解释具体如下:

上面两个方法都来自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像素数据的简单统计,计算均值与方差

OpenCV Core模块中已经实现了这类API,具体解释如下:

完整的基于均值实现图像二值分割的代码如下:

// 加载图像
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,那么基本上图像可以看成是无效图像或者空白图像
    因为标准方差越小则说明图像各个像素的差异越小,图像本身携带的有效信息越少
  • 在图像处理中,可以利用这个结论来提取和过滤质量不高的扫描或者打印图像




3. 算术操作与调整图像的亮度和对比度

3.1 算术操作API的介绍

下面是一个简单的算术运算的例子,使用加法,将两个Mat对象的叠加结果输出:

// 输入图像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);
3.2 调整图像的亮度和对比度

加减法只能使各个通道值保持差值(差距)去变大变小;
乘除法能放大缩小差值;

基于Mat与Scalar算术操作,实现图像亮度或者对比度调整的代码实现如下:

// 输入图像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);




4. 基于权重的图像叠加

Core模块中已经实现了这样的API函数,方法名称与各个参数的解释具体如下:

dst=src1*alpha+src2*beta+gamma

其中,
如果src2是纯黑色的背景图像,
gamma大小决定了图像的亮度
alpha大小决定了图像的对比度(因为src2纯黑色背景则基本无对比度,所以该由src1决定得多),
alpha+beta=1

基于权重叠加的图像亮度与对比度调整的完整代码实现如下:

// 加载图像
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);

其中,
两个参数alpha和gamma分别表示对比度与亮度调整的幅度,这里的默认值分别为1.530
完整代码可以参考文末作者的GitHub;




5. Mat的其他各种像素操作

OpenCV除了支持图像的算术操作之外,还支持图像的逻辑操作、平方、取LOG、归一化值范围等操作,
这些操作在处理复杂场景的图像二值或者灰度图像分析的时候非常有用。

图像逻辑操作相关的API与参数说明具体如下:

(因唯两个高值像素相与得高值像素,
高值与低值、低值与低值的结果都是低值,
于是三分之二的运算都是降低亮度的操作)

(其理解同与操作相反)

异或操作可以看作是对输入图像的叠加取反效果

下面创建两个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中:

相关API解释如下:


(数据   只要经过   归一化   就可以变成   彩色图像  输出,划重点!!!!!!)

下面简单演示一下如何创建一个0~1的浮点数图像,
然后将其归一化到0~255,
代码实现如下:

// 创建随机浮点数图像
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);

上述代码将创建一张大小为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帮助文档。

参考资料
上一篇 下一篇

猜你喜欢

热点阅读