使用 libyuv 缩放 YUV420 图像会产生奇怪的输出
Scaling YUV420 Image using libyuv produces weird output
可能与 question with major parts picked from here 重复。我已经尝试了那里提供的任何解决方案,它们对我不起作用。
背景
我正在通过 Android NDK 在 YUV_420_888 image format returned from ARCore's frame.acquireCameraImage()
method. Since I've set the camera configuration at 1920*1080 resolution, I need to scale it down to 224*224 to pass it to my tensorflow-lite implementation. I do that by using LibYuv 库中捕获图像。
实施
准备图像帧
//Figure out the source image dimensions
int y_size = srcWidth * srcHeight;
//Get dimensions of the desired output image
int out_size = destWidth * destHeight;
//Generate input frame
i420_input_frame.width = srcWidth;
i420_input_frame.height = srcHeight;
i420_input_frame.data = (uint8_t*) yuvArray;
i420_input_frame.y = i420_input_frame.data;
i420_input_frame.u = i420_input_frame.y + y_size;
i420_input_frame.v = i420_input_frame.u + (y_size / 4);
//Generate output frame
free(i420_output_frame.data);
i420_output_frame.width = destWidth;
i420_output_frame.height = destHeight;
i420_output_frame.data = new unsigned char[out_size * 3 / 2];
i420_output_frame.y = i420_output_frame.data;
i420_output_frame.u = i420_output_frame.y + out_size;
i420_output_frame.v = i420_output_frame.u + (out_size / 4);
我使用 Libyuv 的 I420Scale
方法缩放图像
libyuv::FilterMode mode = libyuv::FilterModeEnum::kFilterBox;
jint result = libyuv::I420Scale(i420_input_frame.y, i420_input_frame.width,
i420_input_frame.u, i420_input_frame.width / 2,
i420_input_frame.v, i420_input_frame.width / 2,
i420_input_frame.width, i420_input_frame.height,
i420_output_frame.y, i420_output_frame.width,
i420_output_frame.u, i420_output_frame.width / 2,
i420_output_frame.v, i420_output_frame.width / 2,
i420_output_frame.width, i420_output_frame.height,
mode);
和return到java
//Create a new byte array to return to the caller in Java
jbyteArray outputArray = env -> NewByteArray(out_size * 3 / 2);
env -> SetByteArrayRegion(outputArray, 0, out_size, (jbyte*) i420_output_frame.y);
env -> SetByteArrayRegion(outputArray, out_size, out_size / 4, (jbyte*) i420_output_frame.u);
env -> SetByteArrayRegion(outputArray, out_size + (out_size / 4), out_size / 4, (jbyte*) i420_output_frame.v);
实际图像:
它看起来像什么 post 缩放:
如果我从 i420_input_frame
创建图像而不缩放会是什么样子:
由于缩放很长时间会弄乱颜色,tensorflow 无法正确识别对象。 (它在他们的示例应用程序中正确识别)我做错了什么把颜色弄乱了?
要么是我做错了什么(我无法修复),要么是 LibYuv 在处理来自 Android.
的 YUV 图像时没有正确处理颜色
参考 Libyuv 库上发布的官方错误:https://bugs.chromium.org/p/libyuv/issues/detail?id=815&can=1&q=&sort=-id
他们建议我先使用一种方法 Android420ToI420()
,然后再应用我需要的任何转换。我最终先使用 Android420ToI420()
,然后缩放,然后转换为 RGB。最后,输出比上面发布的杯子图像稍微好一些,但仍然存在扭曲的颜色。我最终使用 OpenCV 缩小图像并将其转换为 RGBA 或 RGB 格式。
// The camera image received is in YUV YCbCr Format at preview dimensions
// so we will scale it down to 224x224 size using OpenCV
// Y plane (0) non-interleaved => stride == 1; U/V plane interleaved => stride == 2
// Refer : https://developer.android.com/reference/android/graphics/ImageFormat.html#YUV_420_888
val cameraPlaneY = cameraImage.planes[0].buffer
val cameraPlaneUV = cameraImage.planes[1].buffer
// Create a new Mat with OpenCV. One for each plane - Y and UV
val y_mat = Mat(cameraImage.height, cameraImage.width, CvType.CV_8UC1, cameraPlaneY)
val uv_mat = Mat(cameraImage.height / 2, cameraImage.width / 2, CvType.CV_8UC2, cameraPlaneUV)
var mat224 = Mat()
var cvFrameRGBA = Mat()
// Retrieve an RGBA frame from the produced YUV
Imgproc.cvtColorTwoPlane(y_mat, uv_mat, cvFrameRGBA, Imgproc.COLOR_YUV2BGRA_NV21)
//Then use this frame to retrieve all RGB channel data
//Iterate over all pixels and retrieve information of RGB channels
for(rows in 1 until cvFrameRGBA.rows())
for(cols in 1 until cvFrameRGBA.cols()) {
val imageData = cvFrameRGBA.get(rows, cols)
// Type of Mat is 24
// Channels is 4
// Depth is 0
rgbBytes.put(imageData[0].toByte())
rgbBytes.put(imageData[1].toByte())
rgbBytes.put(imageData[2].toByte())
}
颜色问题是因为您使用的是不同的 YUV 格式。相机框架使用的 YUV 格式是 YUV NV21。 此格式 (NV21) 是 Android 相机预览的标准图片格式。 YUV 4:2:0 平面图像,具有 8 位 Y 样本,后跟具有 8 位 2x2 子采样色度样本的交错 V/U 平面。
如果你的颜色是反色的,这意味着:
- 您正在使用 YUV NV12(平面 U 是 V,V 是 U)。
- 你的一个彩色平面在做一些奇怪的事情。
为了与 libyuv
正常工作,我建议您使用 transformI420
方法将相机输出转换为 YUV I420,并通过参数发送格式:
return libyuv::ConvertToI420(src, srcSize, //src data
dstY, dstWidth, //dst planes
dstU, dstWidth / 2,
dstV, dstWidth / 2,
cropLeft, cropTop, //crop start
srcWidth, srcHeight, //src dimensions
cropRight - cropLeft, cropBottom - cropTop, //dst dimensions
rotationMode,
libyuv::FOURCC_NV21); //libyuv::FOURCC_NV12
完成此转换后,您将能够使用所有 I420scale
、I420rotate
... 等正确使用 libyuv。您的缩放方法应如下所示:
JNIEXPORT jint JNICALL
Java_com_aa_project_images_yuv_myJNIcl_scaleI420(JNIEnv *env, jclass type,
jobject srcBufferY,
jobject srcBufferU,
jobject srcBufferV,
jint srcWidth, jint srcHeight,
jobject dstBufferY,
jobject dstBufferU,
jobject dstBufferV,
jint dstWidth, jint dstHeight,
jint filterMode) {
const uint8_t *srcY = static_cast<uint8_t *>(env->GetDirectBufferAddress(srcBufferY));
const uint8_t *srcU = static_cast<uint8_t *>(env->GetDirectBufferAddress(srcBufferU));
const uint8_t *srcV = static_cast<uint8_t *>(env->GetDirectBufferAddress(srcBufferV));
uint8_t *dstY = static_cast<uint8_t *>(env->GetDirectBufferAddress(dstBufferY));
uint8_t *dstU = static_cast<uint8_t *>(env->GetDirectBufferAddress(dstBufferU));
uint8_t *dstV = static_cast<uint8_t *>(env->GetDirectBufferAddress(dstBufferV));
return libyuv::I420Scale(srcY, srcWidth,
srcU, srcWidth / 2,
srcV, srcWidth / 2,
srcWidth, srcHeight,
dstY, dstWidth,
dstU, dstWidth / 2,
dstV, dstWidth / 2,
dstWidth, dstHeight,
static_cast<libyuv::FilterMode>(filterMode));
}
如果您想在完成所有过程后将此图像转换为 JPEG。您可以使用 I420toNV21
方法,然后使用 android 从 YUV 到 JPEG 的本机转换。您也可以使用 libJpegTurbo,这是针对这种情况的补充库。
可能与
背景
我正在通过 Android NDK 在 YUV_420_888 image format returned from ARCore's frame.acquireCameraImage()
method. Since I've set the camera configuration at 1920*1080 resolution, I need to scale it down to 224*224 to pass it to my tensorflow-lite implementation. I do that by using LibYuv 库中捕获图像。
实施
准备图像帧
//Figure out the source image dimensions
int y_size = srcWidth * srcHeight;
//Get dimensions of the desired output image
int out_size = destWidth * destHeight;
//Generate input frame
i420_input_frame.width = srcWidth;
i420_input_frame.height = srcHeight;
i420_input_frame.data = (uint8_t*) yuvArray;
i420_input_frame.y = i420_input_frame.data;
i420_input_frame.u = i420_input_frame.y + y_size;
i420_input_frame.v = i420_input_frame.u + (y_size / 4);
//Generate output frame
free(i420_output_frame.data);
i420_output_frame.width = destWidth;
i420_output_frame.height = destHeight;
i420_output_frame.data = new unsigned char[out_size * 3 / 2];
i420_output_frame.y = i420_output_frame.data;
i420_output_frame.u = i420_output_frame.y + out_size;
i420_output_frame.v = i420_output_frame.u + (out_size / 4);
我使用 Libyuv 的 I420Scale
方法缩放图像
libyuv::FilterMode mode = libyuv::FilterModeEnum::kFilterBox;
jint result = libyuv::I420Scale(i420_input_frame.y, i420_input_frame.width,
i420_input_frame.u, i420_input_frame.width / 2,
i420_input_frame.v, i420_input_frame.width / 2,
i420_input_frame.width, i420_input_frame.height,
i420_output_frame.y, i420_output_frame.width,
i420_output_frame.u, i420_output_frame.width / 2,
i420_output_frame.v, i420_output_frame.width / 2,
i420_output_frame.width, i420_output_frame.height,
mode);
和return到java
//Create a new byte array to return to the caller in Java
jbyteArray outputArray = env -> NewByteArray(out_size * 3 / 2);
env -> SetByteArrayRegion(outputArray, 0, out_size, (jbyte*) i420_output_frame.y);
env -> SetByteArrayRegion(outputArray, out_size, out_size / 4, (jbyte*) i420_output_frame.u);
env -> SetByteArrayRegion(outputArray, out_size + (out_size / 4), out_size / 4, (jbyte*) i420_output_frame.v);
实际图像:
它看起来像什么 post 缩放:
如果我从 i420_input_frame
创建图像而不缩放会是什么样子:
由于缩放很长时间会弄乱颜色,tensorflow 无法正确识别对象。 (它在他们的示例应用程序中正确识别)我做错了什么把颜色弄乱了?
要么是我做错了什么(我无法修复),要么是 LibYuv 在处理来自 Android.
的 YUV 图像时没有正确处理颜色参考 Libyuv 库上发布的官方错误:https://bugs.chromium.org/p/libyuv/issues/detail?id=815&can=1&q=&sort=-id
他们建议我先使用一种方法 Android420ToI420()
,然后再应用我需要的任何转换。我最终先使用 Android420ToI420()
,然后缩放,然后转换为 RGB。最后,输出比上面发布的杯子图像稍微好一些,但仍然存在扭曲的颜色。我最终使用 OpenCV 缩小图像并将其转换为 RGBA 或 RGB 格式。
// The camera image received is in YUV YCbCr Format at preview dimensions
// so we will scale it down to 224x224 size using OpenCV
// Y plane (0) non-interleaved => stride == 1; U/V plane interleaved => stride == 2
// Refer : https://developer.android.com/reference/android/graphics/ImageFormat.html#YUV_420_888
val cameraPlaneY = cameraImage.planes[0].buffer
val cameraPlaneUV = cameraImage.planes[1].buffer
// Create a new Mat with OpenCV. One for each plane - Y and UV
val y_mat = Mat(cameraImage.height, cameraImage.width, CvType.CV_8UC1, cameraPlaneY)
val uv_mat = Mat(cameraImage.height / 2, cameraImage.width / 2, CvType.CV_8UC2, cameraPlaneUV)
var mat224 = Mat()
var cvFrameRGBA = Mat()
// Retrieve an RGBA frame from the produced YUV
Imgproc.cvtColorTwoPlane(y_mat, uv_mat, cvFrameRGBA, Imgproc.COLOR_YUV2BGRA_NV21)
//Then use this frame to retrieve all RGB channel data
//Iterate over all pixels and retrieve information of RGB channels
for(rows in 1 until cvFrameRGBA.rows())
for(cols in 1 until cvFrameRGBA.cols()) {
val imageData = cvFrameRGBA.get(rows, cols)
// Type of Mat is 24
// Channels is 4
// Depth is 0
rgbBytes.put(imageData[0].toByte())
rgbBytes.put(imageData[1].toByte())
rgbBytes.put(imageData[2].toByte())
}
颜色问题是因为您使用的是不同的 YUV 格式。相机框架使用的 YUV 格式是 YUV NV21。 此格式 (NV21) 是 Android 相机预览的标准图片格式。 YUV 4:2:0 平面图像,具有 8 位 Y 样本,后跟具有 8 位 2x2 子采样色度样本的交错 V/U 平面。
如果你的颜色是反色的,这意味着:
- 您正在使用 YUV NV12(平面 U 是 V,V 是 U)。
- 你的一个彩色平面在做一些奇怪的事情。
为了与 libyuv
正常工作,我建议您使用 transformI420
方法将相机输出转换为 YUV I420,并通过参数发送格式:
return libyuv::ConvertToI420(src, srcSize, //src data
dstY, dstWidth, //dst planes
dstU, dstWidth / 2,
dstV, dstWidth / 2,
cropLeft, cropTop, //crop start
srcWidth, srcHeight, //src dimensions
cropRight - cropLeft, cropBottom - cropTop, //dst dimensions
rotationMode,
libyuv::FOURCC_NV21); //libyuv::FOURCC_NV12
完成此转换后,您将能够使用所有 I420scale
、I420rotate
... 等正确使用 libyuv。您的缩放方法应如下所示:
JNIEXPORT jint JNICALL
Java_com_aa_project_images_yuv_myJNIcl_scaleI420(JNIEnv *env, jclass type,
jobject srcBufferY,
jobject srcBufferU,
jobject srcBufferV,
jint srcWidth, jint srcHeight,
jobject dstBufferY,
jobject dstBufferU,
jobject dstBufferV,
jint dstWidth, jint dstHeight,
jint filterMode) {
const uint8_t *srcY = static_cast<uint8_t *>(env->GetDirectBufferAddress(srcBufferY));
const uint8_t *srcU = static_cast<uint8_t *>(env->GetDirectBufferAddress(srcBufferU));
const uint8_t *srcV = static_cast<uint8_t *>(env->GetDirectBufferAddress(srcBufferV));
uint8_t *dstY = static_cast<uint8_t *>(env->GetDirectBufferAddress(dstBufferY));
uint8_t *dstU = static_cast<uint8_t *>(env->GetDirectBufferAddress(dstBufferU));
uint8_t *dstV = static_cast<uint8_t *>(env->GetDirectBufferAddress(dstBufferV));
return libyuv::I420Scale(srcY, srcWidth,
srcU, srcWidth / 2,
srcV, srcWidth / 2,
srcWidth, srcHeight,
dstY, dstWidth,
dstU, dstWidth / 2,
dstV, dstWidth / 2,
dstWidth, dstHeight,
static_cast<libyuv::FilterMode>(filterMode));
}
如果您想在完成所有过程后将此图像转换为 JPEG。您可以使用 I420toNV21
方法,然后使用 android 从 YUV 到 JPEG 的本机转换。您也可以使用 libJpegTurbo,这是针对这种情况的补充库。