unsigned long输出格式_刻意练习FFmpeg系列:最简单的图像格式PGM灰度图

7e8b57bd65c3c24955a8a683f6401593.png

前言

本专题之所以说是「整体性学习」,是因为最好的学习的过程,是先看到大体的轮廓,再逐步探索细节。就像渐进式jpeg图像加载,从模糊的全局图到清晰的全局图。

e22a829a0ed576bbde7967300134bd6e.png
先有个简单的全局认知,再有深入细致的认知

前面两篇文章都是概念性的解释,这篇文章,我们要下场翻代码了,Let's Do it!

kamuel:刻意练习FFmpeg系列:音视频基础概念格式和编码​zhuanlan.zhihu.com
8ff3682e65f07094af5a393475fb8a61.png
kamuel:刻意练习FFmpeg系列:颜色和像素​zhuanlan.zhihu.com
8ff3682e65f07094af5a393475fb8a61.png

本文将用一个最简单的图像格式来从整体性地探讨FFmpeg的流程和框架。在开始后面的阅读之前,先带着个疑问:FFmpeg怎么知道应该用哪个格式解封装器Demuxer的?

788b72fc09c7ff6c0b566b4778ab11c7.png

什么是PGM灰度图

PGM是一个可以用文本直接描述灰度图的图像格式,是Netpbm图像处理开源库中使用并定义的几种图形格式之一,Netpbm还包括PPM(可以是RGB彩色图)和PBM等几种,有时也统称PNM。

直接举个例子就明白了,下面这个文本,复制保存为demo.pgm,就可以是一张图像了。

P2 # 表明文件类型的Magic Number
# 可以加注释,这张图就是现实 "KAMU"四个字母
25 7 # 宽度和高度
15 # 色彩范围
# 下面是图像的像素,每个数字是一个像素
0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
0  9  0  0  9  0  0  0  7  7  0  0  0 11 11 11 11 11  0  0 15  0  0 15  0
0  9  0  9  0  0  0  7  0  0  7  0  0 11  0 11  0 11  0  0 15  0  0 15  0
0  9  9  0  0  0  0  7  7  7  7  0  0 11  0 11  0 11  0  0 15  0  0 15  0
0  9  0  9  0  0  0  7  0  0  7  0  0 11  0 11  0 11  0  0 15  0  0 15  0
0  9  0  0  9  0  0  7  0  0  7  0  0 11  0 11  0 11  0  0 15 15 15 15  0
0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0

可以明显看到,PGM文件分为几部分,第一部分是文件头,第二部分是灰度的像素信息。

文件头包括:P2是表明这是PGM图像,25和7是图像尺寸的宽和高,15 是灰度色彩范围。

文件头之后是图像内容,是一系列的像素,每个数字代表一个像素,数字越大,越靠近白色,数字越小,越靠近黑色。

PNM系列图像还可以添加用#符号添加注释,非常方便理解。

如果用Mac OS,previewer可以直接打开,效果是这样:

6b5c0d2501ba27bc746c20a3b10ba85e.png
用预览打开pgm灰度图

尺寸太小了,放大效果是这样

16aef0680ebfc0798682c001d94078dd.png
放大的pgm灰度图

FFmpeg的下载和编译

FFmpeg的编译指南很多,本文只简单介绍,方便没有编译过的同学直接编译,因为后面我们需要进行一些调试工作。

首先用git下载

git clone -b n4.2.2 --depth=1 https://git.ffmpeg.org/ffmpeg.git

然后执行 configure 和 make,这里开启调试,并为了方便禁用OpenSSL。

./configure --enable-debug=3 --disable-optimizations 
--disable-stripping --disable-openssl

make -j 4 # 4个线程并行编译
make doc/examples/avio_reading #编译示例

LLDB调试的简单入门

lldb启动调试

lldb -- ffplay_g ~/Works/media/images/demo.pgm

添加断点

(lldb) b main
Breakpoint 1: 24 locations.

控制运行

(lldb) r #运行 run
(lldb) n #下一行代码 next 
(lldb) s #深入调用函数 step in

查看当前代码

(lldb) list
   68
   69  	    if (argc != 2) {
   70  	        fprintf(stderr, "usage: %s input_filen"
   71  	                "API example program to show how to read from a custom buffer "
   72  	                "accessed through AVIOContext.n", argv[0]);
   73  	        return 1;
   74  	    }

输出变量

(lldb) p argc
(int) $0 = 2

准备工作做完,可以开始调试FFmpeg是怎么打开一个图像格式的了。

FFmpeg打开PGM图像过程

解封装器Demuxer

之前我们介绍到,FFmpeg打开一个格式,是使用demuxer,也就是AVInputFromat。它的关键结构是这样的:

typedef struct AVInputFormat {
...
    int(* read_probe )(const AVProbeData *)
    int(* read_header )(struct AVFormatContext *)
    int(* read_packet )(struct AVFormatContext *, AVPacket *pkt)
    int(* read_close )(struct AVFormatContext *)
...
}

最主要的是这几种函数

  • 试探文件格式 read_probe()
  • 读取文件头 read_header()
  • 读取数据包 read_packet()
  • 关闭读取 read_close()
  • 还有其他函数,暂时不关心

一个demuxer的具体例子

// 见 libavformat/mpeg.c

AVInputFormat ff_vobsub_demuxer = {
    .name           = "vobsub",
    .long_name      = NULL_IF_CONFIG_SMALL("VobSub subtitle format"),
    .priv_data_size = sizeof(MpegDemuxContext),
    .read_probe     = vobsub_probe,
    .read_header    = vobsub_read_header,
    .read_packet    = vobsub_read_packet,
    .read_seek2     = vobsub_read_seek,
    .read_close     = vobsub_read_close,
    .flags          = AVFMT_SHOW_IDS,
    .extensions     = "idx",
    .priv_class     = &vobsub_demuxer_class,
};

PGM Demuxer

其实判断是什么格式,主要就是通过read_probe()来实现的。那么PGM图像的demuxer在哪里呢?我们可以直接搜索pgm。

➜  ffmpeg4.2.2 git:(4.2.2) grep --include="*.c" pgm libavformat -rin
...
libavformat/img2dec.c:955:static int pgm_probe(const AVProbeData *p)
...

可以看到有pgm_probe()函数,就是它了。

开始调试

如果使用ffmpeg来把pgm转换为bmp,可以使用下面的代码。如果你的电脑不支持预览pgm,转换为bmp后,你就可以预览了。而且这是没任何失真的。

./ffmpeg_g -i /Works/media/images/demo.pgm /tmp/demo.bmp

然后可以用lldb调试这个过程

➜  ffmpeg4.2.2 git:(4.2.2) lldb -- ffmpeg_g -i /Works/media/images/demo.pgm /tmp/demo.bmp
(lldb) target create "ffmpeg_g"
Current executable set to '/Works/cpp/ffmpeg4.2.2/ffmpeg_g' (x86_64).
(lldb) settings set -- target.run-args  "-i" "/Works/media/images/demo.pgm" "/tmp/demo.bmp"

直接设置断点pgm_probe()

(lldb) b pgm_probe
Breakpoint 1: where = ffmpeg_g`pgm_probe + 12 at img2dec.c:957:26, address = 0x000000010043decc

r执行,我们就能进入到pgm_probe()函数了

(lldb) r
Process 7766 launched: '/Works/cpp/ffmpeg4.2.2/ffmpeg_g' (x86_64
...
Process 7766 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: . ffmpeg_g`pgm_probe(p=.) at img2dec.c:957:26
   954
   955 	static int pgm_probe(const AVProbeData *p)
   956 	{
-> 957 	    int ret = pgmx_probe(p);
   958 	    return ret && !av_match_ext(p->filename, "pgmyuv") ? ret : 0;
   959 	}
   960
Target 0: (ffmpeg_g) stopped

bt 看看调用栈

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
  * frame #0: . ffmpeg_g`pgm_probe(p=.) at img2dec.c:957:26
    frame #1: . ffmpeg_g`av_probe_input_format3(pd=., is_opened=1, score_ret=.) at format.c:165:21
    frame #2: . ffmpeg_g`av_probe_input_format2(pd=., is_opened=1, score_max=.) at format.c:208:37
    frame #3: . ffmpeg_g`av_probe_input_buffer2(pb=., fmt=., filename="/images/demo.pgm", logctx=., offset=0, max_probe_size=1048576) at format.c:280:16
    frame #4: . ffmpeg_g`init_input(s=., filename="/images/demo.pgm", options=.) at utils.c:443:12
    frame #5: . ffmpeg_g`avformat_open_input(ps=., filename="/images/demo.pgm", fmt=., options=.) at utils.c:573:16
    frame #6: . ffmpeg_g`open_input_file(o=., filename="/images/demo.pgm") at ffmpeg_opt.c:1104:11
    frame #7: . ffmpeg_g`open_files(l=., inout="input", open_file=(ffmpeg_g`open_input_file at ffmpeg_opt.c:998)) at ffmpeg_opt.c:3275:15
    frame #8: . ffmpeg_g`ffmpeg_parse_options(argc=4, argv=.) at ffmpeg_opt.c:3315:11
    frame #9: . ffmpeg_g`main(argc=4, argv=.) at ffmpeg.c:4872:11
    frame #10: . libdyld.dylib`start + 1
    frame #11: . libdyld.dylib`start + 1

这里只需要留意av_probe_input_format3() ,代码是如下。FFmpeg通过av_demuxer_iterate() 把所有的demuxer解封装器都列出来了,然后循环逐个试探格式得分fmt1->read_probe(),得分最高的解封装器会被返回。

ff_const59 AVInputFormat *av_probe_input_format3(ff_const59 AVProbeData *pd, int is_opened, int *score_ret)
{
    ...
    while ((fmt1 = av_demuxer_iterate(&i))) {
        ...
        score = 0;
        if (fmt1->read_probe) {
            score = fmt1->read_probe(&lpd);
            ...
    }

    if (score > score_max) {
        score_max = score;
        fmt = (AVInputFormat*) fmt1; 
    } else if (score == score_max)
        fmt = NULL;
    }
    ...
    return fmt;
}

继续看pgm_probe(),因为这里给出了PGM图像的试探得分。主要就是通过Magic Number来判断的,也就是看有无P2 P3 P4 等开头的两个魔术字符,

static int pgm_probe(const AVProbeData *p)
{
    int ret = pgmx_probe(p);
    return ret && !av_match_ext(p->filename, "pgmyuv") ? ret : 0;
}

static inline int pgmx_probe(const AVProbeData *p)
{
    return pnm_magic_check(p, 2) || pnm_magic_check(p, 5) ? pnm_probe(p) : 0;
}

static int pnm_magic_check(const AVProbeData *p, int magic)
{
    const uint8_t *b = p->buf;
    return b[0] == 'P' && b[1] == magic + '0';
}

通过更改lldb的调试堆栈frame,可以向上找到调用pgm_probe()的调用方 av_probe_input_format3()

(lldb) up
frame #1: . ffmpeg_g`av_probe_input_format3(pd=., is_opened=1, score_ret=.) at format.c:165:21
   162 	            continue;
   163 	        score = 0;
   164 	        if (fmt1->read_probe) {
-> 165 	            score = fmt1->read_probe(&lpd);
   166 	            if (score)
   167 	                av_log(NULL, AV_LOG_TRACE, "Probing %s score:%d size:%dn", fmt1->name, score, lpd.buf_size);
   168 	            if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions)) {

这时候试着把fmt1的名字打印出来,可以看到pgm_pipe

(lldb) p fmt1->name
(const char *const) $4 = ... "pgm_pipe"

pgm_pipelibavformat/allformats.c 搜索,可以看到demuxer的命名是ff_image_pgm_pipe_demuxer

extern AVInputFormat  ff_image_pgm_pipe_demuxer;

啊哈,原来FFmpeg是通过这个方式来判断文件格式的!

最后,让我们把ff_image_pgm_pipe_demuxer的定义找出来,其实是在 libavformat/img2dec.c

// libavformat/img2dec.c

#define IMAGEAUTO_DEMUXER(imgname, codecid)
static const AVClass imgname ## _class = {
    .class_name = AV_STRINGIFY(imgname) " demuxer",
    .item_name  = av_default_item_name,
    .option     = ff_img2pipe_options,
    .version    = LIBAVUTIL_VERSION_INT,
};
AVInputFormat ff_image_ ## imgname ## _pipe_demuxer = {
    .name           = AV_STRINGIFY(imgname) "_pipe",
    .long_name      = NULL_IF_CONFIG_SMALL("piped " AV_STRINGIFY(imgname) " sequence"),
    .priv_data_size = sizeof(VideoDemuxData),
    .read_probe     = imgname ## _probe,
    .read_header    = ff_img_read_header,
    .read_packet    = ff_img_read_packet,
    .priv_class     = & imgname ## _class,
    .flags          = AVFMT_GENERIC_INDEX, 
    .raw_codec_id   = codecid,
};

IMAGEAUTO_DEMUXER(pgm,     AV_CODEC_ID_PGM)

通过探索代码,我们终于窥探到了FFmpeg处理多媒体文件的思路。

小结:

本文通过详细介绍FFmpeg打开PGM图像的流程,解答了FFmpeg是如何判断文件的「格式Format」这个疑问。

下一篇,我们将继续通过PGM图像来介绍FFmpeg的解码过程,不要错过哦。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值