¿Convertir Android ImageReader Image a una matriz de bytes YCbCr_420_SP(NV21) usando un script de render?
image-processing camera (1)
Actualmente estoy trabajando con Javacv que hace uso de la función de cámara public void onPreviewFrame(byte[] data, Camera camera)
.
Dado que la cámara está obsoleta, he estado buscando en camera2 y MediaProjection . Ambas bibliotecas hacen uso de la clase ImageReader .
Actualmente, he creado una instancia de este ImageReader
con el siguiente código:
ImageReader.newInstance(DISPLAY_WIDTH, DISPLAY_HEIGHT, PixelFormat.RGBA_8888, 2);
Y adjunte un OnImageAvailableListener
como este:
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
= new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
mBackgroundHandler.post(new processImage(reader.acquireNextImage()));
}
He intentado usar el formato RGBA_8888
con Javacv según este hilo: https://github.com/bytedeco/javacv/issues/298 pero eso no funciona para mí.
Así que en lugar de eso, estaba pensando en usar el script Render para convertir estas Image a NV21(YUV_420_SP)
(que es la salida predeterminada de la cámara en la función onPreviewFrame
) ya que funcionó para mí con la biblioteca de camera
.
También he leído publicaciones como esta y este sitio web para hacer la conversión, pero no me funcionaron y me temo que serán demasiado lentas. Además, mi conocimiento de C es muy limitado. Básicamente, parece que quiero la operación inversa de https://developer.android.com/reference/android/renderscript/ScriptIntrinsicYuvToRGB.html
Entonces, ¿cómo se puede pasar de una Image
a una matriz de bytes que coincida con la salida de la función onPreviewFrame
, es decir, el NV21(YUV_420_SP)
? Preferiblemente usar Renderscript ya que es más rápido.
Edición 1:
He intentado usar ImageFormat.YUV_420_888
pero fue en vano. Seguí recibiendo errores como The producer output buffer format 0x1 doesn''t match the ImageReader''s configured buffer format
. PixelFormat.RGBA_8888
nuevo a PixelFormat.RGBA_8888
y descubrí que solo hay un plano en el ImageObject
. El búfer de bytes de este plano es de tamaño ancho * altura * 4 (un byte para R, G, B, A respectivamente). Así que traté de convertir esto al formato NV21
.
He modificado el código de esta respuesta para producir la siguiente función:
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++];
// 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++;
}
}
}
e invocarlo utilizando:
int mWidth = mImage.getWidth();
int mHeight = mImage.getHeight();
byte[] rgbaBytes = new byte[mWidth * mHeight * 4];
mImage.getPlanes()[0].getBuffer().get(rgbaBytes);
mImage.close();
byte[] yuv = new byte[mWidth * mHeight * 3 / 2];
RGBtoNV21(yuv, rgbaBytes, mWidth, mHeight);
Aquí mImage
es un objeto Image
producido por mi ImageReader
.
Sin embargo, esto produce un resultado similar a esta imagen:
que está claramente mal formado Parece que mi conversión está desactivada pero no puedo entender qué es exactamente.
@TargetApi(19)
public static byte[] yuvImageToByteArray(Image image) {
assert(image.getFormat() == ImageFormat.YUV_420_888);
int width = image.getWidth();
int height = image.getHeight();
Image.Plane[] planes = image.getPlanes();
byte[] result = new byte[width * height * 3 / 2];
int stride = planes[0].getRowStride();
assert (1 == planes[0].getPixelStride());
if (stride == width) {
planes[0].getBuffer().get(result, 0, width*height);
}
else {
for (int row = 0; row < height; row++) {
planes[0].getBuffer().position(row*stride);
planes[0].getBuffer().get(result, row*width, width);
}
}
stride = planes[1].getRowStride();
assert (stride == planes[2].getRowStride());
int pixelStride = planes[1].getPixelStride();
assert (pixelStride == planes[2].getPixelStride());
byte[] rowBytesCb = new byte[stride];
byte[] rowBytesCr = new byte[stride];
for (int row = 0; row < height/2; row++) {
int rowOffset = width*height + width/2 * row;
planes[1].getBuffer().position(row*stride);
planes[1].getBuffer().get(rowBytesCb);
planes[2].getBuffer().position(row*stride);
planes[2].getBuffer().get(rowBytesCr);
for (int col = 0; col < width/2; col++) {
result[rowOffset + col*2] = rowBytesCr[col*pixelStride];
result[rowOffset + col*2 + 1] = rowBytesCb[col*pixelStride];
}
}
return result;
}
He publicado otra función con requisitos similares. Esa nueva implementación intenta aprovechar el hecho de que, con bastante frecuencia, YUV_420_888 solo es NV21 disfrazado.