Android Camera2入门系列2 - ImageReader获得预览数据

Android Camera2入门

Android Camera2入门系列1 - Camera2在textureView预览
Android Camera2入门系列2 - ImageReader获得预览数据
Android Camera2入门系列3 - Image中获得YUV数据及YUV格式理解
Android Camera2入门系列4 - libyuv的编译和使用

上篇文章Android Camera系列1 - Camera2在textureView预览理了理如何实现最简单的TextureView预览Camera2。
饭要一口一口的吃,胖子要一斤一斤的长。
开门见山,我们需要用到ImageReader这个类去得到一个Image。

Image:

Image类允许应用通过一个或多个ByteBuffers直接访问Image的像素数据, ByteBuffer包含在Image.Plane类中,同时包含了这些像素数据的配置信息。因为是作为提供raw数据使用的,Image不像Bitmap类可以直接填充到UI上使用。

因为Image的生产消费是跟硬件直接挂钩的,所以为了效率起见,Image如果不被使用了应该尽快的被销毁掉。比如说,当我们使用ImageReader从不用的媒体来源获取到Image的时候,如果Image的数量到达了maxImages,不关闭之前老的Image,新的Image就不会继续生产。

  • close : 关掉当前帧for reuse。调用此方法后再调用其他Image的方法都会报IllegalStateException
  • getFormat : 获取当前Image的格式,format决定了Image需要提供的ByteBuffers数量和每个ByteBuffer的像素数量。这里还涉及到Image.Plane.
    Image.Plane : plane这里翻译为一个平面。通过作为一个数组返回,数组的数量由Image的格式决定,比如ImageFormat.JPEG返回的数组size就是1,ImageFormat.YUV_420_888返回的数字size就是3。一旦Image被关闭了,再去尝试获取plane的ByteBuffer将会失败。
Format Plane count Layout Details
JPEG 1 压缩过的数据,所以行数为0,解压缩需要使用BitmapFactory#decodeByteArray
YUV_420_888 3 一个明度通道+两个色彩CbCr通道,UV的宽高是Y的一半。

附一部分ImageFormat的描述。

Constants Descriptions
JPEG Encoded formats.
NV16 YCbCr format, used for video.
NV21 YCrCb format used for images, which uses the NV21 encoding format.
RGB_565 RGB format used for pictures encoded as RGB_565.
YUV_420_888 Multi-plane Android YUV format,This format is a generic YCbCr format, capable of describing any 4:2:0 chroma-subsampled planar or semiplanar buffer (but not fully interleaved), with 8 bits per color sample.
YUY2 YCbCr format used for images, which uses YUYV (YUY2) encoding format.
YV12 Android YUV format.
ImageReader:

image的data被存储在Image类里面,构造参数maxImages控制了最多缓存几帧,新的images通过ImageReader的surface发送给ImageReader,类似一个队列,需要通过acquireLatestImage()或者acquireNextImage()方法取出Image。如果ImageReader获取并销毁图像的速度小于数据源产生数据的速度,那么就会丢帧。

也就是说ImageReader只会给我们maxImages个Image。如果你acquire掉之前的Image,那么永远不会有新的Image回调过来,因为队列已经满了,只有从队列中移除掉头部的元素,才能给新的Image留出空间来。
用法Like Below:

...
          //构造一个ImageReader的实例,设置宽高,输出格式,缓存max数量
           mImageReader = ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(),
                            ImageFormat.JPEG, 2);
           mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mCameraHandler);
...
private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            Image image = reader.acquireNextImage();
            ...
            image.close();
        }
    };

部分重要API:

  • acquireLatestImage() - 从ImageReader队列中获取最新的一帧Image,并且将老的Image丢弃,如果没有新的可用的Image则返回null。
    此操作将会从ImageReader中获取所有可获取到的Images,并且关闭除了最新的Image之外的Image。此功能大多数情况下比acquireNextImage更推荐使用,更加适用于视频实时处理。
    需要注意的是maxImages应该至少为2,因为丢弃除了最新的之外的所有帧需要至少两帧。换句话说,(maxImages - currentAcquiredImages < 2)的情况下,丢帧将会不正常。
  • acquireNextImage() - 从ImageReader的队列中获取下一帧Image,如果没有新的则返回null。
    Android推荐我们使用acquireLatestImage来代替使用此方法,因为它会自动帮我们close掉旧的Image,并且能让效率比较差的情况下能获取到最新的Image。acquireNextImage更推荐在批处理或者后台程序中使用,不恰当的使用本方法将会导致得到的images出现不断增长的延迟。
  • close() - 释放所有跟此ImageReader关联的资源。调用此方法后,ImageReader不会再被使用,再调用它的方法或者调用被acquireLatestImageacquireNextImage返回的Image会抛出IllegalStateException,尝试读取之前Plane#getBuffer返回的ByteBuffers将会导致不可预测的行为。
  • newInstance(int width, int height, int format, int maxImages) - 创建新的reader以获取期望的size和format的Images。maxImages决定了ImageReader能同步返回的最大的Image的数量,申请越多的buffers会耗费越多的内存空间,使用合适的数量很重要。
    format :reader生产的Image的格式,必须是ImageFormatPixelFormat中的常量,并不是所有的formats都会被支持,比如ImageFormat.NV21就是不支持的,Android一般都会支持ImageFormat_420_888。那很多人可能会想,不支持你写这儿干嘛?当然这里只是说Camera不支持格式直出,并不是其他地方不认识这种格式,比如YuvImage就支持ImageFormat.NV21
    maxImages:前面讲过很多了,缓存的最大帧数,必须大于0。

其他的方法像getHeight,getWidth,getMaxImages,getImageFormat,字面意思,不再赘述。

下面我们先实现一个ImageReader得到ImageFormat.JPEG格式的数据并显示到view上,跟上一篇相比,改动在以下几个地方:

private void openCamera(int width, int height) {
...        //创建ImageFormat.JPEG格式的ImageReader并设置OnImageAvailableListener
          mImageReader = ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(),
                            ImageFormat.JPEG, 2);
          mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mCameraHandler);
...
}

private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice camera) {
            ...
                mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                mPreviewBuilder.addTarget(previewSurface);
//把ImageReader的surface添加给CaptureRequest.Builder,使预览surface和ImageReader同时收到数据回调。
                mPreviewBuilder.addTarget(mImageReader.getSurface());
                mCameraDevice.createCaptureSession(Arrays.asList(previewSurface, mImageReader.getSurface()), mStateCallBack, mCameraHandler);
            ...
        }
}

private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
//获取最新的一帧的Image
            Image image = reader.acquireLatestImage();
//因为是ImageFormat.JPEG格式,所以 image.getPlanes()返回的数组只有一个,也就是第0个。
            ByteBuffer byteBuffer = image.getPlanes()[0].getBuffer();
            byte[] bytes = new byte[byteBuffer.remaining()];
            byteBuffer.get(bytes);
//ImageFormat.JPEG格式直接转化为Bitmap格式。
            Bitmap temp = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
//因为摄像机数据默认是横的,所以需要旋转90度。
            Bitmap newBitmap = BitmapUtil.rotateBitmap(temp, 90);
//抛出去展示或存储。
            mOnGetBitmapInterface.getABitmap(newBitmap);
//一定需要close,否则不会收到新的Image回调。
            image.close();
        }
    };

本文地址在Camera2ProviderWithData.java,自行取阅。

关键代码都在上面了,如果只是拍照或截取预览,这样的调用就足够了,当然如果是做实时美颜特效,这样是远远不够的,因为返回JPEG格式需要进行encode,时间必然会长,这时候就不能用JPEG了,我们需要用到前面提到的ImageFormat.YUV_420_888。


YUV跟JPEG相比相机逻辑改动就是把ImageFormat.JPEG改为ImageFormat.YUV_420_888

mImageReader = ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(),
                            ImageFormat.YUV_420_888, 2);

如果我们仍然想用上面的方式预览,我们要做的就是如何把I420的数据转为Bitmap。

private ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            Image image = reader.acquireLatestImage();
            if (image == null) {
                return;
            }
            int width = image.getWidth(), height = image.getHeight();
            byte[] i420bytes = CameraUtil.getDataFromImage(image, COLOR_FormatI420);
//                BitmapUtil.dumpFile("mnt/sdcard/1.yuv", i420bytes);
            byte[] i420RorateBytes = BitmapUtil.rotateYUV420Degree90(i420bytes, width, height);
            byte[] nv21bytes = BitmapUtil.I420Tonv21(i420RorateBytes, height, width);
            Bitmap bitmap = BitmapUtil.getBitmapImageFromYUV(nv21bytes, height, width);
            if (mOnGetBitmapInterface != null) {
                mOnGetBitmapInterface.getABitmap(bitmap);
            }
            image.close();
        }
    };

这里推荐一个工具GLYUVPlay,(好像最新的mac系统用不了,现在我换成了YuvEye),可以查看得到的YUV是否正常,宽高和format一定要设置对,看显示是否正常就可以查看YUV是否获取正确了。

这里转化的时候还要涉及到一个类就是YuvImage

  • YuvImage : YuvImage包含YUV数据并且提供把YUV数据转化成Jpeg的方法。YUV数据应该是一个单纯的byte数组,而不是image返回的好几个planes。现在只支持ImageFormat.NV21ImageFormat.YUY2,用户需要给定上下左右的宽高。

所以我们只需要把YUV_420_888转化为ImageFormat.NV21就可以了,使用YuvImage就可以转化成Bitmap了。每个YUV格式的区别这篇说不开,另开一篇。

public static byte[] I420Tonv21(byte[] data, int width, int height) {
        byte[] ret = new byte[data.length];
        int total = width * height;

        ByteBuffer bufferY = ByteBuffer.wrap(ret, 0, total);
        ByteBuffer bufferV = ByteBuffer.wrap(ret, total, total / 4);
        ByteBuffer bufferU = ByteBuffer.wrap(ret, total + total / 4, total / 4);

        bufferY.put(data, 0, total);
        for (int i = 0; i < total / 4; i += 1) {
            bufferV.put(data[total + i]);
            bufferU.put(data[i + total + total / 4]);
        }

        return ret;
    }

这里提供一个java的转换方法,只是作为理清逻辑使用,工程中还是使用C++或者三方库提高效率。
YUV代码详见:Camera2ProviderPreviewWithYUV 欢迎star/follow

本篇其实没有说清楚如何从Image中得到YUV数据下一篇 Android Camera系列3 - Image中获得YUV数据及YUV格式理解分享。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,639评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,277评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,221评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,474评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,570评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,816评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,957评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,718评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,176评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,511评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,646评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,322评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,934评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,755评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,987评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,358评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,514评论 2 348

推荐阅读更多精彩内容

  • MediaCodec 解码视频快速取帧 开发背景 所以考虑在需要 1s 视频取 30 帧缩略图时,采取 Media...
    叉腰大魔王阅读 8,659评论 1 6
  • 一、Camera2简介 Camera2是Google在Android 5.0后推出的一个全新的相机API,Came...
    Rising_suns阅读 5,016评论 0 3
  • 打开杨绛的《我们仨》,映入眼帘的便是杨绛、钱钟书夫妇和女儿钱媛手写的名字,瞬间觉得这是一个充满爱又有趣的家...
    原序阅读 1,411评论 0 3
  • 。为了完成这样的任务,我建议写读后感有难度的同学可以按模块进行分解,然后再把各模块的内容串起来,从而降低写作难度。...
    竹青青阅读 895评论 0 3
  • 如果不是做了母亲我想我不能如此的去诠释爱的意义 《一》爱是无悔的付出 我和刘先生是自由恋爱,历经6年步入婚姻的殿堂...
    虚空中的兔子阅读 333评论 0 2