实现思路 🤔
- 基于ffmpeg,画布的方式,创建画布 -> 水印 -> 旋转 -> 抠图 -> 叠加到图像上
- 基于ffmpeg,旋转图片的方式,填充 -> 水印 -> 顺时针旋转 -> 逆时针旋转 -> 截图
- 基于opencv,创建画布 -> 水印 -> 仿射变换 -> 水平垂直拼接 -> 叠加图片上
经测试比对,opencv实现方式效率是最快的
代码实现 💨
- FFmpeg
-
旋转画布方式
- 这种方式相对实现简单一些,但经过试验,对于一个2k的视频,实现全屏文字倾斜,效率太慢了 。不知道是我代码问题,还是什么问题。如果有大佬能提高效率,方便指导一二,抱拳了.
- 实现参考
- https://blog.csdn.net/qq_38722827/article/details/121549999
- https://blog.csdn.net/qq_23282479/article/details/119904850
-
旋转图片方式
- 思路:
- 首先将图片填充放大,然后计算出水印位置,叠加水印,再逆时针将图片旋转回来,字体就变斜了。然后通过剪裁,将图片剪切成原图大小。
- 基于
ffmpeg filter
实现
// 填充 // [in]scale=原图宽:原图高,pad=填充后宽:填充后高:填充左右大小:填充上下大小:black[out] // 四周填充, pad 输入尺寸的一半 char dtext[512]; snprintf(dtext, sizeof(dtext), "[in]scale=%d:%d,pad=%d:%d:%d:%d:black[out]", frameWidth, frameHeight, expandFrameWidth, expandFrameHeight, expandSize, expandSize); // 水印 // inxxxx[a];[a]xxx[a];[a]xxx[out] 中间传的文字标签首位对应([a] [a]). 结束没有';'. char dtext[1000]; snprintf(dtext, sizeof(dtext), "[in]drawtext=fontfile=%s:text=%s:x=%d:y=%d:fontsize=%d:fontcolor=%s:alpha=%.1f[a];",kFont, txt.c_str(), start_x, start_y, kFontSize, kFontColor, italicTextInfo_.alpha); // 翻转 // [in]rotate=PI* angel /180[out] char dtext[512]; snprintf(dtext, sizeof(dtext), "[in]rotate=PI* %d /180[out]", angel); // 截取 // [in]crop=原图宽:原图高:截取x:截取y[out] char dtext[512]; snprintf(dtext, sizeof(dtext), "[in]crop=%d:%d:%d:%d[out]", frameWidth, frameHeight, startx, starty);
- 创建
AVFilterGraph
int init_filter_graph(const std::string& filter) { filterGraph_ = avfilter_graph_alloc(); const AVFilter* buffersrc = avfilter_get_by_name("buffer"); const AVFilter* buffersink = avfilter_get_by_name("buffersink"); char args[512]; snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", frameWidth_, frameHeight_, frameFormat_, 1, videoFps_, 1, 1); int ret = avfilter_graph_create_filter(&buffersrcCtx_, buffersrc, "in", args, NULL, filterGraph_); if (ret < 0) { avfilter_graph_free(&filterGraph_); filterGraph_ = NULL; return -1; } // buffer video sink: to terminate the filter chain. enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NV12, AV_PIX_FMT_NONE }; AVBufferSinkParams* buffersink_params = av_buffersink_params_alloc(); buffersink_params->pixel_fmts = pix_fmts; ret = avfilter_graph_create_filter(&buffersinkCtx_, buffersink, "out", NULL, buffersink_params, filterGraph_); av_free(buffersink_params); if (ret < 0) { avfilter_graph_free(&filterGraph_); filterGraph_ = NULL; return -1; } AVFilterInOut* outputs = avfilter_inout_alloc(); /* Endpoints for the filter graph. */ outputs->name = av_strdup("in"); outputs->filter_ctx = buffersrcCtx_; outputs->pad_idx = 0; outputs->next = NULL; AVFilterInOut* inputs = avfilter_inout_alloc(); inputs->name = av_strdup("out"); inputs->filter_ctx = buffersinkCtx_; inputs->pad_idx = 0; inputs->next = NULL; if ((ret = avfilter_graph_parse_ptr(filterGraph_, filter.c_str(), &inputs, &outputs, NULL)) < 0) goto end; ret = avfilter_graph_config(filterGraph_, NULL); if (ret < 0) goto end; end: avfilter_inout_free(&inputs); avfilter_inout_free(&outputs); return ret; }
- 向滤波输入帧, 然后从
FilterGraph
中取出一个AVFrame
AVFrame* overlay(AVFrame* in) { if (!isInit_) return nullptr; int ret = av_buffersrc_add_frame(buffersrcCtx_, in); if (ret < 0) return nullptr; ret = av_buffersink_get_frame(buffersinkCtx_, out_); if (ret < 0) { av_frame_unref(out_); return nullptr; } return out_; }
- 生成对应
filter
, 然后 向滤波中输入帧
// 大概流程思路 void xxxxx(){ ... // 生成对应filter std::string filter = "对应filter"; init_filter_graph(filter); ... while (true) { ret = av_read_frame(ifmtCtx, pkt); if (ret < 0) goto end; // audio if (pkt->stream_index == aistream) { av_packet_unref(pkt); continue; } // decode encode pkt ret = avcodec_send_packet(deCodecCtx, pkt); if (ret < 0) goto end; while (ret >= 0) { ret = avcodec_receive_frame(deCodecCtx, frame); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } else if (ret < 0) { goto end; } // solove [libx264 @ 0x34b88a0] specified frame type (3) at 0 is not compatible with keyframe interval frame->pict_type = AV_PICTURE_TYPE_NONE; // overlay italic txt info AVFrame* overlay_frame = overlay(frame); if (overlay_frame == nullptr) goto end; ret = encode_frame_to_write_file(enCodecCtx, overlay_frame, pkt, ifmtCtx->streams[vistream]->time_base, ofmtCtx->streams[vostream]->time_base, ofmtCtx); if (ret < 0) { av_frame_free(&overlay_frame); goto end; } av_packet_unref(pkt); av_frame_unref(frame); av_frame_unref(overlay_frame); } #pragma endregion av_packet_unref(pkt); } ... }
- 思路:
-
- Opencv
- 思路
- 先创建一张斜体水印的画布,然后重复叠加到每一张图片上,这样可以提高很大的效率。
- 代码
- 创建斜体水印
int init_water_mark(const std::string& txt, cv::Mat& water) { // 原图宽高 int imageW = italicTextInfo_.frameWidth / italicTextInfo_.col; int imageH = italicTextInfo_.frameHeight / italicTextInfo_.row; cv::Mat canvasMat = cv::Mat::zeros(cv::Size(imageW, imageH), CV_8UC3); // 水印,使用 Times New Roman 字体 VTText::putTextHuskyC(canvasMat, txt.c_str(), cv::Scalar(255, 255, 255), kFontSize, "Times New Roman", false, false); // 实际显示的水印行列数 int w_count = italicTextInfo_.col * 2 - 1; int h_count = italicTextInfo_.row * 2; // 垂直拼接水印 std::vector<cv::Mat> vImgs; cv::Mat vResult; for (int vi = 0; vi < h_count; vi++) vImgs.push_back(canvasMat); vconcat(vImgs, vResult); // 水平拼接水印 std::vector<cv::Mat> hImgs; cv::Mat hResult; for (int hi = 0; hi < w_count; hi++) hImgs.push_back(vResult); hconcat(hImgs, hResult); cv::Point2f center(hResult.cols / 2.0, hResult.rows / 2.0); // 通过仿射变换将水印旋转45度,实现全屏斜体效果 cv::Mat rotation_matix = getRotationMatrix2D(center, italicTextInfo_.italicAngel, 1.0); cv::Mat rotated_image; warpAffine(hResult, rotated_image, rotation_matix, hResult.size()); // 然后将水印图裁剪成原图大小,用于后续叠加到原图上. water = rotated_image(cv::Rect((hResult.cols - italicTextInfo_.frameWidth) / 2, (hResult.rows - italicTextInfo_.frameHeight) / 2, italicTextInfo_.frameWidth, italicTextInfo_.frameHeight)); return 0; } // 将水印图叠加到原图上 // frame: 解码帧 avframe AVFrame* overlayItalicTextInfoCVCanvas(AVFrame* frame) { //cv::Mat src_mat; //frame_to_mat(frame, src_mat); int pktSize; uint8_t* buffer_rgb = convertRgb(frame, frame->width, frame->height, AV_PIX_FMT_YUV420P, frame->width, frame->height, AV_PIX_FMT_BGR24, pktSize); cv::Mat src_mat = cv::Mat(frame->height, frame->width, CV_8UC3, buffer_rgb); cv::Mat roi; roi = src_mat(cv::Rect(0, 0, watermark_.cols, watermark_.rows)); cv::addWeighted(roi, 1.0, watermark_, italicTextInfo_.alpha, 0, roi); AVFrame* dst = mat_to_frame(src_mat, src_mat.cols, src_mat.rows, AV_PIX_FMT_YUV420P); if (buffer_rgb != nullptr) av_free(buffer_rgb); return dst; }
- 大概流程
void xxxxx(){ ... // 先创建个带有斜体水印的mat cv::Mat water; const std::string txt = "12321321"; init_water_mark(txt, water) ... while (true) { ret = av_read_frame(ifmtCtx, pkt); if (ret < 0) goto end; // audio if (pkt->stream_index == aistream) { av_packet_unref(pkt); continue; } // decode encode pkt ret = avcodec_send_packet(deCodecCtx, pkt); if (ret < 0) goto end; while (ret >= 0) { ret = avcodec_receive_frame(deCodecCtx, frame); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } else if (ret < 0) { goto end; } // solove [libx264 @ 0x34b88a0] specified frame type (3) at 0 is not compatible with keyframe interval frame->pict_type = AV_PICTURE_TYPE_NONE; // overlay italic txt info AVFrame* overlay_frame = overlayItalicTextInfo(frame); if (overlay_frame == nullptr) goto end; ret = encode_frame_to_write_file(enCodecCtx, overlay_frame, pkt, ifmtCtx->streams[vistream]->time_base, ofmtCtx->streams[vostream]->time_base, ofmtCtx); if (ret < 0) { av_frame_free(&overlay_frame); goto end; } av_packet_unref(pkt); av_frame_unref(frame); av_frame_unref(overlay_frame); } #pragma endregion av_packet_unref(pkt); } ... }
- 创建斜体水印
- 思路
整理个demo,基于opencv实现的。demo中 需依赖 opencv ffmpeg ,自己搭建个环境即可运行。
demo: https://download.csdn.net/download/haiyangyunbao813/87499341
参考文章 ✨
- https://blog.csdn.net/qq_23282479/article/details/119904850
- https://blog.csdn.net/qq_38722827/article/details/121549999
总结 👊
- 经测试,基于opencv方式,相比较ffmpeg filter ,效率至少能提升2倍。(1920 * 1080 i7 6代u)
- opencv : 平均一帧 20ms
- ffmpeg : 平均一帧 50ms
- 基于ffmpeg filter, 我感觉应该是可以提升效率的,我的想法先创建出来水印,然后重复叠加到每一张frame上就行,但效率总是提升不了,希望哪位大佬有时间能帮忙试试,或者有什么别的好的方式,抱拳了。
- 以上就是我分享的全部内容了,有不对的地方好,欢迎留言指正。有需要的小伙伴,可以参考参考。
最后来个三连,今年就属你最牛逼了 🚀