Android ImageReader получит формат NV21?
У меня нет фона в изображениях или графике, поэтому, пожалуйста, потерпите меня:)
Я использую JavaCV в одном из моих проектов. В примерах Frame
построен, который имеет буфер определенного размера.
При использовании public void onPreviewFrame(byte[] data, Camera camera)
функция в Android, копируя это data
байтовый массив не проблема, если вы объявляете Frame
как new Frame(frameWidth, frameHeight, Frame.DEPTH_UBYTE, 2);
где frameWidth
а также frameHeight
объявлены как
Camera.Size previewSize = cameraParam.getPreviewSize();
int frameWidth = previewSize.width;
int frameHeight = previewSize.height;
Недавно в Android был добавлен метод захвата экрана. Естественно, я хотел получить эти изображения, а также скрыть их Frame
s. Я изменил пример кода из Google, чтобы использовать ImageReader.
это ImageReader
построен как ImageReader.newInstance(DISPLAY_WIDTH, DISPLAY_HEIGHT, PixelFormat.RGBA_8888, 2);
, Поэтому в настоящее время он использует пиксельный формат RGBA_8888. Я использую следующий код для копирования байтов в Frame
, который создается как new Frame(DISPLAY_WIDTH, DISPLAY_HEIGHT, Frame.DEPTH_UBYTE, 2);
:
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
mImage.close();
((ByteBuffer) frame.image[0].position(0)).put(bytes);
Но это дает мне java.nio.BufferOverflowException
, Я напечатал размеры обоих буферов, а размер буфера Frame равен 691200, тогда как bytes
массив выше имеет размер 1413056
, Выяснить, как этот последний номер построен не удалось, потому что я столкнулся с этим собственным вызовом. Так ясно, что это не сработает.
После недолгих поисков я обнаружил, что формат изображения NV21 является "форматом по умолчанию для изображений предварительного просмотра с камеры, если не установлено иное с помощью setPreviewFormat(int)", но класс ImageReader не поддерживает формат NV21 (см. Параметр формата). Так что это тяжелая удача. В документации также говорится, что "Для API android.hardware.camera2 формат YUV_420_888 рекомендуется вместо вывода YUV".
Поэтому я попытался создать ImageReader, как это ImageReader.newInstance(DISPLAY_WIDTH, DISPLAY_HEIGHT, ImageFormat.YUV_420_888, 2);
, но это дает мне java.lang.UnsupportedOperationException: The producer output buffer format 0x1 doesn't match the ImageReader's configured buffer format 0x23.
так что это тоже не сработает.
В крайнем случае я попытался самостоятельно конвертировать RGBA_8888 в YUV, используя, например, этот пост, но я не понимаю, как получить int[] rgba
согласно ответу.
Итак, TL;DR, как я могу получить данные изображения NV21, как вы получаете в Android public void onPreviewFrame(byte[] data, Camera camera)
функция камеры, чтобы создать экземпляр моего Frame
и работать с ним с помощью Android ImageReader (и Media Projection)?
Изменить (25-10-2016)
Я создал следующее преобразование, работающее для перехода из формата RGBA в NV21:
private class updateImage implements Runnable {
private final Image mImage;
public updateImage(Image image) {
mImage = image;
}
@Override
public void run() {
int mWidth = mImage.getWidth();
int mHeight = mImage.getHeight();
// Four bytes per pixel: width * height * 4.
byte[] rgbaBytes = new byte[mWidth * mHeight * 4];
// put the data into the rgbaBytes array.
mImage.getPlanes()[0].getBuffer().get(rgbaBytes);
mImage.close(); // Access to the image is no longer needed, release it.
// Create a yuv byte array: width * height * 1.5 ().
byte[] yuv = new byte[mWidth * mHeight * 3 / 2];
RGBtoNV21(yuv, rgbaBytes, mWidth, mHeight);
((ByteBuffer) yuvImage.image[0].position(0)).put(yuv);
}
void RGBtoNV21(byte[] yuv420sp, byte[] argb, int width, int height) {
final int frameSize = width * height;
int yIndex = 0;
int uvIndex = frameSize;
int A, R, G, B, Y, U, V;
int index = 0;
int rgbIndex = 0;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
R = argb[rgbIndex++];
G = argb[rgbIndex++];
B = argb[rgbIndex++];
A = argb[rgbIndex++]; // Ignored right now.
// RGB to YUV conversion according to
// https://en.wikipedia.org/wiki/YUV#Y.E2.80.B2UV444_to_RGB888_conversion
Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;
// NV21 has a plane of Y and interleaved planes of VU each sampled by a factor
// of 2 meaning for every 4 Y pixels there are 1 V and 1 U.
// Note the sampling is every other pixel AND every other scanline.
yuv420sp[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
if (i % 2 == 0 && index % 2 == 0) {
yuv420sp[uvIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
yuv420sp[uvIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
}
index++;
}
}
}
}
yuvImage
объект инициализируется как yuvImage = new Frame(DISPLAY_WIDTH, DISPLAY_HEIGHT, Frame.DEPTH_UBYTE, 2);
, DISPLAY_WIDTH
а также DISPLAY_HEIGHT
просто два целых числа, указывающие размер дисплея. Это код, где обработчик фона обрабатывает onImageReady:
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
= new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
mBackgroundHandler.post(new updateImage(reader.acquireNextImage()));
}
};
...
mImageReader = ImageReader.newInstance(DISPLAY_WIDTH, DISPLAY_HEIGHT, PixelFormat.RGBA_8888, 2);
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler);
Методы работают, и я, по крайней мере, не получаю никаких ошибок, но выходное изображение искажено. Что не так в моем обращении? Пример изображения, которое создается:
Изменить (15-11-2016)
Я изменил RGBtoNV21
функция должна быть следующей:
void RGBtoNV21(byte[] yuv420sp, int width, int height) {
try {
final int frameSize = width * height;
int yIndex = 0;
int uvIndex = frameSize;
int pixelStride = mImage.getPlanes()[0].getPixelStride();
int rowStride = mImage.getPlanes()[0].getRowStride();
int rowPadding = rowStride - pixelStride * width;
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
Bitmap bitmap = Bitmap.createBitmap(getResources().getDisplayMetrics(), width, height, Bitmap.Config.ARGB_8888);
int A, R, G, B, Y, U, V;
int offset = 0;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
// Useful link: https://stackru.com/questions/26673127/android-imagereader-acquirelatestimage-returns-invalid-jpg
R = (buffer.get(offset) & 0xff) << 16; // R
G = (buffer.get(offset + 1) & 0xff) << 8; // G
B = (buffer.get(offset + 2) & 0xff); // B
A = (buffer.get(offset + 3) & 0xff) << 24; // A
offset += pixelStride;
int pixel = 0;
pixel |= R; // R
pixel |= G; // G
pixel |= B; // B
pixel |= A; // A
bitmap.setPixel(j, i, pixel);
// RGB to YUV conversion according to
// https://en.wikipedia.org/wiki/YUV#Y.E2.80.B2UV444_to_RGB888_conversion
// Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
// U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
// V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;
Y = (int) Math.round(R * .299000 + G * .587000 + B * .114000);
U = (int) Math.round(R * -.168736 + G * -.331264 + B * .500000 + 128);
V = (int) Math.round(R * .500000 + G * -.418688 + B * -.081312 + 128);
// NV21 has a plane of Y and interleaved planes of VU each sampled by a factor
// of 2 meaning for every 4 Y pixels there are 1 V and 1 U.
// Note the sampling is every other pixel AND every other scanline.
yuv420sp[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
if (i % 2 == 0 && j % 2 == 0) {
yuv420sp[uvIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
yuv420sp[uvIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
}
}
offset += rowPadding;
}
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath(), "/Awesomebitmap.png");
FileOutputStream fos = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
} catch (Exception e) {
Timber.e(e, "Converting image to NV21 went wrong.");
}
}
Теперь изображение больше не искажено, но цветность выключена.
Правая сторона - это растровое изображение, которое создается в этом цикле, левая сторона - это NV21, сохраненная в изображение. Таким образом, пиксели RGB обрабатываются правильно. Очевидно, что цветность выключена, но преобразование RGB в YUV должно быть таким же, как показано в википедии. Что здесь может быть не так?
1 ответ
Вообще говоря, цель ImageReader - предоставить вам необработанный доступ к пикселям, отправляемым на поверхность, с минимальными издержками, поэтому пытаться заставить его выполнять преобразование цветов не имеет смысла.
Для камеры вы можете выбрать один из двух форматов вывода (NV21 или YV12), поэтому выберите YV12. Это ваши необработанные данные YUV. Для захвата экрана вывод всегда будет RGB, поэтому вам нужно выбрать RGBA_8888
(формат 0x1) для вашего ImageReader, а не YUV_420_888
(формат 0x23). Если вам для этого нужен YUV, вам нужно будет сделать конвертацию самостоятельно. ImageReader дает вам серию Plane
объекты, а не byte[]
, так что вам нужно будет адаптироваться к этому.