MediaCodec 和 Camera:颜色空间不匹配

34

我一直在尝试使用新的低级MediaCodec使Android平板电脑拍摄的输入图像进行H264编码。由于MediaCodecAPI文档质量欠佳,我遇到了一些困难,但最终我已经让它可以工作。

我是如下设置相机:

        Camera.Parameters parameters = mCamera.getParameters();
        parameters.setPreviewFormat(ImageFormat.YV12); // <1>
        parameters.setPreviewFpsRange(4000,60000);
        parameters.setPreviewSize(640, 480);            
        mCamera.setParameters(parameters);

对于编码部分,我正在通过以下方式实例化MediaCodec对象:

    mediaCodec = MediaCodec.createEncoderByType("video/avc");
    MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 640, 480);
    mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 500000);
    mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
    mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
            MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar); // <2>
    mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
    mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    mediaCodec.start();
最终目标是创建一个RTP流(并与Skype通信),但目前我只是将原始H264直接流式传输到我的桌面。在那里,我使用以下GStreamer管道来显示结果:
gst-launch udpsrc port=5555 ! video/x-h264,width=640,height=480,framerate=15/1 ! ffdec_h264 ! autovideosink
除了颜色问题,一切正常。我需要在计算机中设置两种颜色格式:一种用于相机预览(标有<1>),一种用于MediaCodec对象(标有<2>)。
为确定行<1>的可接受值,我使用了parameters.getSupportedPreviewFormats()。从这个函数返回值,我知道相机仅支持ImageFormat.NV21ImageFormat.YV2这两种格式。
对于<2>,我获取了类型为video/avcMediaCodecInfo.CodecCapabilities对象,它对应的整数值为19(对应于MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar)和2130708361(这不对应任何一个MediaCodecInfo.CodecCapabilities的值)。
除此之外的任何值都会导致崩溃。
将这些设置组合起来会产生不同的结果,下面我将展示这些结果。以下是在Android上的屏幕截图(即“真实”颜色): Input on Android-tablet 以下是Gstreamer显示的结果: <1> = NV21,<2> = COLOR_FormatYUV420Planar Gstreamer-output for NV21-COLOR_FormatYUV420Planar <1> = NV21,<2> = 2130708361 Gstreamer-output for NV21-2130708361 <1> = YV2,<2> = COLOR_FormatYUV420Planar Gstreamer-output for YV2-COLOR_FormatYUV420Planar <1> = YV2,<2> = 2130708361 Gstreamer-output for YV2-2130708361 可以看出,这些结果都不令人满意。YV2颜色空间看起来最有前途,但是红色(Cr)和蓝色(Cb)似乎被颠倒了。NV21看起来是交错的(尽管我不是这个领域的专家)。
由于目的是与Skype通信,我假设我不应更改解码器(即Gstreamer命令),对吗?这可以在Android中解决吗?如果可以,怎么解决?还有其他的建议吗?

1
看起来2130708361对应于COLOR_FormatSurface。您必须使用API版本> 17。 - Ryan
5个回答

7
我通过在 Android 平台上交换字节平面自己解决了它,使用了一个简单的函数:
public byte[] swapYV12toI420(byte[] yv12bytes, int width, int height) {
    byte[] i420bytes = new byte[yv12bytes.length];
    for (int i = 0; i < width*height; i++)
        i420bytes[i] = yv12bytes[i];
    for (int i = width*height; i < width*height + (width/2*height/2); i++)
        i420bytes[i] = yv12bytes[i + (width/2*height/2)];
    for (int i = width*height + (width/2*height/2); i < width*height + 2*(width/2*height/2); i++)
        i420bytes[i] = yv12bytes[i - (width/2*height/2)];
    return i420bytes;
}

顺便说一句,有些设备接受COLOR_FormatYUV420Planar,有些设备接受COLOR_FormatYUV420SemiPlanar。 - fadden
2
在4.3(API 18)中,您可以使用Surface预览而不是ByteBuffer。这样可以避免需要更正颜色平面,并且具有可移植性。请参见http://bigflake.com/mediacodec/上的CameraToMpegTest示例。 - fadden
是的,500、1000毫秒,请删除此答案。 - user25
@user25,这个问题和答案已经超过5年了。我们都知道图片格式是错的,但更改它的函数没有记录——可能甚至不存在。如果您仍然遇到类似的问题,您应该编写一个新的答案,因为其他人肯定也会遇到相同的问题。 - John La Rooy

6

我认为直接在原地交换数值更加高效。

        int wh4 = input.length/6; //wh4 = width*height/4
        byte tmp;
        for (int i=wh4*4; i<wh4*5; i++)
            {
            tmp = input[i];
            input[i] = input[i+wh4];
            input[i+wh4] = tmp;
            }

也许更好的方法是,您可以替换掉原始的代码。
            inputBuffer.put(input);

在正确的顺序下,使用3个平面切片。
            inputBuffer.put(input, 0, wh4*4);
            inputBuffer.put(input, wh4*5, wh4);
            inputBuffer.put(input, wh4*4, wh4);

我认为这只会产生很小的开销。


使用cameraParamters.setPreviewFormat(ImageFormat.YV12);(ImageFormat.YV12),你的解决方案什么也不做(只有三个平面切片正确排序时才有效)。 - user25

3
似乎Android正在传输YV12,但在H264头文件中设置的格式是YUV420。这些格式相等,除了U和V通道的顺序不同,这解释了红色和蓝色的交换。
当然,最好的方式是修复Android端的设置。但如果没有办法为摄像头和编码器设置兼容的设置,您将不得不在GStreamer端强制执行格式。
可以通过在ffdec_h264后添加capssetter元素来完成这项操作。
... ffdec_h264!capssetter caps="video/x-raw-yuv, format=(fourcc)YV12"!colorspace!...

我能否通过在Android端添加GStreamer管道来解决这个问题?鉴于上述方案使用了高达50%的CPU,您认为是否有足够的资源来实现并行的H264解码呢? - gleerman
Android默认格式为NV21,而不是YV12。 - user25

0

使用ImageFormat.NV21设置相机和COLOR_FormatYUV420Planar作为编码器时,我的情况下出现了类似的蓝色阴影重叠。据我所知,在我的情况下不能使用上述交换函数,有什么算法建议可以用于解决这个问题吗? 附注:当相机预览格式设置为YV12时,在解码器中是完全黑屏状态。


以上的交换仅在您在相机端设置ImageFormat.YV12时才有效。从NV21进行转换将会更加复杂,因为它包括一个交错平面(请参见http://www.fourcc.org/yuv.php)。如果我理解正确,这个交错平面也交换了U和V的值,因此交换函数可能仍然有用(稍作修改,因为宽度和高度只有一半)。 - gleerman

0

我使用了这里的代码,使用媒体编码器将相机图像转换为视频,但出现了同样的问题。

因此,参考维基百科文章,我修改了代码,输出似乎正常工作。将字节顺序从uvuv更改为uuuuuvvvv。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接