文章目录

  • 环境介绍
  • ffmpeg简介
  • ffmpeg解码视频流程
  • 将ffmpeg.c改成ffmpeg.cpp
  • 在ffmpeg.cpp中解码函数嵌入水印
  • 对于显示png的logo图
  • ffmpeg另外一种嵌入logo的方法
  • 参考链接


环境介绍

所使用的开发环境如下

  • 系统:ubuntu14.01
  • eclipse版本:2019-12 (4.14.0)
  • OpenCV版本:3.4.10

ffmpeg简介

FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多code都是从头开发的

FFmpeg在Linux平台下开发,但它同样也可以在其它操作系统环境中编译运行,包括Windows、Mac OS X等。这个项目最早由Fabrice Bellard发起,2004年至2015年间由Michael Niedermayer主要负责维护。许多FFmpeg的开发人员都来自MPlayer项目,而且当前FFmpeg也是放在MPlayer项目组的服务器上。项目的名称来自MPEG视频编码标准,前面的"FF"代表"Fast Forward"

ffmpeg解码视频流程


这个解码流程只是针对于自己利用ffmpeg api来对视频进行解码需要用到的步骤,如果要在ffmpeg.c代码中进行这种操作,值需要找到decode_video函数即可


decode_video函数在process_input_packet中调用,这里还可以对audio做处理


将ffmpeg.c改成ffmpeg.cpp

要首先确定gcc、g++的版本是4.4.7,这个版本是不知道c11的,这样在ffmpeg的configure中就不会使用c11,c11对.cpp的要求要严格一些,需要在malloc转换后指定强制类型,否则报错;但ffmpeg.c很多都没指定类型,所以要使用了更新的版本4.8+则需要改一大堆问题

但是要编译新版的opencv3.4.10则需要使用到g++4.8以上的版本,所以这里涉及到gcc、g++版本的切换,编译opencv的时候gcc、g++使用4.8+的版本,切换代码如下(如果没有4.8的版本,需要apt-get进行安装)

cd /usr/bin
sudo mv gcc gcc.bak
sudo ln -s gcc-4.8 gcc
gcc -v
sudo mv g++ g++.bak
sudo ln -s g++-4.8 g++
g++ -v

编译完OpenCV后,需要切换回4.4的版本,如果没有4.4的版本,第一行可以直接安装

sudo apt-get install gcc-4.4 g++-4.4 g++-4.4-multilib
sudo mv gcc gcc.bak
sudo ln -s gcc-4.4 gcc
sudo mv g++ g++.bak
sudo ln -s g++-4.4 g++

解压ffmpeg4.2.2,目前应该是4.2.3了,进入到目录中,把ffmpeg.c修改成ffmpeg.cpp,再次./configure和make,最后在eclipse中引入工程,操作请参考:linux使用eclipse&&linux编译ffmpeg

在ffmpeg.cpp中解码函数嵌入水印

下面的代码需要在decode_video函数中完成

所要做的操作如下

  • 1、新建全局变量,记录logo图像数据,logo图的行高,logo图的列高
  • 2、OpenCV加载logo图,转成yuv数据
  • 3、将2保存到对应的unsigned char*数组中
  • 4、在decode_video中解码后的每一帧decoded_frame中把3的对应yuv分量数据分别插入到yuv分量中

嵌入logo图完整的decode_video函数如下

unsigned char* logo_img_data = 0;
int logo_rows = 0;
int logo_cols = 0;
int u = 0; 	// 水印图的u偏移
int v = 0;	// 水印图的v偏移
static int decode_video(InputStream *ist, AVPacket *pkt, int *got_output, int64_t *duration_pts, int eof,
                        int *decode_failed)
{
    AVFrame *decoded_frame;
    int i, ret = 0, err = 0;
    int64_t best_effort_timestamp;
    int64_t dts = AV_NOPTS_VALUE;
    AVPacket avpkt;

    // With fate-indeo3-2, we're getting 0-sized packets before EOF for some
    // reason. This seems like a semi-critical bug. Don't trigger EOF, and
    // skip the packet.
    if (!eof && pkt && pkt->size == 0)
        return 0;

    if (!ist->decoded_frame && !(ist->decoded_frame = av_frame_alloc()))
        return AVERROR(ENOMEM);
    if (!ist->filter_frame && !(ist->filter_frame = av_frame_alloc()))
        return AVERROR(ENOMEM);
    decoded_frame = ist->decoded_frame;
    if (ist->dts != AV_NOPTS_VALUE)
        dts = av_rescale_q(ist->dts, AV_TIME_BASE_Q, ist->st->time_base);
    if (pkt) {
        avpkt = *pkt;
        avpkt.dts = dts; // ffmpeg.c probably shouldn't do this
    }

    // The old code used to set dts on the drain packet, which does not work
    // with the new API anymore.
    if (eof) {
        void *new_value = av_realloc_array(ist->dts_buffer, ist->nb_dts_buffer + 1, sizeof(ist->dts_buffer[0]));
        if (!new_value)
            return AVERROR(ENOMEM);
        ist->dts_buffer = new_value;
        ist->dts_buffer[ist->nb_dts_buffer++] = dts;
    }

    update_benchmark(NULL);
    ist->dec_ctx->width;
    ret = decode(ist->dec_ctx, decoded_frame, got_output, pkt ? &avpkt : NULL);
    update_benchmark("decode_video %d.%d", ist->file_index, ist->st->index);
    if (ret < 0)
        *decode_failed = 1;

    // 得到水印图的unsigned char*数据
    if(!logo_img_data)
    {
    	// 操作的是rgb的图,需要先转成yuv,再分别嵌入yuv分量
    	cv::Mat logo_img_rgb;
    	logo_img_rgb = cv::imread("logo.jpg", 1);
    	cv::Mat logo_yuv_img;
    	cv::cvtColor(logo_img_rgb, logo_yuv_img, CV_BGR2YUV_I420);
    	cout << logo_yuv_img.rows << endl;
    	logo_rows = logo_img_rgb.rows;
    	logo_cols = logo_img_rgb.cols;
    	int image_size = logo_yuv_img.cols * logo_yuv_img.rows;
    	logo_img_data = new unsigned char[image_size];
    	int a = 0;
    	for (int i = 0; i < logo_yuv_img.rows; i++) // -----------------------charu
    	{
    		for (int j = 0; j < logo_yuv_img.cols; j++)
    		{
    			logo_img_data[a] = logo_yuv_img.at<uchar>(i, j); // 从yuv中拿数据,但是宽高是一开始的img
    			a++;
    		}
    	}
//    	u = logo_img_rgb.rows * 2 / 3 * logo_cols;
    	u = logo_img_rgb.rows * logo_cols;
    	v = logo_yuv_img.rows * 5 / 6 * logo_cols;
//    	v = u + logo_cols / 2;
    }
//    cout << logo_rows << " " << logo_cols << " " << decoded_frame->linesize[0] << endl;
	// 如果直接在yuv上面操作,则只需要改变行
    if(decoded_frame->linesize[0] != 0)
    {
        int a = 0;
        // 在y分量嵌入
    	for (int i = 0; i < logo_rows; i++)
    	{
    		memcpy(decoded_frame->data[0] + decoded_frame->linesize[0] * a, logo_img_data + logo_cols * a, logo_cols);
    		a++;
    	}
    	a = 0;
    	// 在u分量嵌入
    	for (int i = 0; i < logo_rows / 2; i++)
    	{
    		memcpy(decoded_frame->data[1] + decoded_frame->linesize[1] * a, logo_img_data + u + logo_cols / 2 * a, logo_cols / 2);
    		a++;
    	}
    	a = 0;
    	// 在v分量嵌入
    	for (int i = 0; i < logo_rows / 2; i++)
    	{
    		memcpy(decoded_frame->data[2] + decoded_frame->linesize[2] * a, logo_img_data + v + logo_cols / 2 * a, logo_cols / 2);
    		a++;
    	}
    }

    // The following line may be required in some cases where there is no parser
    // or the parser does not has_b_frames correctly
    if (ist->st->codecpar->video_delay < ist->dec_ctx->has_b_frames) {
        if (ist->dec_ctx->codec_id == AV_CODEC_ID_H264) {
            ist->st->codecpar->video_delay = ist->dec_ctx->has_b_frames;
        } else
            av_log(ist->dec_ctx, AV_LOG_WARNING,
                   "video_delay is larger in decoder than demuxer %d > %d.\n"
                   "If you want to help, upload a sample "
                   "of this file to ftp://upload.ffmpeg.org/incoming/ "
                   "and contact the ffmpeg-devel mailing list. (ffmpeg-devel@ffmpeg.org)\n",
                   ist->dec_ctx->has_b_frames,
                   ist->st->codecpar->video_delay);
    }

    if (ret != AVERROR_EOF)
        check_decode_result(ist, got_output, ret);

    if (*got_output && ret >= 0) {
        if (ist->dec_ctx->width  != decoded_frame->width ||
            ist->dec_ctx->height != decoded_frame->height ||
            ist->dec_ctx->pix_fmt != decoded_frame->format) {
            av_log(NULL, AV_LOG_DEBUG, "Frame parameters mismatch context %d,%d,%d != %d,%d,%d\n",
                decoded_frame->width,
                decoded_frame->height,
                decoded_frame->format,
                ist->dec_ctx->width,
                ist->dec_ctx->height,
                ist->dec_ctx->pix_fmt);
        }
    }

    if (!*got_output || ret < 0)
        return ret;

    if(ist->top_field_first>=0)
        decoded_frame->top_field_first = ist->top_field_first;

    ist->frames_decoded++;

    if (ist->hwaccel_retrieve_data && decoded_frame->format == ist->hwaccel_pix_fmt) {
        err = ist->hwaccel_retrieve_data(ist->dec_ctx, decoded_frame);
        if (err < 0)
            goto fail;
    }
    ist->hwaccel_retrieved_pix_fmt = decoded_frame->format;

    best_effort_timestamp= decoded_frame->best_effort_timestamp;
    *duration_pts = decoded_frame->pkt_duration;

    if (ist->framerate.num)
        best_effort_timestamp = ist->cfr_next_pts++;

    if (eof && best_effort_timestamp == AV_NOPTS_VALUE && ist->nb_dts_buffer > 0) {
        best_effort_timestamp = ist->dts_buffer[0];

        for (i = 0; i < ist->nb_dts_buffer - 1; i++)
            ist->dts_buffer[i] = ist->dts_buffer[i + 1];
        ist->nb_dts_buffer--;
    }

    if(best_effort_timestamp != AV_NOPTS_VALUE) {
        int64_t ts = av_rescale_q(decoded_frame->pts = best_effort_timestamp, ist->st->time_base, AV_TIME_BASE_Q);

        if (ts != AV_NOPTS_VALUE)
            ist->next_pts = ist->pts = ts;
    }

    if (debug_ts) {
        av_log(NULL, AV_LOG_INFO, "decoder -> ist_index:%d type:video "
               "frame_pts:%s frame_pts_time:%s best_effort_ts:%"PRId64" best_effort_ts_time:%s keyframe:%d frame_type:%d time_base:%d/%d\n",
               ist->st->index, av_ts2str(decoded_frame->pts),
               av_ts2timestr(decoded_frame->pts, &ist->st->time_base),
               best_effort_timestamp,
               av_ts2timestr(best_effort_timestamp, &ist->st->time_base),
               decoded_frame->key_frame, decoded_frame->pict_type,
               ist->st->time_base.num, ist->st->time_base.den);
    }

    if (ist->st->sample_aspect_ratio.num)
        decoded_frame->sample_aspect_ratio = ist->st->sample_aspect_ratio;

    err = send_frame_to_filters(ist, decoded_frame);

fail:
    av_frame_unref(ist->filter_frame);
    av_frame_unref(decoded_frame);
    return err < 0 ? err : ret;
}

在main函数的最后只需要释放这个logo_img_data即可

if(logo_img_data)
{
	delete[] logo_img_data;
	logo_img_data = NULL;
}


编译是没有问题的


logo图如下


原始视频1.mp4如下


执行转码命令,让其进入decode_video:

./ffmpeg -i 1.mp4 -vcodec h264 -strict -2 2.mp4 -y

完成后编码后提示如下


嵌入logo后的视频如下


对于显示png的logo图

由于在OpenCV中对png的解析不太好,显示png白色背景会出现如下问题:


原图应该是这样的


OpenCv图像叠加时png图片的透明部分无法透明的解决办法提供了一个使用阈值来过滤像素的方法,查看了一下也是不太好的

Mat image = imread("l_hires.jpg");
Mat logo = imread("logo.png");
Mat mask = imread("2.png", 0); //注意要是灰度图才行
threshold(mask, mask, 254, 255, CV_THRESH_BINARY);
Mat mask1 = 255 - mask; //掩模反色
//imshow(“img”,mask1);
Mat imageROI;
imageROI = image(Rect(480, 320, logo.cols, logo.rows));
logo.copyTo(imageROI, mask1);
namedWindow("result");
imshow("result", image);
waitKey();
return 0;


并且十分不灵活的时候,需要按照透明处的像素值去设置,如果单纯白色还比较好操作,如果是下面的颜色,会出现如下


ffmpeg另外一种嵌入logo的方法

使用命令如下

./ffmpeg -i 1.mp4 -i 2.png -filter_complex "overlay = 10:10 1" -c:a copy new.mp4 -y

这种方式是自带的方法,是可以完美支持png的logo图的,可以通过overlay进行叠加,主要是文件libavfilter/vf_overlay.c中


核心在于do_blend(中的execute最后调用了blend_slice_yuv420)—>线程分发->blend_slice_yuv420(如果视频是420的形式,其他还有yuv444等)—>blend_slice_yuv





blend_plane其实是要对AVFrame中的yuv,每一个data[i]中的像素做处理


如果只是对yuv和隔帧进行嵌入logo,那么在blend_slice_yuv函数进行操作是最合适的,比如我就试过每隔25帧嵌入一个logo,或者仅仅在关键帧时候嵌入logo


还有可以不嵌入y分量,只嵌入uv,会得到这样的效果


比较遗憾的是,比如隔25帧嵌入,嵌入的logo图,会发生损失,目前还不清楚是什么原因造成的,但同样的隔帧,在转码时候在yuv分别嵌入水印是不会发生损失的