专栏首页李蔚蓬的专栏计算机视觉 OpenCV Android | Mat像素操作

计算机视觉 OpenCV Android | 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_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中一次读取全部像素数据
  • 首先定义数组长度,这里为图像宽度×图像高度×通道数目, 然后一次性获取全部像素数据, 即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的时候, 需要注意应根据项目需求, 选择第二种或者第三种方法实现像素读写第一种方法只适用于随机少量像素读写的场合。


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) 该功能与第一个方法所实现的功能完全一致,唯一不同的是多了一个参数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,那么基本上图像可以看成是无效图像或者空白图像,因为标准方差越小则说明图像各个像素的差异越小,图像本身携带的有效信息越少。在图像处理中,我们可以利用上述结论来提取和过滤质量不高的扫描或者打印图像。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 自定义View | 仿QQ运动步数进度效果

    凌川江雪
  • OpenCV | 基于Android系统详析Mat与Bitmap对象(创建、初始化、使用与转换 | 附大量demo)

    下图形象地展示了一张图像中的各个像素点数据是如何存储的, 因为图像本身的像素点比较多,下图显示的图像像素数据只是图片左上角20×20大小的部分数据:

    凌川江雪
  • Tip | 数据类型占位 & 降采样 & 像素读取 & Bitmap & Color源码

    下面修改通道的时候使用的是位运算, 其实对比Color源码我们知道这跟调用Color的API是一样的:

    凌川江雪
  • Android实现垂直进度条VerticalSeekBar

    本文实例为大家分享了Android实现垂直进度条的具体代码,供大家参考,具体内容如下

    砸漏
  • 程序员必须掌握哪些算法和数据结构?

    以下 5 个步骤总结了此方法,依次为如下,我们设计的实验必须是可以重现的,我们形成的假设必须是具有真伪的。

    CSDN技术头条
  • 浙大版《C语言程序设计(第3版)》题目集 习题10-7 十进制转换二进制

    C you again 的博客
  • UVALive 6933 Virus synthesis(回文树)

    Viruses are usually bad for your health. How about ghting them with... other vir...

    ShenduCC
  • 用 WPF 写的颜色拾取器

    之前都是用别人的颜色拾取器,今天自己用WPF写了一个颜色拾取器小程序 拾取鼠标所在位置的颜色,按键盘上的空格键停止取色 程序下载:MyWPFScreenColo...

    lpxxn
  • palindrome - 132. Palindrome Partitioning II

    Given a string s, partition s such that every substring of the partition is a pa...

    用户5705150
  • 原 初学图论-DAG单源最短路径算法

    不高不富不帅的陈政_

扫码关注云+社区

领取腾讯云代金券