最近做毕设用到了 OpenCV 和 FFmpeg 两个库,需求是将任意大小的 cv::Mat 转换到 AVFrame 做推流。
显然 “任意大小” 这个需求不是很常见,但我碰上了。
在学习过 完美解决OpenCV Mat 与 FFmpeg AVFrame 的相互转换 之后,能够满足基本的使用需求。
然而对于尺寸(宽)不是 64 的倍数 时,还是会出现底部绿屏的效果,猜测应该是数据丢失或者数据对齐的问题。
在阅读了 FFmpeg原理 后,发现 FFmpeg 中数据的 内存对齐 问题。便在原来帖子的基础上改了一下,主要就是在 cv::Mat 转 AVFrame 时,考虑是否需要内存对齐的问题。
inline
cv::Mat AVFrame_to_CVMat(AVFrame* yuv420Frame)
{
//得到AVFrame信息
int srcW = yuv420Frame->width;
int srcH = yuv420Frame->height;
SwsContext* swsCtx = sws_getContext(
srcW, srcH, (AVPixelFormat)yuv420Frame->format,
srcW, srcH, (AVPixelFormat)AV_PIX_FMT_BGR24,
SWS_FAST_BILINEAR, NULL, NULL, NULL
);
//生成Mat对象
cv::Mat mat;
mat.create(cv::Size(srcW, srcH), CV_8UC3);
//格式转换,直接填充Mat的数据data
AVFrame* bgr24Frame = av_frame_alloc();
av_image_fill_arrays(bgr24Frame->data, bgr24Frame->linesize, (uint8_t*)mat.data, (AVPixelFormat)AV_PIX_FMT_BGR24, srcW, srcH, 1);
sws_scale(swsCtx, (const uint8_t* const*)yuv420Frame->data, yuv420Frame->linesize, 0, srcH, bgr24Frame->data, bgr24Frame->linesize);
//释放
av_frame_free(&bgr24Frame);
sws_freeContext(swsCtx);
return mat;
}
inline
AVFrame* CVMat_to_AVFrame(cv::Mat& inMat)
{
//得到Mat信息
AVPixelFormat dstFormat = AV_PIX_FMT_YUV420P;
int width = inMat.cols;
int height = inMat.rows;
//创建AVFrame填充参数 注:调用者释放该frame
AVFrame* frame = av_frame_alloc();
frame->width = width;
frame->height = height;
frame->format = dstFormat;
//初始化AVFrame内部空间
int ret = av_frame_get_buffer(frame, 0);
if (ret < 0)
{
std::cerr << "Could not allocate the video frame data";
return nullptr;
}
ret = av_frame_make_writable(frame);
if (ret < 0)
{
std::cerr << "Av frame make writable failed.";
return nullptr;
}
cv::Size osize = inMat.size();
//转换颜色空间为YUV420
cv::cvtColor(inMat, inMat, cv::COLOR_BGR2YUV_I420);
cv::Size nsize = inMat.size();
// 按YUV420格式,设置数据地址,注意内存对齐
int frame_width = frame->linesize[0];
int frame_size = width * height;
unsigned char* data = inMat.data;
// 无填充情况
if (frame_width == width) {
memcpy(frame->data[0], data, frame_size);
memcpy(frame->data[1], data + frame_size, frame_size / 4);
memcpy(frame->data[2], data + frame_size * 5 / 4, frame_size / 4);
}
else {
memset(frame->data[0], 0, frame->linesize[0] * height);
memset(frame->data[1], 0, frame->linesize[0] * height / 4);
memset(frame->data[2], 0, frame->linesize[0] * height / 4);
// Y Panel
for (int i = 0; i < height; i++) {
memcpy(frame->data[0] + i * frame->linesize[0], data + i * width, width);
}
unsigned char* udata = data + frame_size;
unsigned char* vdata = udata + frame_size / 4;
// UV Panel
for (int i = 0; i < height / 2; i++) {
memcpy(frame->data[1] + i * frame->linesize[1], udata + i * width / 2, width / 2);
memcpy(frame->data[2] + i * frame->linesize[2], vdata + i * width / 2, width / 2);
}
}
return frame;
}
在 av_frame_get_buffer() 后,要比较一下实际分配的存储空间 linesize[0] 是否和原宽度 width 一致,不一致则说明每行的数据并非 width 那么长,而是做了填充,通常是 16 或 64 的整数倍。分配后存储形式如下:
根据新的内存空间将 cv::Mat 中数据正确拷贝即可
另外注意图像宽高均为 偶数 以满足 yuv420 采样格式