首页 > 解决方案 > Swscale - 图像补丁 (NV12) 颜色转换 - 无效边框

问题描述

目标是将 NV12 转换为 BGR24 图像,更准确地说是图像块(x:0, y:0, w:220, h:220)。
问题是转换后的补丁右侧未定义的像素列,如下所示: 在此处输入图像描述

问题是为什么会发生这种情况(即使补丁的坐标和尺寸具有偶数值)(有趣的是,对于奇数宽度值,该问题不存在)


该补丁具有以下边界框:(x:0, y:0, w:220, h:220)。
该行为应该可以用任何图像重现。可以使用ppm 转换页面进行转换。

以下代码从 bgr24 图像创建 nv12 图像,然后将 nv12 补丁转换回 bgr24 补丁。如果一切正常,输出应该与源图像相同。

#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>

void readPPM(const char* filename, uint8_t** bgrData, int* stride, int* w, int* h)
{
    FILE* fp = fopen(filename, "rb");
    fscanf(fp, "%*s\n"); //skip format check

    fscanf(fp, "%d %d\n", w, h);
    fscanf(fp, "%*d\n"); //skip max value check

    *stride = *w * 3;
    *bgrData = av_malloc(*h * *stride);

    for (int r = 0; r < *h; r++)
    {
        uint8_t* rowData = *bgrData + r * *stride;
        for (int c = 0; c < *w; c++)
        {
            //rgb -> bgr
            fread(&rowData[2], 1, 1, fp);
            fread(&rowData[1], 1, 1, fp);
            fread(&rowData[0], 1, 1, fp);

            rowData += 3;
        }
    }

    fclose(fp);
}

void writePPM(const char* filename, uint8_t* bgrData, int stride, int w, int h)
{
    FILE* fp = fopen(filename, "wb");
    fprintf(fp, "P6\n");
    fprintf(fp, "%d %d\n", w, h);
    fprintf(fp, "%d\n", 255);

    for (int r = 0; r < h; r++)
    {
        uint8_t* rowData = bgrData + r * stride;
        for (int c = 0; c < w; c++)
        {
            //bgr -> rgb
            fwrite(&rowData[2], 1, 1, fp);
            fwrite(&rowData[1], 1, 1, fp);
            fwrite(&rowData[0], 1, 1, fp);

            rowData += 3;       
        }
    }

    fclose(fp);
}


void bgrToNV12(uint8_t* srcData[4], int srcStride[4], 
               uint8_t* tgtData[4], int tgtStride[4],
               int w, int h)
{
    struct SwsContext* context = sws_getContext(w, h, AV_PIX_FMT_BGR24,
                                                w, h, AV_PIX_FMT_NV12, SWS_POINT, NULL, NULL, NULL);
    {
        sws_scale(context,
                  srcData, srcStride, 0, h,
                  tgtData, tgtStride);
    }
    sws_freeContext(context);
}

void nv12ToBgr(uint8_t* srcData[4], int srcStride[4],
               uint8_t* tgtData[4], int tgtStride[4],
               int w, int h)
{
    struct SwsContext* context = sws_getContext(w, h, AV_PIX_FMT_NV12,
                                                w, h, AV_PIX_FMT_BGR24, SWS_POINT, NULL, NULL, NULL);
    {
        sws_scale(context,
                  srcData, srcStride, 0, h,
                  tgtData, tgtStride);
    }
    sws_freeContext(context);
}


int main()
{
    //load BGR image
    uint8_t* bgrData[4]; int bgrStride[4]; int bgrW, bgrH;
    readPPM("sample.ppm", &bgrData[0], &bgrStride[0], &bgrW, &bgrH);

    //create NV12 image from the BGR image
    uint8_t* nv12Data[4]; int nv12Stride[4];
    av_image_alloc(nv12Data, nv12Stride, bgrW, bgrH, AV_PIX_FMT_NV12, 16);
    bgrToNV12(bgrData, bgrStride, nv12Data, nv12Stride, bgrW, bgrH);

    //convert nv12 patch to bgr patch
    nv12ToBgr(nv12Data, nv12Stride, bgrData, bgrStride, 220, 220);   //invalid result (random column stripe)
    //nv12ToBgr(nv12Data, nv12Stride, bgrData, bgrStride, 221, 220); //valid result

    //save bgr image (should be exactly as original BGR image)
    writePPM("sample-out.ppm", bgrData[0], bgrStride[0], bgrW, bgrH);

    //cleanup
    av_freep(bgrData);
    av_freep(nv12Data);
    return 0;
}

标签: cffmpegswscalecolor-conversion

解决方案


sws_scale 同时进行颜色转换和缩放。

大多数使用的算法需要在目标像素的计算中包括相邻像素。当然,如果图像尺寸不是 x 的倍数,这可能会导致边缘出现问题。其中 x 取决于使用的算法。

如果您在此处将图像尺寸设置为 8 的倍数(下一个 8 的倍数 = 224),则它可以在没有伪影的情况下工作。

nv12ToBgr(nv12Data, nv12Stride, bgrData, bgrStride, 224, 224);

演示

在左侧使用 220 x 220 的图像尺寸,会在转换后的补丁的右边缘产生伪影。

如果选择 224 x 224 则不会产生伪像,请参见屏幕截图中比较两个程序的正确图像。

比较

理论上要求的最小对齐

我们来看看YVU420格式:

为每个像素确定亮度值。颜色信息分为 Cb 和 Cr,从 2x2 像素块计算得出。因此,最小图像大小将是 2 x 2 图像块,导致 6 个字节(即每字节 12 个像素 = 12 * 4 = 48 位 = 6 个字节),请参见此处的图形:

yuv420

因此,最低技术要求是图像的宽度和高度均匀。

您已经为缩放定义了 SWS_POINT 标志,即使用最近邻法。因此理论上对于每个输出像素,确定并使用最近的输入像素,这不会导致任何对齐限制。

表现

但是,算法实际实现的一个重要方面通常是性能。在这种情况下,例如可以一次处理几个相邻像素。也不要忘记硬件加速操作的可能性。

替代解决方案

如果出于某种原因您需要坚持使用 220x220 格式,您也可以使用 SWS_BITEXACT 标志。

它确实:

启用位精确输出。

https://ffmpeg.org/ffmpeg-scaler.html#scaler_005foptions

所以在 nv12ToBgr 你会使用类似的东西:

struct SwsContext* context = sws_getContext(w, h, AV_PIX_FMT_NV12,
                                            w, h, AV_PIX_FMT_BGR24, SWS_POINT | SWS_BITEXACT, NULL, NULL, NULL);

这也不会产生任何工件。如果你必须转换很多帧,我会看看性能。


推荐阅读