Android技术知识Android开发Android开发经验谈

OpenCV | 基于Android系统详析Mat与Bitmap

2019-01-26  本文已影响5人  凌川江雪

1. Mat对象

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




1.1 加载图片与读取基本信息
Mat src = Imgcodecs.imread(fileUri.getPath()); 
Mat src = Imgcodecs.imread(fileUri.getPath(), Imgcodecs.IMREAD_COLOR)

如上这句代码,
第一个参数表示文件路径;
第二个参数表示加载图像类型,最常见的类型有如下几种:



使用如下代码从Mat对象中得到图像的宽、高、维度、通道数、深度、类型信息

int width = src.cols();
int height = src.rows();
int dims = src.dims();
int channels = src.channels();
int depth = src.depth();
int type = src.type();

其中要特别关注通道数、图像深度与图像类型、OpenCV加载的Mat类型图像对象

当调用imread函数时,
如果只使用文件路径参数读入加载一张图像,则默认值三通道CV_8UC3图像深度CV_8U

其中:

在如上的七行类型表中,每个类型都可以做类似的解读;
也可以看出CV_8U就是图像深度,所以图像类型与深度之间是有直接关系的。


1.2 Mat创建与初始化
如图是Mat在内存中的结构:
Mat m1 = new Mat();
m1.create(new Size(3, 3), CvType.CV_8UC3);
Mat m2 = new Mat();
m2.create(3, 3, CvType.CV_8UC3);

上述代码创建两个Mat对象——m1m2,它们的大小都是3×3、类型都是三通道8位的无符号字符型。

2)通过ones、eye、zeros方法初始化创建:

Mat m3 = Mat.eye(3, 3,CvType.CV_8UC3);
Mat m4 = Mat.eye(new Size(3, 3),CvType.CV_8UC3);
Mat m5 = Mat.zeros(new Size(3, 3), CvType.CV_8UC3);
Mat m6 = Mat.ones(new Size(3, 3), CvType.CV_8UC3);

上述代码创建了m3、m4、m5、m6四个Mat对象,基于这种初始化方式来得到Mat对象是OpenCV借鉴了Matlabeye、zeros、ones三个函数实现的。

3)先定义Mat,然后通过setTo方法实现初始化:

Mat m7 = new Mat(3, 3, CvType.CV_8UC3);
m7.setTo(new Scalar(255, 255, 255));

此方法与第一种方法有点类似,区别在于第一种方法通过create初始化时没有指定颜色值
在OpenCV中,颜色向量通常用Scalar表示,这里Scalar(255,255,255)表示白色

4)通过Mat的copyTo()clone()实现对象的创建,
Mat中的克隆与拷贝方法会复制一份完全相同的数据以创建一个新的Mat对象,

克隆相关代码如下:

Mat m8 = new Mat(500, 500, CvType.CV_8UC3);
m8.setTo(new Scalar(127, 127, 127));
Mat cmat = image.clone();

拷贝的相关代码如下:

at m8 = new Mat(500, 500, CvType.CV_8UC3);
m8.setTo(new Scalar(127, 127, 127));
Mat result = new Mat();
m8.copyTo(result)

1.3 Mat对象保存

创建好的Mat对象经过一系列的操作之后,就可以通过OpenCV4Android的imwrite函数直接将对象保存为图像:

// 创建Mat对象并保存
Mat image = new Mat(500, 500, CvType.CV_8UC3);
image.setTo(new Scalar(127, 127, 127));
ImageSelectUtils.saveImage(image);

其中:
500表示图像的宽度与高度,vType.CV_8UC3声明图像是RGB彩色三通道图像、每个通道都是8位;
第二行代码是指定图像的每个像素点、每个通道的灰度值为127
第三行代码是使用imwrite将图像保存到手机中的指定目录下;

saveImage方法内容如下:

File fileDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), "mybook");
if(!fileDir.exists()) {
  fileDir.mkdirs();
}
String name = String.valueOf(System.currentTimeMillis()) + "_book.jpg";
File tempFile = new File(fileDir.getAbsoluteFile()+File.separator, name);
Imgcodecs.imwrite(tempFile.getAbsolutePath(), image);

上面的前几行代码是创建目录与文件路径
最后一行代码通过imwrite来实现文件的保存
保存图像的格式取决于文件路径为图像指定的扩展名类型(如代码中的.jpg)。


2. Android中的Bitmap对象

其实Android系统中有一个与Mat对象相似的对象Bitmap
通过它可以获取图像的常见属性、像素数据,修改图像的像素数据,呈现出不同的图像显示效果,保存图像,等等。


2.1 图像文件与资源加载

在Android系统中,
可以把给定图像的文件路径或者图像资源ID作为参数
通过调用API来实现文件加载,使目标图片成为一个Bitmap实例对象。

最常见的加载资源图像的方法:

Bitmap bm = BitmapFactory.decodeResource(this.getResources(), R.drawable.lena);

加载图像文件时,为了避免OOM问题,

private void displaySelectedImage() {
        if(fileUri == null) return;
        ImageView imageView = (ImageView)this.findViewById(R.id.sample_img);

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;

        BitmapFactory.decodeFile(fileUri.getPath(), options);
        int w = options.outWidth;
        int h = options.outHeight;
        int inSample = 1;
        if(w > 1000 || h > 1000) {
            while(Math.max(w/inSample, h/inSample) > 1000) {
                inSample *=2;
            }
        }

        options.inJustDecodeBounds = false;
        options.inSampleSize = inSample;
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;

        Bitmap bm = BitmapFactory.decodeFile(fileUri.getPath(), options);
        imageView.setImageBitmap(bm);
    }

2.2 读写像素

对Bitmap对象,首先可以通过相关的API查询到图像的长、宽、配置信息;
在Bitmap中,像素数据是最占内存的部分;
根据长、宽与配置信息可以计算出图像像素的大小为多少;

读取像素时,

Bitmap获取图像宽、高与配置信息的接口代码如下:

public final int getWidth()
public final int getHeight()
public final Config getConfig()

其中,Config是Java中的枚举类型
当前Android支持的Bitmap像素存储类型具体如下:

Bitmap.Config.ALPHA_8;
Bitmap.Config.ARGB_4444;
Bitmap.Config.RGB_565;
Bitmap.Config.ARGB_8888;

默认情况下,Bitmap是在RGB色彩空间。
其中:

其中ALPHA_8表示该图像只有透明通道而没有颜色通道,是一张透明通道图像,
这种图像通常会被用作mask图像。

上述代码参数具体分析如下:

·ARGB_4444:表示每个通道占四位,总计两个字节,表示一个像素的图像。
·ARGB_8888:表示每个通道占八位,总计四个字节,表示一个像素的图像,这个是最常见的。
·ARGB_565:表示每个通道分别占5位、6位、5位,总计两个字节,表示一个像素的图像。



在Bitmap中循环读取每个像素每个通道修改的代码如下:

public void getBitmapInfo() {
        Bitmap bm = BitmapFactory.decodeResource(this.getResources(), R.drawable.lena);
        int width = bm.getWidth();
        int height = bm.getHeight();
        Bitmap.Config config = bm.getConfig();

        int a=0, r=0, g=0, b=0;
        for(int row=0; row<height; row++) {
            for(int col=0; col<width; col++) {
                // 读取像素
                int pixel = bm.getPixel(col, row);
                a = Color.alpha(pixel);
                r = Color.red(pixel);
                g = Color.green(pixel);
                b = Color.blue(pixel);
                // 修改像素
                r = 255 - r;
                g = 255 - g;
                b = 255 - b;
                // 保存到Bitmap中
                bm.setPixel(col, row, Color.argb(a, r, g, b));
            }
        }

这种方式每次只读取一个像素点的颜色值,然后修改设置的方法,
会造成对Bitmap对象的频繁访问,效率低下



DVM内存不紧张的时候,应该选择:

这种方法速度很快,也更为常见
实现代码如下:

private void scanPixelsDemo() {
        Bitmap bm = BitmapFactory.decodeResource(this.getResources(), R.drawable.lena).copy(Bitmap.Config.ARGB_8888, true);
        int width = bm.getWidth();
        int height = bm.getHeight();
        Bitmap.Config config = bm.getConfig();

        int[] pixels = new int[width*height];
        bm.getPixels(pixels, 0, width, 0, 0, width, height);
        int a=0, r=0, g=0, b=0;
        int index = 0;
        for(int row=0; row<height; row++) {
            for(int col=0; col<width; col++) {
                // 读取像素
                index = width*row + col;
                a=(pixels[index]>>24)&0xff;
                r=(pixels[index]>>16)&0xff;
                g=(pixels[index]>>8)&0xff;
                b=pixels[index]&0xff;
                // 修改像素
                r = 255 - r;
                g = 255 - g;
                b = 255 - b;
                // 保存到Bitmap中
                pixels[index] = (a << 24) | (r << 16) | (g << 8) | b;
            }
        }

        bm.setPixels(pixels, 0, width, 0, 0, width, height);

        ImageView iv = (ImageView)this.findViewById(R.id.matInfo_imageView);
        iv.setImageBitmap(bm);
        bm.recycle();

    }

关于上述代码读取保存部分代码的补充解析:

  • 初定义时,itmap.Config.ARGB_8888,也即每个像素点有8 * 4 = 32个bit
    其中ARGB四个通道各用8个bit表示,依序排列;

  • 0xff,刚好为8bit,因为每位16进制数等于4位bit

  • 类似a=(pixels[index]>>24)&0xff;右移操作,
    意义在于截取8 * 4 = 32个bit中各自的8bit有效位
    接着同0xff相与,1位为1保留,0位为0保留;

  • 各个通道各自的8bit有效位计算(修改)完毕之后,
    再将各通道相与结果左移对应的位数,
    最后统一相或,则得到修改后的一个像素点的32个bit


2.3 释放内存

3. 基础形状绘制与填充


3.0 首先是OpenCV是在Mat图像上绘制与填充

OpenCV2.xAndroid SDK图形绘制是在Core模块中,
到了OpenCV3.x中,图形绘制就已经移到Imgproc这个模块中了。


3.1 在Mat上绘制基本几何形状与文本

Mat上绘制的基本几何形状包括矩形、直线、圆、椭圆,还有文本文字
下面是绘制这几个形状相关的API说明:

  1. line(Mat img,Point pt1,Point pt2,Scalar color,int thickness,int lineType,int shift)表示绘制直线

  1. rectangle(Mat img,Point pt1,Point pt2,Scalar color,int thickness,int lineType,int shift)
    绘制矩形跟绘制直线的方法参数极其类似,主要是两个坐标点参数含义不一样:
    pt1:表示矩形左上角点的屏幕坐标;
    pt2:表示矩形右下角点的屏幕坐标;

  1. circle(Mat img,Point center,int radius,Scalar color,int thickness,int lineType,int shift)
    img:同上。
    center:表示圆的中心点位置的屏幕坐标,单位是像素
    radius:表示圆的半径大小,单位是像素
    color:表示圆的颜色

  1. ellipse(Mat img,Point center,Size axes,double angle,double startAngle,double endAngle,Scalar color,int thickness,int lineType,int shift)

  1. putText(Mat img,String text,Point org,int fontFace,double fontScale,Scalar color,int thickness)

另外补充:

·LINE_4:表示绘制线段的时候使用四邻域填充方法。
·LINE_8:表示绘制线段的时候使用八邻域填充方法。
·LINE_AA:表示绘制线段的时候使用反锯齿填充方法。

下面创建一个500×500px大小的Mat对象,类型是CV_8UC3
然后在上面的API实际操作练习一下:

private void basicDrawOnMat() {
        //创建Mat对象
        Mat src = Mat.zeros(500, 500, CvType.CV_8UC3);

        //开始绘制
        Imgproc.ellipse(src, new Point(250, 250), new Size(100, 50),
                360, 0, 360, new Scalar(0, 0, 255), 2, 8, 0);

        Imgproc.putText(src, "Basic Drawing Demo", new Point(20, 20),
                Core.FONT_HERSHEY_PLAIN, 1.0, new Scalar(0, 255, 0), 1);
        Rect rect = new Rect();
        rect.x = 50;
        rect.y = 50;
        rect.width = 100;
        rect.height = 100;
        Imgproc.rectangle(src, rect.tl(), rect.br(), //矩形
                new Scalar(255, 0, 0), 2, 8, 0);
        Imgproc.circle(src, new Point(400, 400), 50,
                new Scalar(0, 255, 0), 2, 8, 0);
        Imgproc.line(src, new Point(10, 10), new Point(490, 490),
                new Scalar(0, 255, 0), 2, 8, 0);
        Imgproc.line(src, new Point(10, 490), new Point(490, 10),
                new Scalar(255, 0, 0), 2, 8, 0);
        //绘制完毕

        //创建一个同Mat一样大小Bitmap对象
        Bitmap bm = Bitmap.createBitmap(src.cols(), src.rows(), Bitmap.Config.ARGB_8888);

        Mat dst = new Mat();//准备一个Mat缓冲变量
        Imgproc.cvtColor(src, dst, Imgproc.COLOR_BGR2RGBA);//把三通道的Mat对象(即src)转化成四通道的Mat对象赋到dst上
        Utils.matToBitmap(dst, bm);//dst转换成Bitmap对象

        ImageView iv = (ImageView)this.findViewById(R.id.matInfo_imageView);
        iv.setImageBitmap(bm);
    }

运行效果如下:



3.2 在Canvas上绘制基本几何形状与文本

Android中在Bitmap上绘制几何形状与文本对象,要借助Canvas相关API实现;

像这样首先创建Paint实例,然后设置颜色与风格:

Paint p = new Paint();
p.setColor(Color.GREEN);
p.setStyle(Paint.Style.STROKE)

常见的风格还包括如下几种:

·Paint.Style.STROKE:描边。
·Paint.Style.FILL:填充。
·Paint.Style.FILL_AND_STROKE:填充与描边。

设置好Paint之后就可以开始绘制了:

// 绘制直线
canvas.drawLine(10, 10, 490, 490, p);
canvas.drawLine(10, 490, 490, 10, p);
// 绘制矩形
android.graphics.Rect rect = new android.graphics.Rect();
rect.set(50, 50, 150, 150); // 矩形左上角点,与右下角点坐标
canvas.drawRect(rect, p);
// 绘制圆
p.setColor(Color.GREEN);
canvas.drawCircle(400, 400, 50, p);
// 绘制文本
p.setColor(Color.RED);
canvas.drawText("Basic Drawing on Canvas", 40, 40, p);

绘制方法全文(canvas绘制的内容都会映射在绑定的Bitmap上):

private void basicDrawOnCanvas() {
        // 创建Bitmap对象
        Bitmap bm = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);

        // 创建画布与画笔风格
        Canvas canvas = new Canvas(bm);

        Paint p = new Paint();
        p.setColor(Color.BLUE);
        p.setStyle(Paint.Style.FILL_AND_STROKE);

        // 绘制直线
        canvas.drawLine(10, 10, 490, 490, p);
        canvas.drawLine(10, 490, 490, 10, p);

        // 绘制矩形
        android.graphics.Rect rect = new android.graphics.Rect();
        rect.set(50, 50, 150, 150); // 矩形左上角点,与右下角点坐标
        canvas.drawRect(rect, p);

        // 绘制圆
        p.setColor(Color.GREEN);
        canvas.drawCircle(400, 400, 50, p);

        // 绘制文本
        p.setColor(Color.RED);
        canvas.drawText("Basic Drawing on Canvas", 40, 40, p);

        // 显示结果
        ImageView iv = (ImageView)this.findViewById(R.id.matInfo_imageView);
        iv.setImageBitmap(bm);
        bm.recycle();//!!!!!!!!!!!!!!!!!!!!!!!!!!
    }

综上,
Android中提供的基于Canvas的API完整地实现了图形绘制功能,
当用OpenCV在Android中做开发时,若需绘制复杂的几何图形或中文文字
优先选择本地Canvas API来完成。



4. Mat与Bitmap的使用与转换


4.1 Mat与Bitmap相互转换

第一种情况:

将这样的Mat对象转换为Bitmap对象的情况;

可以参考以下实例代码处理这种情况:

    private void mat2BitmapDemo(int index) {
        Mat src = Imgcodecs.imread(fileUri.getPath());//通过imread读取返回的Mat对象
        int width = src.cols();
        int height = src.rows();

        Bitmap bm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);将```图像Bitmap```加载为```ARGB_8888```方式,

        Mat dst = new Mat();//准备一个Mat缓冲变量
        Imgproc.cvtColor(src, dst, Imgproc.COLOR_BGR2RGBA);//把三通道的Mat对象(即src)转化成四通道的Mat对象赋到dst上
        Utils.matToBitmap(dst, bm);//dst转换成Bitmap对象
        dst.release();//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

        ImageView iv = (ImageView)this.findViewById(R.id.matInfo_imageView);
        iv.setImageBitmap(bm);
    }

其中:

  • Utils.matToBitmap()来自OpenCV4Android SDKUtil包,
    包中还有另外一个与它相对应的方法Utils.bitmapToMat()
    通过它们就可以实现Bitmap与Mat的相互转换

  • Bitmap的类型ARGB_8888
    OpenCV加载图像默认的类型BGR
    所以需要通过cvtColor()转换为RGBA四通道图像之后,
    再调用mat与Bitmap的相互转换方法(matToBitmap())。

否则的出现通道顺序不正确,会导致图像显示颜色异常

第二种情况更为常见
通常地,

可以参考以下实例代码处理这种情况:

    private void bitmap2MatDemo() {
        Bitmap bm = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);//将```图像Bitmap```加载为```ARGB_8888```方式,

        Mat m = new Mat();
        Utils.bitmapToMat(bm, m);

        Imgproc.circle(m, new Point(m.cols()/2, m.rows()/2), 50,
                new Scalar(255, 0, 0, 255), 2, 8, 0);

        Utils.matToBitmap(m, bm);

        ImageView iv = (ImageView)this.findViewById(R.id.matInfo_imageView);
        iv.setImageBitmap(bm);
    }

4.2 内存与显示

示例代码:

Mat dst = new Mat();
Imgproc.cvtColor(src, dst, Imgproc.COLOR_BGR2RGBA);
Utils.matToBitmap(dst, bm);
dst.release();//及时释放临时Mat对象内存空间

4.3 通道数、通道顺序与透明通道问题
(1)默认通道数与顺序

使用OpenCV4Android SDK创建图像的时候最好将其指定为三通道默认的BGR顺序
这也是OpenCV加载图像文件Mat对象的时候使用的默认通道数与通道顺序

(2)透明通道
(3)灰度与二值图像
参考资料
上一篇 下一篇

猜你喜欢

热点阅读