最近工作中遇到一个需求。现有代码中的图形库使用 ImageMagic 加载图片并做简单处理,但是在移植到 iOS 平台的过程中遇到了些问题。于是找到我,看能否用 FFmpeg 实现图片的从文件中读取加载、存储到文件中、以及缩放、裁剪等简单处理,并对比与 ImageMagic 相关功能的效率
于是就边学边做,用 FFmpeg API 接口实现了一个 MyImage 类,提供 Load(string filename), Load(uint8_t* buffer, int w, int h, AVPixelFormat fmt) , Write(string filename), Write(uint8_t* buffer, int bufsize, enum AVPixelFormat format), width(), height(), format(), Resize(int to_w, to_h), Crop(int x, int y, int w, int h) 等接口。

编解码是大头

但很容易理解。关键就是把一张图片看作是只有一帧的视频

读取

所以,读取图片文件的整个代码流程如下:

bool Load(string filename) {AVFormatContext* fmt_ctx = nullptr;AVCodecContext* codec_ctx = nullptr;AVCodec* codec = nullptr;avformat_open_input(&fmt_ctx, filename.c_str(), nullptr, nullptr);// 代码删除了所有判断 api 调用失败的情况int stid = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, false);codec_ctx = avcodec_alloc_context3(codec);avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[stid]->codecpar);avcodec_open2(codec_ctx, codec, nullptr);AVFrame* frame = av_frame_alloc();frame->format = codec_ctx->pix_fmt;frame->width = codec_ctx->width;frame->height = codec_ctx->height;av_frame_get_buffer(frame, 0);AVPacket pkt;av_init_packet(&pkt);int ret = 0;while (av_read_frame(fmt_ctx, &pkt) >= 0) {if (pkt.stream_index == stid) {avcodec_send_packet(codec_ctx, &pkt);while ((ret = avcodec_receive_frame(codec_ctx, frame)) != 0) {if (ret == AVERROR_EOF) break;avcodec_send_packet(codec_ctx, nullptr);}av_packet_unref(&pkt);break;} else {av_packet_unref(&pkt);}}av_packet_unref(&pkt); // 注意:缺少这句将造成内存泄漏return frame->width * frame->height > 0;
}

以上,就算是成功地将一张 .jpg 或者 .png 或者其他扩展名的图片,读取到内存中了。只是暂时它存储在 frame->data 里,我们没有拷贝到自己申请的内存中。缺少倒数第三行会造成内存泄漏,参见我的文章FFmpeg av_packet_unref() 不严谨导致的一次内存泄漏,每次 6MB。当然,正常编码中 AVFrame* frame 是一个类成员变量,而非局部变量。此处为了方便,简化了。

存储

同样的,存储的代码流程如下:

bool Save(string filename) {if (!frame || frame->data[0]) { // 假设 frame 是个类成员变量cout << "nothing to save to file." << endl;return false;}AVFormatContext* ofmt_ctx = nullptr;AVCodecContext* ocodec_ctx = nullptr;AVCodec* ocodec = nullptr;int ret = avformat_alloc_output_context2(&ofmt_ctx, nullptr, nullptr, filename.c_str());if (!ofmt_ctx) {cout << "could not deduce output format from file extension, using image2. << endl;avformat_alloc_output_context2(&ofmt_ctx, nullptr, "image2", filename.c_str());}ocodec = avcodec_find_encoder(ofmt_ctx->oformat->video_codec);AVStream* st = avformat_new_stream(ofmt_ctx, nullptr);st->id = ofmt_ctx->nb_streams - 1;ocodec_ctx = avcodec_alloc_context3(ocodec);ocodec_context_->codec_id = oformat_context_->oformat->video_codec;ocodec_context_->codec_type = AVMEDIA_TYPE_VIDEO;ocodec_context_->width = frame_->width;ocodec_context_->height = frame_->height;st->time_base = av_make_q(1, 25);ocodec_context_->time_base = st->time_base;ocodec_context_->sample_aspect_ratio = av_make_q(1, 1);ocodec_context_->profile = FF_PROFILE_MJPEG_HUFFMAN_PROGRESSIVE_DCT;ocodec_context_->pix_fmt = AV_PIX_FMT_YUVJ420P; avcodec_open2(ocodec_ctx, ocodec, nullptr);avcodec_parameters_from_context(st->codecpar, ocodec_ctx);avio_open(&ofmt_ctx->pb, filename.c_str(), AVIO_FLAG_WRITE);avformat_write_header(ofmt_ctx, nullptr);avcodec_send_frame(ocodec_ctx, frame);AVPacket pkt = {0};avcodec_receive_packet(ocodec_ctx, &pkt);av_packet_rescale_ts(&pkt, ocodec_ctx->time_base, st->time_base);pkt.stream_index = st->index;av_write_frame(ofmt_ctx, &pkt);av_packet_unref(&pkt);av_write_trailer(ofmt_ctx);avio_closep(&ofmt_ctx->pb);return true;
}

同样地,过程中省略了一些必要的 ffmpeg api 执行错误判断。

缩放

缩放听简单的,就是读取出来的图片在 frame 中,再利用 swscale 相关接口进行一次缩放或者图片颜色格式转换即可,与视频缩放一样。暂不赘述。

裁剪

由于 FFmpeg 的裁剪功能是用滤镜实现的。感觉颇为麻烦,于是,自己参考其 crop 滤镜实现了一种基于 RGBA 颜色格式的裁剪方法。具体代码如下:

bool Crop(int x, int y, int w, int h) { // 以 (x,y) 为起始,裁剪宽 w 高 h 的图片// 如果不是 RGBA 颜色格式,需先转换成 RGBA 颜色格式,此处省略// 转换成 RGBA 后的图片帧缓存在 frame 中croped_frame->width = w;croped_frame->height = h;croped_frame->format = frame->format;av_image_alloc(croped_frame->data, croped_frame->linesize, croped_frame->width, croped_frame->height, (AVPixelFormat)croped_frame->format, 1);uint8_t * p = nullptr;for (int i = 0; i < 8 && frame->linesize[i] > 0; i++) {// p = frame->data[i];// p += y * frame->linesize[i];// p += (frame->linesize[i] / frame->width) * x;// croped_frame->linesize[i] = (frame->linesize[i] / frame->width) * w;// av_image_copy_plane(croped_frame->data[i], croped_frame->linesize[i], p, frame->linesize[i], croped_frame->linesize[i], croped_frame->height);frame->data[i] += y * frame->linesize[i];frame->data[i] += (frame->linesize[i] / frame->width) * x;croped_frame->linesize[i] = (frame->linesize[i] / frame->width) * w;av_image_copy_plane(croped_frame->data[i], croped_frame->linesize[i], frame->data[i], frame->linesize[i], croped_frame->linesize[i], croped_frame->height);}av_frame_free(&frame);frame = croped_frame;croped_frame = nullptr;return true;
}

现在看起来,这个 crop 方法,可能存在一定的内存泄漏啊。因为 data[i]+= 操作移走了,av_frame_free 时估计会释放不完所有内存。因此,这里应该用一个临时变量 uint8_t * p 来代替其进行操作。由于是后期发现的 bug,因此代码中用 注释 进行标记。
for循环里,i < 8 是因为目前 FFmpeg 结构体中的 AVFrame->data[8] 是个长度为 8 的指针数组。当然,也得保证 frame->linesize[i] > 0 才行,其实这一条件,在这里,决定了,它只会执行一次,因为 RGBA 颜色格式的 frame->linesize[1] = 0
假设我们有一张位图,当前指针指向位图的 (0,0) 位置,第一句,frame->data[i] += y * frame->linesize[i]; 是将指针纵向向下移动 y 行。这时,指针来到了 (0,y - 1)位置
第二句 frame->data[i] += (frame->linesize[i] / frame->width) * x; 这里面,() 中的表达式计算了一个像素的字节数,然后乘上 x,就是讲指针向右移动 x 列,目前,指针来到了 (x - 1,y - 1) 位置。然后调用 ffmpeg 的接口 av_image_copy_plane 即可。

测试

在一台 linux 机器上,使用了一张 5.76MB 的 jpg 图片进行了简单的测试。
加载 100 次用时 15 秒,而使用 ImageMagic 则用时 29 秒。
使用 FFmpeg 的 swscale 对图片进行缩放,从 5k5k 到 1k1k,反复 100 次,用时 8 秒。使用 ImageMagic 反复缩放 10 次即用时 18 秒。
实现了一个 crop 方案,crop 50 次,用时 1 秒,而 ImageMagic 用时 2 秒。

结语

虽然 FFmpeg 对图片进行加载、处理,比 ImageMagic 和 stb_image 快不少。但是,FFmpeg 毕竟比较复杂,而且相关依赖更多。具体取舍还得三思。
我们是刚好工程中有音视频模块,本就用到 FFmpeg,所以,既杀牛又杀鸡的,就用一把刀也无所谓了。而且我们需要反复加载多张图片,如果只加载一次图片(例如之前实现的安全围栏的纹理素材),则 hpp 的 stb_image 是最好最简单的选择了。只需一个头文件,和一个接口,即可实现。

【AVD】杀鸡用牛刀,FFmpeg API 加载存储图片,比 ImageMagic 和 stb_image 快多了相关推荐

  1. 201203-4-1-兄弟组件之间通信,杀鸡不用牛刀

    1$emit,.y $emit进行处理,一般逻辑比较复杂 vuex 杀鸡用牛刀??? 使用集成的中央数据总线.

  2. Python装饰器——四两拨千斤还是杀鸡用牛刀?

    一.引言 最近做了一个小小小项目,写了一些偏工程的代码.项目的目的看起来很简单,就是去组里的一个能显示调试信息网站上,根据我们提供的一堆查询,获取调试信息的response,然后离线的解析来完成后续实 ...

  3. 杀鸡用牛刀:Sketch流程图绘制体验

    程序流程图是改进工作方法的有效工具.不论作业研究过程中采用何种技术,流程程序图总是必不可少的一步,是应用最普遍的一种工具.绘制流程图的工具很多,Word.ppt.Visio.ProcessOn.XMi ...

  4. 杀鸡用用牛刀 scrapy框架爬取豆瓣电影top250信息

    文章目录 一.分析网页 二.scrapy爬虫 三.处理数据 原文链接:https://yetingyun.blog.csdn.net/article/details/108282786 创作不易,未经 ...

  5. 【小学递归】杀鸡用牛刀——要用递归啊!

    背景:     哈哈!我们终于学了递归了,现在大家一定感到非常有意思吧,那个典型的"汉诺塔"问题,一个非常短的程序居然可以完成如此复杂的工作,真是神奇啊!来吧,让我们也动手编写一个 ...

  6. ArcGIS JS API加载GeoServer发布的WFS服务

    文章目录 前言 主要代码 总结 参考链接 前言 WFS(Web Feature Service),OGC标准下的要素服务.其支持的主要操作如下: GetCapabilities (discovery ...

  7. ios 高德地图加载瓦片地图_IOS 高德地图 API 加载 WMS 服务

    IOS 高德地图 API 加载 WMS 服务 本文主要介绍通过自定义高德地图 MATileOverlay 接口,添加 WMS 服务到地图上.废话少说,先贴代码. 代码 自定义类 WMSTileOver ...

  8. ArcGIS Javascript API 加载高德在线地图扩展

    利用ArcGIS JavaScript API加载高德在线地图的扩展 /*** Created by WanderGIS on 2015/7/15.*/ define(["dojo/_bas ...

  9. android 百分比loading,牛逼的loading加载效果

    牛逼的loading加载效果 介绍: AnimatedCircleLoadingView一个不错的loading加载效果,自定义AnimatedCircleLoadingView设置startDete ...

最新文章

  1. c语言 求sin近似值,用泰勒公式求sin(x)的近似值
  2. Android移动端音视频的快速开发教程(十)
  3. mac bash file密码_Mac系统 | 菜鸟程序员项目模拟数据迁移,会安装Mysql服务端吗
  4. dota是java中的_用java开发dota英雄最华丽的技能(实例讲解)
  5. Bootstrap组件_路径导航,标签,徽章
  6. 多线程异步处理:AsyncTask异步更新UI界面(详细完整总结篇)
  7. 计算机lab模式适用于,计算机考证二级选择题1
  8. 在阿里云服务器中安装配置mysql数据库完整教程
  9. jQuery过滤选择器 通过过滤条件选取需要的元素
  10. Kali linux 渗透测试(五)——渗透WPS攻击
  11. Android音频之多设备同时输出-cast通路分析
  12. 第一个nanomsg的程序
  13. 柱状堆积图(论文绘制)
  14. php开发支付宝支付密码忘记了怎么办_php开发支付宝支付密码忘记了怎么办_玩机小技巧:OPPO手机忘记锁屏密码怎么办?......
  15. OMCS 语音视频框架
  16. 在UniApp的H5项目中,生成二维码和扫描二维码的操作处理
  17. 如何选择一台适合个人使用的云服务器?
  18. Androidstudio ADB调试
  19. 产品经理入门03:需求评审和技术评审
  20. 1、Java好的书籍

热门文章

  1. React 异步加载组件
  2. php .asmx,PHP应用:php实现通过soap调用.Net的WebService asmx文件
  3. TiDB 重要监控指标详解
  4. linux多线程同步概览
  5. 李开复写给大学生的一封信
  6. sql嵌套查询时避免报错的方式
  7. java 图形用户界面
  8. Ci2451无线MCU芯片2.4GHz射频芯片集成8位RISC内核集成无线收发器和8位RISC(精简指令集)MCU的SOC芯片
  9. Bluetooth 蓝牙介绍(一) :基础知识
  10. 全百科搜索采集器 可采集百度搜索网址/贴吧/哔哩哔哩/微博信息