Posted on 05-11-2014
Filed Under (技术) by waterlin

今天在使用 FFMpeg 解码 iPad 过来的视频流时,有一个非常奇怪的现象,即对于分辨率为 406*720 和 450*720 的图像来说,解码一切正常,但是把 YUV420 的数据转为 RGBA 显示出来时,图像的最右边会有一条几个像素的黑竖条,如下图所示:

./images/ffmpeg-black-bar2.jpg

碰到这个问题,通常的解决思路如下。

第一,检查一下解码是否有错误,这个好办,只要解码不返回错误码就是正常。

第二,如果解码没有问题的话,仔细查看各步的图片格式转化操作是否有问题。

1. 先看看解码出来的 yuv 数据是否就有这条黑边。可以直接把 yuv 保存成二进制文件,然后用 yuvplayer 播放。

如果是使用 FFMpeg 解码,则可以使用如下的代码来保存 yuv (平面格式)数据:

//保存yuv数据
static int fcount = 0;
char yuvdumpName[255];

sprintf(yuvdumpName, "E:/temp/yuv/%d.yuv", fcount);
FILE *fp = fopen(yuvdumpName, "ab");
if (fp) {

    char* yuv = (char*)src_frame->data[0];
    int stride = src_frame->linesize[0];

    for(int y=0; y<720; y++) {
        fwrite(yuv+stride*y, 1, 540, fp);
    }

    char* udata = (char*)src_frame->data[1];
    int ustride = src_frame->linesize[1];

    for(int u=0; u<720/2; u++) {
        fwrite(udata+u*ustride, 1, 540/2, fp);
    }

    char* vdata = (char*)src_frame->data[2];
    int vstride = src_frame->linesize[2];
    for(int v=0; v<720/2; v++) {
        fwrite(vdata+v*vstride, 1, 540/2, fp);
    }

    fclose(fp);
    fcount++;
}

在我这里有一个特别奇怪的现象,分辨是 540*720,但是解码后得到的 YUV 的 stridesize 是: 576, 288, 288。经过查找资料,原来为了方便各种格式转换及效率,通常是把分辨率的宽度进行了 16 倍整数化,方便硬件加速。

在我查看了 yuv 图片后,还是一切正常,真是怪事,只能继续找原因了。

2. 既然 yuv 数据没有黑边,则把 sws_scale 转成 BGRA 数据后的图片保存成位图,看是否有黑边。

可以使用如下的代码来保存 BGRA 的数据:

void saveBmpFile(uint8_t *src,int srcW,int srcH,int srcStride)
{
    static int fileIndex = 0;

    char filename[256];
    sprintf(filename,"e:\\temp\\yuv\\bmp\\%d.bmp", fileIndex++);
    BITMAPINFOHEADER bih; 
    BITMAPFILEHEADER bhh;  
    int widthStep = srcStride; //(((src->m_width * 24) + 31) & (~31)) / 8 ;  
    bih.biSize=40;       // header size  
    bih.biWidth= srcW;
    bih.biHeight= abs(srcH);
    bih.biPlanes=1;  
    bih.biBitCount=32;     // RGB encoded, 24 bit  
    bih.biCompression=BI_RGB;   // no compression
    bih.biSizeImage=srcStride*abs(srcH);
    bih.biXPelsPerMeter=0;  
    bih.biYPelsPerMeter=0;  
    bih.biClrUsed=0;  
    bih.biClrImportant=0;   
    bhh.bfType = ((WORD) ('M' << 8) | 'B');  //'BM'  
    bhh.bfSize = (DWORD)sizeof(BITMAPFILEHEADER) + (DWORD)sizeof(BITMAPINFOHEADER) + widthStep * abs(srcH);
    bhh.bfReserved1 = 0;  
    bhh.bfReserved2 = 0;  
    bhh.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + (DWORD)sizeof(BITMAPINFOHEADER);

    FILE *fp = fopen(filename, "wb");
    if( fp ) {
        src = src + (abs(srcH)-1)*srcStride;
        fwrite(&bhh,1,sizeof(bhh),fp);
        fwrite(&bih,1,sizeof(bih),fp);
        for(int y=0; y<abs(srcH); y++) {
            fwrite(src,1,srcW*4,fp);
            src -= srcStride;
        }
        fclose(fp);
    }
}

如果使用了 Qt 库,也可以直接这样保存图片:

AVFrame *pFrameRGB = avcodec_alloc_frame();

//some decode operation

static int index = 0;
QImage image( pFrameRGB->data[0], codec->width, codec->height, QImage::Format_RGB32 );
image = image.copy();
image.save(QString("E:/temp/yuv/jpg/%1.jpg").arg(index));
index++;

在我这里,恰恰是第2步使用 sws_scale 转换图片格式后出现的黑边。我原来 sws_scale 的使用方法如下:

SwsContext *img_convert_ctx_temp;
img_convert_ctx_temp = sws_getContext(
                                    codec->width, codec->height,
                                    codec->pix_fmt,
                                    dst_w, dst_h, (PixelFormat)dst_fmt,
                                    SWS_BICUBIC, NULL, NULL, NULL);

sws_scale(img_convert_ctx_temp,
          src_frame->data, src_frame->linesize, 0, codec->height,
          pFrameRGB->data,
          pFrameRGB->linesize);

事实上问题就出在这里,是 sws_getContext 这个函数的参数需要带上 SWS_ACCURATE_RND,即修改成如下方式:

SwsContext *img_convert_ctx_temp;
img_convert_ctx_temp = sws_getContext(
                                    codec->width, codec->height,
                                    codec->pix_fmt,
                                    dst_w, dst_h, (PixelFormat)dst_fmt,
                                    SWS_BICUBIC | SWS_ACCURATE_RND, NULL, NULL, NULL);

但是加了这个参数后,运算速度就变得巨慢了。

有一个更好的解决办法,就是在解码后把图片的 yuv 格式转成 BGRA 格式之前,先考虑一下设置 RGBA 格式的图像的宽度是 16 的倍数:

PixelFormat dst_fmt = PIX_FMT_BGRA;
int dst_w = (codec->width/16)*16;
int dst_h = codec->height;

这样在你以后用 sws_scale 时就可以直接使用 dst_w 这样的16的倍数来进行操作。这样,黑边的问题就解决了,黑边得到了裁剪,并且并不会耗费大量的计算时间,不过缺点是图像的分辨率也变小了。*最佳的解决办法,是在压缩流的输出端就保证分辨率的宽度值是 16 的整数倍*。

PS:我这里碰到这个问题,有可能是因为使用的 FFMpeg 库被修改过,用 SSE4 加速了 rgb 到 yuv420 的转换操作。原版的 FFMpeg 在这些奇怪的分辨率下是否会是如此,我没有验证。

© 2014, 浏忙大爆炸. All rights reserved.
除非注明,浏忙大爆炸文章均为原创,转载请以链接形式标明本文地址。

(0) Comments    (751) Views    Read More   
Post a Comment
Name:
Email:
Website:
Comments: