FFmpeg集成二维码处理:使用qrencode和quirc
概述
本文将介绍如何在FFmpeg中集成qrencode(二维码生成)和quirc(二维码识别)库,实现视频画面中二维码的读写功能。这种技术可应用于视频水印、互动视频、信息隐藏等多种场景。
系统架构
环境准备
安装依赖库
# 安装qrencode (二维码生成)
sudo apt-get install libqrencode-dev
# 安装quirc (二维码识别)
git clone https://github.com/dlbeer/quirc.git
cd quirc
make
sudo make install
# 安装FFmpeg开发依赖
sudo apt-get install ffmpeg libavcodec-dev libavformat-dev libswscale-dev
二维码生成实现
创建qrencode滤镜
在FFmpeg中创建vf_qrcodegen.c
滤镜:
#include <libavutil/opt.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <qrencode.h>
typedef struct {
const AVClass *class;
char *text; // 二维码内容
int x, y; // 位置坐标
int size; // 二维码尺寸
int margin; // 边距
int version; // 二维码版本
int level; // 容错级别
int update_interval; // 更新间隔(帧数)
// 内部状态
int64_t frame_count;
AVFrame *qr_frame;
struct SwsContext *sws_ctx;
} QRCodeGenContext;
#define OFFSET(x) offsetof(QRCodeGenContext, x)
#define FLAGS AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_FILTERING_PARAM
static const AVOption qrcodegen_options[] = {
{"text", "QR code content", OFFSET(text), AV_OPT_TYPE_STRING, {.str = "FFmpeg QR Code"}, 0, 0, FLAGS},
{"x", "X position", OFFSET(x), AV_OPT_TYPE_INT, {.i64 = 20}, 0, INT_MAX, FLAGS},
{"y", "Y position", OFFSET(y), AV_OPT_TYPE_INT, {.i64 = 20}, 0, INT_MAX, FLAGS},
{"size", "QR code size", OFFSET(size), AV_OPT_TYPE_INT, {.i64 = 100}, 20, 500, FLAGS},
{"margin", "Margin size", OFFSET(margin), AV_OPT_TYPE_INT, {.i64 = 2}, 0, 10, FLAGS},
{"version", "QR version (1-40)", OFFSET(version), AV_OPT_TYPE_INT, {.i64 = 0}, 0, 40, FLAGS},
{"level", "Error correction level (0=L, 1=M, 2=Q, 3=H)", OFFSET(level), AV_OPT_TYPE_INT, {.i64 = 1}, 0, 3, FLAGS},
{"update", "Update interval (frames)", OFFSET(update_interval), AV_OPT_TYPE_INT, {.i64 = 30}, 1, 1000, FLAGS},
{NULL}
};
static int generate_qrcode(QRCodeGenContext *s)
{
QRcode *qrcode;
uint8_t *p;
int x, y, qr_size;
// 释放之前的二维码帧
av_frame_free(&s->qr_frame);
// 生成二维码
QRecLevel level = QR_ECLEVEL_M;
if (s->level == 0) level = QR_ECLEVEL_L;
else if (s->level == 2) level = QR_ECLEVEL_Q;
else if (s->level == 3) level = QR_ECLEVEL_H;
qrcode = QRcode_encodeString(s->text, s->version, level, QR_MODE_8, 1);
if (!qrcode) {
av_log(s, AV_LOG_ERROR, "Failed to generate QR code\n");
return AVERROR(ENOMEM);
}
// 创建二维码帧
qr_size = qrcode->width + 2 * s->margin;
s->qr_frame = av_frame_alloc();
if (!s->qr_frame) {
QRcode_free(qrcode);
return AVERROR(ENOMEM);
}
s->qr_frame->format = AV_PIX_FMT_GRAY8;
s->qr_frame->width = qr_size;
s->qr_frame->height = qr_size;
if (av_frame_get_buffer(s->qr_frame, 32) < 0) {
av_frame_free(&s->qr_frame);
QRcode_free(qrcode);
return AVERROR(ENOMEM);
}
// 填充边距
memset(s->qr_frame->data[0], 255, s->qr_frame->linesize[0] * qr_size);
// 复制二维码数据
p = s->qr_frame->data[0] + s->margin * s->qr_frame->linesize[0] + s->margin;
for (y = 0; y < qrcode->width; y++) {
memcpy(p, qrcode->data + y * qrcode->width, qrcode->width);
p += s->qr_frame->linesize[0];
}
QRcode_free(qrcode);
return 0;
}
static int config_output(AVFilterLink *outlink)
{
AVFilterContext *ctx = outlink->src;
QRCodeGenContext *s = ctx->priv;
// 生成初始二维码
return generate_qrcode(s);
}
static int filter_frame(AVFilterLink *inlink, AVFrame *frame)
{
AVFilterContext *ctx = inlink->dst;
QRCodeGenContext *s = ctx->priv;
int ret;
// 定期更新二维码
s->frame_count++;
if (s->frame_count % s->update_interval == 0) {
if ((ret = generate_qrcode(s)) < 0)
return ret;
}
// 将二维码叠加到视频帧
if (s->qr_frame) {
struct SwsContext *sws_ctx = sws_getContext(
s->qr_frame->width, s->qr_frame->height, AV_PIX_FMT_GRAY8,
s->qr_frame->width, s->qr_frame->height, frame->format,
SWS_BILINEAR, NULL, NULL, NULL);
if (sws_ctx) {
uint8_t *src[] = {s->qr_frame->data[0]};
int src_stride[] = {s->qr_frame->linesize[0]};
// 计算目标位置
int dst_x = s->x;
int dst_y = s->y;
int dst_w = s->size;
int dst_h = s->size;
// 创建临时区域
uint8_t *dst[] = {frame->data[0] + dst_y * frame->linesize[0] + dst_x * 3};
int dst_stride[] = {frame->linesize[0]};
// 缩放并叠加
sws_scale(sws_ctx, src, src_stride, 0, s->qr_frame->height,
dst, dst_stride);
sws_freeContext(sws_ctx);
}
}
return ff_filter_frame(ctx->outputs[0], frame);
}
// 注册滤镜等代码省略...
二维码识别实现
创建quirc滤镜
在FFmpeg中创建vf_qrcoderead.c
滤镜:
#include <libavutil/opt.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <quirc.h>
typedef struct {
const AVClass *class;
int scan_interval; // 扫描间隔(帧数)
int draw_detection; // 是否绘制检测框
int output_file; // 输出文件路径
// 内部状态
int64_t frame_count;
struct quirc *quirc;
FILE *output_fp;
} QRCodeReadContext;
#define OFFSET(x) offsetof(QRCodeReadContext, x)
#define FLAGS AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_FILTERING_PARAM
static const AVOption qrcoderead_options[] = {
{"interval", "Scan interval (frames)", OFFSET(scan_interval), AV_OPT_TYPE_INT, {.i64 = 10}, 1, 1000, FLAGS},
{"draw", "Draw detection boxes", OFFSET(draw_detection), AV_OPT_TYPE_INT, {.i64 = 1}, 0, 1, FLAGS},
{"output", "Output file path", OFFSET(output_file), AV_OPT_TYPE_STRING, {.str = "qrcodes.txt"}, 0, 0, FLAGS},
{NULL}
};
static int config_input(AVFilterLink *inlink)
{
AVFilterContext *ctx = inlink->dst;
QRCodeReadContext *s = ctx->priv;
// 初始化quirc
s->quirc = quirc_new();
if (!s->quirc) {
av_log(ctx, AV_LOG_ERROR, "Failed to create quirc context\n");
return AVERROR(ENOMEM);
}
// 打开输出文件
s->output_fp = fopen(s->output_file, "a");
if (!s->output_fp) {
av_log(ctx, AV_LOG_WARNING, "Could not open output file: %s\n", s->output_file);
}
return 0;
}
static int process_frame(QRCodeReadContext *s, AVFrame *frame)
{
int width = frame->width;
int height = frame->height;
struct quirc *q = s->quirc;
uint8_t *image;
int count, i;
// 调整quirc缓冲区大小
if (quirc_resize(q, width, height) < 0) {
av_log(s, AV_LOG_ERROR, "Failed to resize quirc buffer\n");
return AVERROR(ENOMEM);
}
// 获取图像缓冲区
image = quirc_begin(q, &width, &height);
// 转换为灰度图
struct SwsContext *sws_ctx = sws_getContext(
frame->width, frame->height, frame->format,
width, height, AV_PIX_FMT_GRAY8,
SWS_BILINEAR, NULL, NULL, NULL);
if (sws_ctx) {
const uint8_t *src[] = {frame->data[0]};
int src_stride[] = {frame->linesize[0]};
uint8_t *dst[] = {image};
int dst_stride[] = {width};
sws_scale(sws_ctx, src, src_stride, 0, frame->height, dst, dst_stride);
sws_freeContext(sws_ctx);
} else {
// 简单灰度转换
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
uint8_t *pixel = frame->data[0] + y * frame->linesize[0] + x * 3;
image[y * width + x] = (pixel[0] + pixel[1] + pixel[2]) / 3;
}
}
}
quirc_end(q);
// 检测二维码
count = quirc_count(q);
for (i = 0; i < count; i++) {
struct quirc_code code;
struct quirc_data data;
quirc_decode_error_t err;
quirc_extract(q, i, &code);
err = quirc_decode(&code, &data);
if (err == QUIRC_SUCCESS) {
char timestamp[64];
struct timeval tv;
gettimeofday(&tv, NULL);
snprintf(timestamp, sizeof(timestamp), "%ld.%06ld", tv.tv_sec, tv.tv_usec);
// 输出到日志
av_log(s, AV_LOG_INFO, "QR Code detected: %s\n", data.payload);
// 输出到文件
if (s->output_fp) {
fprintf(s->output_fp, "[%s] Frame: %"PRId64" | QR: %s\n",
timestamp, s->frame_count, data.payload);
fflush(s->output_fp);
}
// 绘制检测框(可选)
if (s->draw_detection) {
draw_detection_box(frame, &code);
}
}
}
return count;
}
static void draw_detection_box(AVFrame *frame, struct quirc_code *code)
{
int i;
const int thickness = 2;
const uint8_t color[3] = {0, 255, 0}; // 绿色
// 绘制四个角点
for (i = 0; i < 4; i++) {
int x0 = code->corners[i].x;
int y0 = code->corners[i].y;
int x1 = code->corners[(i+1)%4].x;
int y1 = code->corners[(i+1)%4].y;
// 使用Bresenham算法绘制直线
draw_line(frame, x0, y0, x1, y1, color, thickness);
}
}
// 注册滤镜等代码省略...
编译与集成
修改FFmpeg配置
在configure
中添加:
# 检测qrencode
enabled libqrencode && require_pkg_config libqrencode qrencode.h qrcode_encodeString
# 检测quirc
quirc_libs="-lquirc"
check_lib quirc.h quirc_new -lquirc || disable quirc
enabled quirc && require libquirc $quirc_libs
注册新滤镜
在libavfilter/allfilters.c
中添加:
extern AVFilter ff_vf_qrcodegen;
extern AVFilter ff_vf_qrcoderead;
// 在滤镜列表中添加
static const AVFilter *filter_list[] = {
// ... 其他滤镜
&ff_vf_qrcodegen,
&ff_vf_qrcoderead,
NULL
};
编译FFmpeg
./configure \
--prefix=/usr/local \
--enable-gpl \
--enable-libqrencode \
--enable-libquirc \
--enable-shared
make -j$(nproc)
sudo make install
使用示例
1. 生成带二维码的视频
# 添加静态二维码
ffmpeg -i input.mp4 -vf "qrcodegen=text='Hello FFmpeg':x=20:y=20:size=100" output.mp4
# 添加动态二维码(每30帧更新)
ffmpeg -i input.mp4 -vf \
"qrcodegen=text='Frame %{frame_num}':x=20:y=20:size=100:update=30" \
output.mp4
# 使用二维码作为水印
ffmpeg -i input.mp4 -i logo.png \
-filter_complex \
"[0:v][1:v]overlay=W-w-10:10,qrcodegen=text='Protected Content':x=10:y=H-h-10:size=80" \
output.mp4
2. 识别视频中的二维码
# 基本识别
ffmpeg -i input.mp4 -vf "qrcoderead" -f null -
# 保存识别结果到文件
ffmpeg -i input.mp4 -vf "qrcoderead=output=qrcodes.txt" -f null -
# 带检测框显示
ffmpeg -i input.mp4 -vf "qrcoderead=draw=1" output.mp4
3. 高级应用:视频元数据嵌入
# 嵌入元数据到二维码
ffmpeg -i input.mp4 -vf \
"qrcodegen=text='title=My Video;author=FFmpeg;date=$(date +%Y-%m-%d)':update=1" \
output.mp4
# 提取元数据
ffmpeg -i output.mp4 -vf "qrcoderead" -f null - 2> metadata.txt
性能优化技巧
1. 二维码生成优化
// 缓存二维码图像
static int need_update(QRCodeGenContext *s)
{
// 仅在内容变化或间隔到达时更新
static char last_text[1024] = {0};
if (strcmp(last_text, s->text) ||
s->frame_count % s->update_interval == 0) {
strncpy(last_text, s->text, sizeof(last_text)-1);
return 1;
}
return 0;
}
2. 二维码识别优化
// ROI(感兴趣区域)检测
static void detect_roi(AVFrame *frame, int *roi_x, int *roi_y, int *roi_w, int *roi_h)
{
// 实现基于运动检测或颜色特征的ROI提取
// 可减少处理区域,提高识别速度
}
// 在process_frame中
int roi_x, roi_y, roi_w, roi_h;
detect_roi(frame, &roi_x, &roi_y, &roi_w, &roi_h);
// 仅处理ROI区域
if (quirc_resize(q, roi_w, roi_h) < 0) {
// 错误处理
}
3. 多线程处理
// 在滤镜初始化中启用多线程
s->thread_count = av_cpu_count();
s->threads = av_malloc_array(s->thread_count, sizeof(pthread_t));
// 分割帧为多个区域并行处理
for (int i = 0; i < s->thread_count; i++) {
int start_y = i * height / s->thread_count;
int end_y = (i+1) * height / s->thread_count;
pthread_create(&s->threads[i], NULL, process_region,
(struct thread_data){s, frame, start_y, end_y});
}
应用场景
1. 视频版权保护
2. 互动视频系统
1. 视频中显示问题二维码
2. 用户扫描二维码回答问题
3. 系统根据答案跳转不同剧情
3. 视频信息增强
# 在视频中嵌入产品信息二维码
ffmpeg -i product.mp4 -vf \
"qrcodegen=text='Product ID:12345;Price:$99.99;URL=...':x=0.9*W:y=0.8*H:size=0.2*min(W,H)" \
output.mp4
故障排除
常见问题及解决方案
问题 | 可能原因 | 解决方案 |
---|---|---|
二维码无法识别 | 分辨率太低 | 增加二维码尺寸,减少边距 |
识别率低 | 视频压缩失真 | 提高视频码率,使用容错级别H |
生成速度慢 | 频繁更新大尺寸二维码 | 增加更新间隔,减小尺寸 |
位置偏移 | 视频分辨率变化 | 使用相对位置(W-w-10) |
内存泄漏 | 未释放资源 | 检查所有分配的资源是否释放 |
调试技巧
# 启用详细日志
ffmpeg -v debug -i input.mp4 -vf "qrcodegen" output.mp4
# 导出二维码图像
ffmpeg -i input.mp4 -vf "select=eq(n\,10)" -vframes 1 qr_frame.png
总结
通过集成qrencode和quirc库,FFmpeg可以实现强大的二维码处理能力:
- 二维码生成:可控制大小、位置、内容和更新频率
- 二维码识别:高效检测并提取视频中的二维码信息
- 性能优化:支持ROI检测、多线程处理和智能更新
- 应用广泛:版权保护、互动视频、信息增强等场景
本文提供的实现方案可以直接集成到FFmpeg中,也可作为独立模块用于其他视频处理系统。实际应用中,可根据需求调整二维码参数和识别策略,平衡性能和效果。