二、在ffmpeg轉碼時實現嵌入水印

環境介紹

所使用的開發環境如下

  • 系統: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. ([email protected])\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分別嵌入水印是不會發生損失的

在這裏插入圖片描述

參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章