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

4.793 (119)

В предыдущих 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)

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