将 YUV_420_888 转换为 JPEG 并保存文件导致图像失真
Posted
技术标签:
【中文标题】将 YUV_420_888 转换为 JPEG 并保存文件导致图像失真【英文标题】:Converting YUV_420_888 to JPEG and saving file results distorted image 【发布时间】:2017-10-16 18:02:04 【问题描述】:我在我的 git repo 中使用了https://***.com/a/40152147/2949966 中提供的ImageUtil
类:https://github.com/ahasbini/cameraview/tree/camera_preview_imp(注意实现在camera_preview_imp
分支中)来实现帧预览回调。 ImageReader
设置为预览ImageFormat.YUV_420_888
格式的帧,该格式将使用ImageUtil
类转换为ImageFormat.JPEG
并将其发送到帧回调。演示应用每 50 帧将回调中的一帧保存到文件中。所有保存的帧图像都出现扭曲,如下所示:
如果我通过在Camera2
中进行以下更改,将ImageReader
改为使用ImageFormat.JPEG
:
mPreviewImageReader = ImageReader.newInstance(previewSize.getWidth(),
previewSize.getHeight(), ImageFormat.JPEG, /* maxImages */ 2);
mCamera.createCaptureSession(Arrays.asList(surface, mPreviewImageReader.getSurface()),
mSessionCallback, null);
图像正常显示,没有任何失真,但是帧速率显着下降并且视图开始滞后。因此我相信ImageUtil
类没有正确转换。
【问题讨论】:
final image
失真是图像写入文件吗?
我的错误,已编辑问题以消除混乱。
在哪里可以看到onImageAvailable(ImageReader reader)
(ImageReader.OnImageAvailableListener) 方法?
在Camera2
类中的mOnPreviewAvailableListener
变量中。
请链接 :) 我找不到。
【参考方案1】:
@volodymyr-kulyk 提供的解决方案没有考虑图像中平面的行步长。下面的代码可以解决问题(image
属于 android.media.Image
类型):
data = NV21toJPEG(YUV420toNV21(image), image.getWidth(), image.getHeight(), 100);
以及实现:
private static byte[] NV21toJPEG(byte[] nv21, int width, int height, int quality)
ByteArrayOutputStream out = new ByteArrayOutputStream();
YuvImage yuv = new YuvImage(nv21, ImageFormat.NV21, width, height, null);
yuv.compressToJpeg(new Rect(0, 0, width, height), quality, out);
return out.toByteArray();
private static byte[] YUV420toNV21(Image image)
Rect crop = image.getCropRect();
int format = image.getFormat();
int width = crop.width();
int height = crop.height();
Image.Plane[] planes = image.getPlanes();
byte[] data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8];
byte[] rowData = new byte[planes[0].getRowStride()];
int channelOffset = 0;
int outputStride = 1;
for (int i = 0; i < planes.length; i++)
switch (i)
case 0:
channelOffset = 0;
outputStride = 1;
break;
case 1:
channelOffset = width * height + 1;
outputStride = 2;
break;
case 2:
channelOffset = width * height;
outputStride = 2;
break;
ByteBuffer buffer = planes[i].getBuffer();
int rowStride = planes[i].getRowStride();
int pixelStride = planes[i].getPixelStride();
int shift = (i == 0) ? 0 : 1;
int w = width >> shift;
int h = height >> shift;
buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift));
for (int row = 0; row < h; row++)
int length;
if (pixelStride == 1 && outputStride == 1)
length = w;
buffer.get(data, channelOffset, length);
channelOffset += length;
else
length = (w - 1) * pixelStride + 1;
buffer.get(rowData, 0, length);
for (int col = 0; col < w; col++)
data[channelOffset] = rowData[col * pixelStride];
channelOffset += outputStride;
if (row < h - 1)
buffer.position(buffer.position() + rowStride - length);
return data;
方法是从以下link获取的。
【讨论】:
我为这些进步奋斗了整个下午,直到我到达这篇文章。我想给你100票!!!! 顺便说一下,转换有点慢,我的设备每帧需要40ms~140ms。 @SiraLam 感谢您的反馈。如果您愿意,您可以在该问题上开一个赏金并奖励它答案;P。关于处理,是的,没错,这是一个缓慢的过程,图像对话本质上往往是这样的。为了能够更快地处理更多的帧,这需要一定程度的多线程和线程之间的一些同步来实现流水线效果。由于帧处理时间约为 100 毫秒,因此用户在查看相机流时不会真正感到延迟 我认为 :P 好吧,用户不会觉得预览有任何延迟,因为我的预览使用的表面与我用于这些帧的表面不同。一旦我使用另一个线程进行转换工作,预览仍然非常流畅。事实上,我正在使用这些框架来进行面部检测并在这些面部上叠加一些东西......遗憾的是,使用 Camera 1 API 这既简单又快速:( 小米A1,使用JPEG格式的图像阅读器应用程序崩溃,转换为YUV_420_888然后使用你的方法。非常感谢。【参考方案2】:更新了 ImageUtil:
public final class ImageUtil
public static byte[] NV21toJPEG(byte[] nv21, int width, int height, int quality)
ByteArrayOutputStream out = new ByteArrayOutputStream();
YuvImage yuv = new YuvImage(nv21, ImageFormat.NV21, width, height, null);
yuv.compressToJpeg(new Rect(0, 0, width, height), quality, out);
return out.toByteArray();
// nv12: true = NV12, false = NV21
public static byte[] YUV_420_888toNV(ByteBuffer yBuffer, ByteBuffer uBuffer, ByteBuffer vBuffer, boolean nv12)
byte[] nv;
int ySize = yBuffer.remaining();
int uSize = uBuffer.remaining();
int vSize = vBuffer.remaining();
nv = new byte[ySize + uSize + vSize];
yBuffer.get(nv, 0, ySize);
if (nv12) //U and V are swapped
vBuffer.get(nv, ySize, vSize);
uBuffer.get(nv, ySize + vSize, uSize);
else
uBuffer.get(nv, ySize , uSize);
vBuffer.get(nv, ySize + uSize, vSize);
return nv;
public static byte[] YUV_420_888toI420SemiPlanar(ByteBuffer yBuffer, ByteBuffer uBuffer, ByteBuffer vBuffer,
int width, int height, boolean deInterleaveUV)
byte[] data = YUV_420_888toNV(yBuffer, uBuffer, vBuffer, deInterleaveUV);
int size = width * height;
if (deInterleaveUV)
byte[] buffer = new byte[3 * width * height / 2];
// De-interleave U and V
for (int i = 0; i < size / 4; i += 1)
buffer[i] = data[size + 2 * i + 1];
buffer[size / 4 + i] = data[size + 2 * i];
System.arraycopy(buffer, 0, data, size, size / 2);
else
for (int i = size; i < data.length; i += 2)
byte b1 = data[i];
data[i] = data[i + 1];
data[i + 1] = b1;
return data;
以JPEG格式写入文件byte[] data
的操作:
//image.getPlanes()[0].getBuffer(), image.getPlanes()[1].getBuffer()
//image.getPlanes()[2].getBuffer(), image.getWidth(), image.getHeight()
byte[] nv21 = ImageUtil.YUV_420_888toI420SemiPlanar(yBuffer, uBuffer, vBuffer, width, height, false);
byte[] data = ImageUtil.NV21toJPEG(nv21, width, height, 100);
//now write `data` to file
!!!处理后不要忘记关闭图像!!!
image.close();
【讨论】:
在聊天中发布的更新:chat.***.com/rooms/144450/…【参考方案3】:Camera2 YUV_420_888 转 Jpeg in Java(Android):
@Override
public void onImageAvailable(ImageReader reader)
Image image = null;
try
image = reader.acquireLatestImage();
if (image != null)
byte[] nv21;
ByteBuffer yBuffer = mImage.getPlanes()[0].getBuffer();
ByteBuffer uBuffer = mImage.getPlanes()[1].getBuffer();
ByteBuffer vBuffer = mImage.getPlanes()[2].getBuffer();
int ySize = yBuffer.remaining();
int uSize = uBuffer.remaining();
int vSize = vBuffer.remaining();
nv21 = new byte[ySize + uSize + vSize];
//U and V are swapped
yBuffer.get(nv21, 0, ySize);
vBuffer.get(nv21, ySize, vSize);
uBuffer.get(nv21, ySize + vSize, uSize);
String savingFilepath = getYUV2jpg(nv21);
catch (Exception e)
Log.w(TAG, e.getMessage());
finally
image.close();// don't forget to close
public String getYUV2jpg(byte[] data)
File imageFile = new File("your parent directory", "picture.jpeg");//no i18n
BufferedOutputStream bos = null;
try
bos = new BufferedOutputStream(new FileOutputStream(imageFile));
bos.write(data);
bos.flush();
bos.close();
catch (IOException e)
return e.getMessage();
finally
try
if (bos != null)
bos.close();
catch (Exception e)
e.printStackTrace();
return imageFile.getAbsolutePath();
注意:处理图像旋转问题。
【讨论】:
这不起作用 :( 之后格式仍然不是 jpeg。【参考方案4】:我认为这里与 YUV 的 NV 和 YV 格式存在一些混淆。 NV(半平面)具有交错的 U/V。 YV(平面)没有。所以这里进行的转换是 YV12/21 而不是 NV12/21。
【讨论】:
以上是关于将 YUV_420_888 转换为 JPEG 并保存文件导致图像失真的主要内容,如果未能解决你的问题,请参考以下文章
Android Camera App 使用 CameraX 将图像保存为 YUV_420_888 格式
将 android.media.Image (YUV_420_888) 转换为位图
使用新的 Android camera2 api 从 YUV_420_888 进行 JPEG 编码时的绿色图像