我正在尝试将 androidcamera2 中获得的 yuv 420 预览帧保存为 jpeg。我发现这样做的唯一方法是将 yuv420 转换为 nv21,构建 yuvimage,然后使用 compresstojpeg 方法获取 jpeg。为了从 yuv420 转换为 jpeg,我使用下面的逻辑
Image.Plane Y = img.getPlanes()[0];
Image.Plane U = img.getPlanes()[2];
Image.Plane V = img.getPlanes()[1];
int Yb = Y.getBuffer().remaining();
int Ub = U.getBuffer().remaining();
int Vb = V.getBuffer().remaining();
byte[] data = new byte[Yb + Ub + Vb];
Y.getBuffer().get(data, 0, Yb);
U.getBuffer().get(data, Yb, Ub);
V.getBuffer().get(data, Yb + Ub, Vb);
YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21,
mPreviewSize.getWidth(), mPreviewSize.getHeight(), null);
ByteArrayOutputStream out = new ByteArrayOutputStream();
yuvImage.compressToJpeg(new Rect(0, 0,
mPreviewSize.getWidth(), mPreviewSize.getHeight()),
100, out);
然而,这会导致某些分辨率 144 x 176、176x144、352x288、480x360、1280x960 获得绿色图像。转换为nv21的逻辑是否正确?我还可以使用什么其他方法将 yuv420 转换为 jpeg。 是否有任何 Java/Android api 可以实现这一点?
不,这是不正确的 - 您没有注意平面的行跨距或像素跨距。
您必须解析这些内容,并确保您的输出缓冲区实际上与 YuvImage 的 NV21 输入的输入期望相匹配,该输入假设行步长 = 宽度和交错的 V/U 平面。
您拥有的代码仅在输入图像 U/V 平面实际上交错时才有效(在这种情况下,您添加所需 UV 数据的两倍,但第一个副本恰好是正确的布局...),并且如果宽度==行步幅。 width==row stride 是否取决于分辨率;由于硬件限制,通常步幅必须是 16 像素的倍数或类似的值。因此,例如,对于不是 16 的倍数的分辨率,您的代码将无法工作。
请解决这两个问题 - 注意行和像素步幅;否则,您可能会意外地使其在您的设备上正常工作,但在具有不同步幅参数的设备上仍然会出现故障。
编辑:
可以在 Android AOSP 相机服务代码中找到一些执行此类转换的示例 C++ 代码:CallbackProcessor::convertFromFlexibleYuv。
供参考的映射:
CameraX 项目 的 Java 实现
/** {@link android.media.Image} to NV21 byte array. */
@NonNull
public static byte[] yuv_420_888toNv21(@NonNull Image image) {
Image.Plane yPlane = image.getPlanes()[0];
Image.Plane uPlane = image.getPlanes()[1];
Image.Plane vPlane = image.getPlanes()[2];
ByteBuffer yBuffer = yPlane.getBuffer();
ByteBuffer uBuffer = uPlane.getBuffer();
ByteBuffer vBuffer = vPlane.getBuffer();
yBuffer.rewind();
uBuffer.rewind();
vBuffer.rewind();
int ySize = yBuffer.remaining();
int position = 0;
// TODO(b/115743986): Pull these bytes from a pool instead of allocating for every image.
byte[] nv21 = new byte[ySize + (image.getWidth() * image.getHeight() / 2)];
// Add the full y buffer to the array. If rowStride > 1, some padding may be skipped.
for (int row = 0; row < image.getHeight(); row++) {
yBuffer.get(nv21, position, image.getWidth());
position += image.getWidth();
yBuffer.position(
Math.min(ySize, yBuffer.position() - image.getWidth() + yPlane.getRowStride()));
}
int chromaHeight = image.getHeight() / 2;
int chromaWidth = image.getWidth() / 2;
int vRowStride = vPlane.getRowStride();
int uRowStride = uPlane.getRowStride();
int vPixelStride = vPlane.getPixelStride();
int uPixelStride = uPlane.getPixelStride();
// Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to
// perform faster bulk gets from the byte buffers.
byte[] vLineBuffer = new byte[vRowStride];
byte[] uLineBuffer = new byte[uRowStride];
for (int row = 0; row < chromaHeight; row++) {
vBuffer.get(vLineBuffer, 0, Math.min(vRowStride, vBuffer.remaining()));
uBuffer.get(uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining()));
int vLineBufferPosition = 0;
int uLineBufferPosition = 0;
for (int col = 0; col < chromaWidth; col++) {
nv21[position++] = vLineBuffer[vLineBufferPosition];
nv21[position++] = uLineBuffer[uLineBufferPosition];
vLineBufferPosition += vPixelStride;
uLineBufferPosition += uPixelStride;
}
}
return nv21;
}
一旦从NV21
图像中获得
YUV420_888
,您可以使用
YuvImage
将其压缩为
JPEG
(您也可以在下面提到的同一文件中找到它)