将 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 编码时的绿色图像

camera2 如何从图像读取器侦听器中的 YUV_420_888 图像中获取 Exif 数据

如何保存 YUV_420_888 图像?

将android.media.Image(YUV_420_888)转换为Bitmap