Разработка Android приложений для Lollipop используя Camera2 API Часть 3 (Получение каждого кадра)

4.792 (118)

В предыдущих 2 частях мы рассмотрели основные принципы работы с камерой: как получить доступ к ней и начать работу. На этот раз посмотрим, каким образом мы можем получить доступ к каждому кадру для просмотра и редактирования изображения.

Как известно, для получения информации с камеры необходимо создать выходной стрим (MediaRecorder, ImageReader ...), куда будут отправляться данные по изображению или видео.
В предыдущих частях мы использовали для выходного потока SurfaceTexture. Но для получения изображения нам необходимо использовать  ImageReader, и при такой задаче уже возникают определенные нюансы, которые мы сейчас рассмотрим.
ImageReader создается путем вызова метода  ImageReader.newInstance(int width, int height, int format, int maxImages)с указанием соответствующих параметров.  Для получения максимально возможной скорости передачи кадров нам необходимо использовать «сырой формат». То есть, jpeg нам не подойдет, поскольку он будет давать задержку на время конвертации кадра и таким образом у нас возникнет очень низкий fps. Для получения максимальной продуктивности следует использовать ImageFormat.YUV_420_888.
И так создаем экземпляр объекта: 

mImageReaderYUV = ImageReader.newInstance(1920,1080,ImageFormat.YUV_420_888,1);

Небольшой совет при создании: не используйте локальных ссылок для рекордера. Создайте в классе поле и используйте его, иначе, если Вы создадите его просто внутри метода, в скором времени сборщик мусора соберет его и ссылка разрушится. В таком случае система выдаст   

E/BufferQueueProducer﹕ [unnamed-1390-1] dequeueBuffer: BufferQueue has been abandoned

Далее нам необходимо указать его в качестве приемника данных. 

final CaptureRequest.Builder builder =
        mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

builder.addTarget(mImageReaderYUV.getSurface());

mCameraDevice.createCaptureSession(
        Arrays.asList(mImageReaderYUV.getSurface()),
….

Итак, с созданием разобрались. Теперь переходим к данным. Как известно, мы получаем изображения в слушателе, который установили в  setOnImageAvailableListene().
Внутри этого метода мы получаем объект класса Image. В этом случае используем YUV изображение. В android предусмотрен класс YuvImage для работы с форматом YUV
Однако тут нас ждет первый сюрприз. Если открыть конструктор класса и посмотреть, какой формат изображения он получает, то увидим:


if (format != ImageFormat.NV21 &&
        format != ImageFormat.YUY2) {
    throw new IllegalArgumentException(
            "only support ImageFormat.NV21 " +
            "and ImageFormat.YUY2 for now");
}

и тут возникает в голове «как же так?». В предыдущей версии api изображение с камеры отдавалось форматом NV21. А в camera2 он стал недоступен. 
Давайте посмотрим, какая же разница между этими двумя форматами. Может у нас получится преобразовать изображение из одного в другой. 
Взглянем на структуру каждого из форматов:
Структура YUV_420:
   

android yuv_420_888 image format

как видим, каналы идут последовательно друг за другом. 
Структура NV21:
 

android nv 21 image format

А в NV21 каналы UV идут уже чередуясь друг за другом. Таким образом, нам ничего не мешает подправить наши байты с данными.
Внутри слушателя, который получает изображения, получаем по каждому каналу данные 

final Image image = reader.acquireLatestImage();

if (image == null) return;

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteBuffer bufferY = image.getPlanes()[0].getBuffer();
byte[] data0 = new byte[bufferY.remaining()];
bufferY.get(data0);
ByteBuffer bufferU = image.getPlanes()[1].getBuffer();
byte[] data1 = new byte[bufferU.remaining()];
bufferU.get(data1);
ByteBuffer bufferV = image.getPlanes()[2].getBuffer();
byte[] data2 = new byte[bufferV.remaining()];
bufferV.get(data2);

Для наглядности, мы разделили на отдельные переменные, что бы было понятно, что да как.
И потом записываем все как нужно  

outputStream.write(data0);
for (int i = 0; i < data1.length; i++) {  
    outputStream.write(data1[i]);
    outputStream.write(data2[i]);
}

Теперь в нашем выходном потоке нужный формат. Конечно подход не особо эффективный и быстрый, но он всего лишь демонстрирует понимание процесса работы с данными формата изображения.
Вроде все правильно сделали и можем сохранять в файл. И тут нас ждал очередной сюрприз, на решение которого ушло очень много времени. Когда сохранился файл, у меня вышло изображение в зеленом цвете.

android green out image yuv_420_88 

Сначала подумал, что где-то ошибся в форматах, и начал перебирать все возможные варианты, проведя много времени в интернете в поисках. Оказалось, что  в андроиде 5.0 есть баг, связанный с этим форматом. Дело в том, что он не выдает всех данных по двум последним каналам. Удивившись этому, посмотрел, что хранится в наших массивах. И оказалось, что действительно больше половины данных (а точнее, почти все) идут просто 0.
Как было описано, это исправили в 5.1, но обновлять телефон для проверки пока не стали. Надеемся, что когда обновим, эта проблема исчезнет.  

Часть 1 Часть 2

См. также Разработка Android приложений для нестандартных задач.

Комментарий (0)

Войдите с помощью соцсетей:
или
введите свои данные: