购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

4.2
JPEG图像

本节介绍FFmpeg对JPEG图像的处理办法,首先描述为什么引入JPEG格式,以及JPEG格式有哪些种类;然后叙述如何把视频文件中的某帧画面保存为JPEG图像文件;最后阐述色彩空间YUV与YUVJ之间的区别,以及如何通过图像转换器把YUV格式转换为YUVJ格式,从而把视频帧保存为真实色彩的JPEG图像文件。

4.2.1 为什么要用JPEG格式

JPEG是一种图像格式编码标准,也是一种图像数据压缩标准,它的全称为Joint Photographic Experts Group,意思是联合图像专家组。JPEG标准于1992年由ISO(International Organization for Standardization,国际标准化组织)首次发布,它是长期以来广泛使用的计算机图像格式。采用JPEG格式的图像文件,其扩展名通常为.jpg或者.jpeg。

前面的4.1.1节提到,BT601、BT709、BT2020三个色度标准都包含数字电视版和全范围版两个版本,其中数字电视版的色度范围是[16,235],全范围版的色度范围是[0,255],全范围版属于JPEG格式的色彩空间,即YUV格式的JPEG变种,简称YUVJ,末尾的J代表JPEG。在FFmpeg框架中,视频帧的YUV图像默认采用数字电视版的色度标准,其像素格式为AV_PIX_FMT_YUV420P;而JPEG图像对应全范围版的色度标准,其像素格式为AV_PIX_FMT_YUVJ420P,也就是在YUV与420P之间多了个J。

JPEG在压缩图像时采用了差分预测编码调制(Differential Predictive Coding Modulation,DPCM)、离散余弦变换(Discrete Cosine Transform,DCT)以及熵编码(Entropy Coding)的联合编码算法,以去除冗余的图像数据。JPEG属于有损压缩格式,它的压缩比率会影响图像的保真程度,如果采用过高的压缩比率,JPEG图片经过解压后的图像质量将明显降低。不过由于JPEG标准发布较早,且压缩效率尚可,因此几乎所有的设备都支持JPEG图片,使得JPEG格式在互联网上大行其道。

根据图像的展示过程,可将JPEG格式分为标准JPEG、渐进JPEG、JPEG2000三种类型,分别说明如下。

(1)标准JPEG:该类型的JPEG图片根据加载进度从上到下、从左往右依序显示图像,直到所有图像数据加载完毕,才能看到整幅图片的全貌。标准JPEG的显示过程如图4-18和图4-19所示。

图4-18 标准JPEG正在加载

图4-19 标准JPEG即将加载完毕

(2)渐进JPEG:该类型的JPEG图片根据加载进度从模糊到清晰逐步显示图像,也就是先呈现出图像的粗略外观,再逐渐呈现出越来越清晰的图像。渐进JPEG的呈现过程如图4-20和图4-21所示。

图4-20 渐进JPEG正在加载

图4-21 渐进JPEG加载完毕

(3)JPEG2000:这是新一代的影像压缩标准,压缩品质更高,压缩性能比标准JPEG提高20%。JPEG2000图片的扩展名为.jp2,对应的MIME类型是image/jp2(传统JPEG的MIME类型是image/jpeg)。虽然JPEG2000的压缩效率得到提高,但是该标准的编解码算法被大量注册专利,因此极大地限制了它的应用场景。

4.2.2 把视频帧保存为JPEG图片

因为FFmpeg自带对JPEG图片编解码的mjpeg库,所以通过MJPEG编码器即可将数据帧压缩为JPEG图片。把视频画面转存为JPEG图片的过程主要有两个步骤:获取并打开MJPEG编码器实例,以及把数据帧重新编码并写入JPEG文件,分别说明如下。

1.获取并打开MJPEG编码器实例

与视频的编码器实例类似,MJPEG编码器实例的打开步骤也分为以下4步。

(1)调用avcodec_find_encoder函数获取代号为AV_CODEC_ID_MJPEG的MJPEG编码器。

(2)调用avcodec_alloc_context3函数分配MJPEG编码器对应的编码器实例。

(3)给MJPEG编码器实例的各字段赋值,包括pix_fmt(像素格式)、width(视频宽度)、height(视频高度)、time_base(时间基)等字段。由于JPEG的颜色空间为YUVJ,因此pix_fmt字段要填AV_PIX_FMT_YUVJ420P。

(4)调用avcodec_open2函数打开MJPEG编码器的实例。

根据以上步骤描述的MJPEG编码器打开过程,编写获取并打开MJPEG编码器实例的FFmpeg代码如下(完整代码见chapter04/savejpg.c):

    // 查找MJPEG编码器
    AVCodec *jpg_codec = (AVCodec*) avcodec_find_encoder(AV_CODEC_ID_MJPEG);
    if (!jpg_codec) {
        av_log(NULL, AV_LOG_ERROR, "jpg_codec not found\n");
        return -1;
    }
    // 获取编解码器上下文信息
    AVCodecContext *jpg_encode_ctx = avcodec_alloc_context3(jpg_codec);
    if (!jpg_encode_ctx) {
        av_log(NULL, AV_LOG_ERROR, "jpg_encode_ctx is null\n");
        return -1;
    }
    // JPG的像素格式是YUVJ。MJPEG编码器支持YUVJ420P/YUVJ422P/YUVJ444P等格式
    jpg_encode_ctx->pix_fmt = AV_PIX_FMT_YUVJ420P;                      // 像素格式
    jpg_encode_ctx->width = frame->width;                               // 视频宽度
    jpg_encode_ctx->height = frame->height;                             // 视频高度
    jpg_encode_ctx->time_base = (AVRational){1, 25};                    // 时间基
    ret = avcodec_open2(jpg_encode_ctx, jpg_codec, NULL);               // 打开编码器的实例
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Can't open jpg_encode_ctx.\n");
        return -1;
    }
2.把数据帧重新编码并写入JPEG文件

对数据帧重新编码的过程主要分为以下三步。

(1)调用avcodec_send_frame函数把原始的数据帧发给MJPEG编码器实例。

(2)调用avcodec_receive_packet函数从MJPEG编码器实例获取压缩后的数据包。

(3)调用av_write_frame函数往JPG文件写入压缩后的数据包。

注意上述的重新编码过程无须重复调用,只要执行一遍即可,因为JPEG格式属于静止的图像,仅包含一幅图片。下面是把视频帧保存为JPEG图片的FFmpeg代码片段。

    int packet_index = -1;  // 数据包的索引序号
    // 对视频帧解码。save_index表示要把第几个视频帧保存为图片
    int decode_video(AVPacket *packet, AVFrame *frame, int save_index) {
        // 把未解压的数据包发给解码器实例
        int ret = avcodec_send_packet(video_decode_ctx, packet);
        if (ret < 0) {
           av_log(NULL, AV_LOG_ERROR, "send packet occur error %d.\n", ret);
           return ret;
       }
       while (1) {
           // 从解码器实例获取还原后的数据帧
           ret = avcodec_receive_frame(video_decode_ctx, frame);
           if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
               break;
           } else if (ret < 0) {
               av_log(NULL, AV_LOG_ERROR, "decode frame occur error %d.\n", ret);
               break;
           }
           packet_index++;
           if (packet_index < save_index) {  // 还没找到对应序号的帧
               return AVERROR(EAGAIN);
           }
           save_jpg_file(frame, save_index);  // 把视频帧保存为JPEG图片
           break;
       }
       return ret;
    }

接着执行下面的编译命令:

     gcc savejpg.c -o savejpg -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat
-lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完成后,执行以下命令启动测试程序(默认保存首帧视频画面,即save_index为0)。

    ./savejpg ../fuzhous.mp4

程序运行完毕,发现控制台输出以下日志信息,说明完成了从视频帧到JPEG图片的转换操作。

    Success open input_file ../fuzhous.mp4.
    format = 0, width = 480, height = 270
    target image file is output_000.jpg
    Success save 0_index frame as jpg file.

最后打开看图软件可以正常浏览output_000.jpg,如图4-22所示,表示上述代码初步实现了把视频画面转存为JPEG图片的功能。

图4-22 把视频画面转存为JPEG图片

运行下面的ffmpeg命令也可以把指定的视频帧保存为JPG图片。命令中的-vframes 1表示输出一帧。

    ffmpeg -i ../fuzhous.mp4 -ss 00:00:10 -vframes 1 ff_capture.jpg

4.2.3 图像转换器

4.2.2节虽然把视频画面转存为JPEG图片,使用看图软件也能浏览该图片,但是好像图片的色彩比原视频淡了一些,而且不止首帧视频画面,其他视频帧转存为JPEG图片也都变淡了。这是什么缘故呢?原来JPEG采用的是YUVJ颜色空间,与视频采用的YUV颜色空间略有不同。YUVJ的色彩范围为0~255,其中0表示黑色,255表示白色;而YUV的色彩范围为16~235,其中16表示黑色,235表示白色。可见YUVJ的色彩范围大于YUV的色彩范围,且YUV的色彩范围仅是YUVJ色彩范围的子集。这意味着YUV的色彩能够放入YUVJ空间,因为[16,235]可被[0,255]所容纳;然而YUVJ色彩不能放入YUV空间,因为[0,255]超出了[16,235]的表达范围,像[0,15]和[236,255]这两个区间均位于[16,235]之外。

不过即使YUV的色彩能够放入YUVJ空间,并不意味着YUV的色彩精度没有损失,实际上由于YUV的色彩空间缺少[0,15]和[236,255]这两个区间,导致YUV色彩在YUVJ看起来不够浓。也就是说,白的不够白,黑的也不够黑,整幅图像的色调就变淡了,没有原来的浓度高了。这便是4.2.2节转存后的JPEG图片颜色变淡的原因。

要想解决JPEG转存后颜色变淡的问题,就得引入FFmpeg提供的图像转换器SwsContext,由图像转换器完成色彩空间的转换操作。SwsContext位于swscale库,它主要用来转换图像数据的像素格式、画面宽度和画面高度等,与之相关的函数说明如下:

· sws_getContext:分配图像转换器的实例。还需要在输入参数中分别指定来源和目标的画面宽度、画面高度、像素格式。

· sws_scale:图像转换器开始处理图像数据。该函数同时实现以下三项功能:图像色彩空间转换、画面分辨率缩放、前后图像滤波处理。

· sws_freeContext:释放图像转换器的实例。

引入图像转换器之后,把视频帧转存为JPEG图片的过程需要注意以下两点:通过图像转换器把数据帧转换为YUVJ420P格式,以及把第一步转换后的数据帧重新编码并写入JPEG文件,分别说明如下。

1.通过图像转换器把数据帧转换为YUVJ420P格式

图像转换器处理数据帧向YUVJ颜色空间的转换操作主要包含以下5个步骤:

01 调用sws_getContext函数分配图像转换器的实例,并分别指定来源和目标的宽度、高度、像素格式,这里的像素格式采用AV_PIX_FMT_YUVJ420P。

02 调用av_frame_alloc函数分配一个YUVJ数据帧,并设置YUVJ帧的像素格式、视频宽度和视频高度。

03 调用av_image_alloc函数分配缓冲区空间,用于存放转换后的图像数据。

04 调用sws_scale函数,命令转换器开始处理图像数据,把YUV图像转为YUVJ图像。

05 转换结束,调用sws_freeContext函数释放图像转换器的实例。

根据以上步骤描述的YUV空间到YUVJ空间的转换过程,编写FFmpeg的颜色空间转换代码(完整代码见chapter04/savejpg_sws.c)。

    enum AVPixelFormat target_format = AV_PIX_FMT_YUVJ420P;  // JPG的像素格式是YUVJ420P
    // 分配图像转换器的实例,并分别指定来源和目标的宽度、高度、像素格式
    struct SwsContext *swsContext = sws_getContext(
            frame->width, frame->height, AV_PIX_FMT_YUV420P,
            frame->width, frame->height, target_format,
            SWS_FAST_BILINEAR, NULL, NULL, NULL);
    if (swsContext == NULL) {
        av_log(NULL, AV_LOG_ERROR, "swsContext is null\n");
        return -1;
    }
    AVFrame *yuvj_frame = av_frame_alloc();                   // 分配一个YUVJ数据帧
    yuvj_frame->format = target_format;                       // 像素格式
    yuvj_frame->width = frame->width;                         // 视频宽度
    yuvj_frame->height = frame->height;                       // 视频高度
    // 分配缓冲区空间,用于存放转换后的图像数据
    av_image_alloc(yuvj_frame->data, yuvj_frame->linesize,
            frame->width, frame->height, target_format, 1);
    // 转换器开始处理图像数据,把YUV图像转为YUVJ图像
    sws_scale(swsContext, (const uint8_t* const*) frame->data, frame->linesize,
        0, frame->height, yuvj_frame->data, yuvj_frame->linesize);
    sws_freeContext(swsContext);                              // 释放图像转换器的实例
2.把第一步转换后的数据帧重新编码并写入JPEG文件

对数据帧重新编码的过程主要分为以下列3步。

(1)调用avcodec_send_frame函数把前一步转换后的YUVJ数据帧发给MJPEG编码器实例。

(2)调用avcodec_receive_packet函数从MJPEG编码器实例获取压缩后的数据包。

(3)调用av_write_frame函数往JPG文件写入压缩后的数据包。

根据以上步骤描述的数据帧重新编码过程,编写FFmpeg的JPEG图片转存代码。

    int packet_index = -1;                    // 数据包的索引序号
    // 对视频帧解码。save_index表示要把第几个视频帧保存为图片
    int decode_video(AVPacket *packet, AVFrame *frame, int save_index) {
        // 把未解压的数据包发给解码器实例
        int ret = avcodec_send_packet(video_decode_ctx, packet);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "send packet occur error %d.\n", ret);
            return ret;
        }
        while (1) {
            // 从解码器实例获取还原后的数据帧
            ret = avcodec_receive_frame(video_decode_ctx, frame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                break;
            } else if (ret < 0) {
               av_log(NULL, AV_LOG_ERROR, "decode frame occur error %d.\n", ret);
               break;
           }
           packet_index++;
           if (packet_index < save_index) {                  // 还没找到对应序号的帧
               return AVERROR(EAGAIN);
           }
           save_jpg_file(frame, save_index);                 // 把视频帧保存为JPEG图片
           break;
       }
       return ret;
    }

接着执行下面的编译命令:

     gcc savejpg_sws.c -o savejpg_sws -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib
-lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完成后,执行以下命令启动测试程序(默认保存首帧视频画面,即save_index为0)。

    ./savejpg_sws ../fuzhous.mp4

程序运行完毕,发现控制台输出以下日志信息,说明完成了从视频帧到JPEG图片的转换操作。

    Success open input_file ../fuzhous.mp4.
    format = 0, width = 480, height = 270
    target image file is output_000.jpg
    Success save 0_index frame as jpg file.

最后打开看图软件可以正常浏览output_000.jpg,并且该图片与视频的首帧画面拥有相同的色彩,如图4-23所示,表明上述代码正确实现了把视频画面转存为JPEG图片的功能。

图4-23 像素格式转换后的JPEG图片 VIKmlft8Vi3S7Mt8p1hgYffDZ27S4tj+YRUcE6ejYdkOsKmi4tvewa/BqYKGPv7C

点击中间区域
呼出菜单
上一章
目录
下一章
×