音视频编解码之路:JPEG编码

音视频编解码之路:JPEG编码

本文首发于音视频编解码之路:JPEG编码

前言

本篇是新开坑的 音视频编解码之路 的第一篇,这个系列主要通过书籍、网上的博文/代码等渠道,整理与各编码协议相关的资料和自己的理解,同时手撸下对应格式的“编解码器”,形成对真正编解码器的原理的基础认识,从而后续可以进一步研究真正意义上的编解码器如libx264的逻辑与优化。

之前在查找编解码的学习资料时,看到了韦神的经验之谈,因此就以JPEG的编码来开篇吧。

本篇整体脉络来自于手动生成一张JPEG图片,不过针对文章中的诸多细节做了补充和资料汇总,另外把代码也用C++和OOP的方式修改了下。范例工程位于avcodec_tutorial

编码步骤

基本系统的 JPEG 压缩编码算法一共分为 10 个步骤:

  1. 颜色模式转换
  2. 分块
  3. 离散余弦变换(DCT)
  4. 量化
  5. Zigzag 扫描排序
  6. DC 系数的差分脉冲调制编码(DPCM)
  7. DC 系数的中间格式计算
  8. AC 系数的游程编码(RLE)
  9. AC 系数的中间格式计算
  10. 熵编码

接下去我们将逐一介绍上述的各个步骤,并在其中穿插涉及的一些概念与实际代码。

颜色模式转换

JPEG 采用的是 YCbCr 颜色空间,这里不再赘述为啥选择YUV等等重复较多的内容,之前没有接触过的可以看下一文读懂 YUV 的采样与格式和其他资料来补补课。

颜色模式从RGB转为YUV的过程中可以把采样也一起做了,这里Demo采样按照YUV444也就是全采样不做额外处理的方式简单实现,代码如下:

uint8_t bound(uint8_t min, int value, uint8_t max) {
    if(value <= min) {
        return min;
    }
    if(value >= max) {
        return max;
    }
    return value;
}

bool JpegEncoder::rgbToYUV444(const uint8_t *r, const uint8_t *g, const uint8_t *b,
                              const unsigned int &w, const unsigned int &h,
                              uint8_t *const y, uint8_t *const u, uint8_t *const v) {
    for (int row = 0; row < h; row++) {
        for (int column = 0; column < w; column++) {
            int index = row * w + column;
            // rgb -> yuv 公式
            // 这里在实现的时候踩了个坑
            // 之前直接将cast后的值赋值给了y/u/v数组,y/u/v数组类型是uint8,计算出来比如v是256直接越界数值会被转成其他如0之类的值
            // 导致最后颜色效果错误
            int yValue = static_cast<int>(round(0.299 * r[index] + 0.587 * g[index] + 0.114 * b[index]));
            int uValue = static_cast<int>(round(-0.169 * r[index] - 0.331 * g[index] + 0.500 * b[index] + 128));
            int vValue = static_cast<int>(round(0.500 * r[index] - 0.419 * g[index] - 0.081 * b[index] + 128));
            // 做下边界容错
            y[index] = bound(0, yValue, 255);
            u[index] = bound(0, uValue, 255);
            v[index] = bound(0, vValue, 255);
        }
    }
    return true;
}

分块

由于后面的 DCT 变换需要对 8x8 的子块进行处理,因此在进行 DCT 变换之前必须把源图像数据进行分块。源图象经过上面的颜色模式转换采样后变成了 YUV 数据,所以需要分别对 Y U V 三个分量进行分块。具体分块方式为由左及右,由上到下依次读取 8x8 的子块,存放在长度为 64 的数组中,之后再进行 DCT 变换。

因为这个分块机制的原因,有些素材的宽高如果不是8的倍数的话,都需要在处理的时候进行补齐。

bool JpegEncoder::yuvToBlocks(const uint8_t *y, const uint8_t *u, const uint8_t *v,
                              const unsigned int &w, const unsigned int &h,
                              uint8_t yBlocks[][64], uint8_t uBlocks[][64], uint8_t vBlocks[][64]) {
    int wBlockSize = w / 8 + (w % 8 == 0 ? 0 : 1);
    int hBlockSize = h / 8 + (h % 8 == 0 ? 0 : 1);
    for (int blockRow = 0; blockRow < hBlockSize; blockRow++) {
        for (int blockColumn = 0; blockColumn < wBlockSize; blockColumn++) {
            int blockIndex = blockRow * wBlockSize + blockColumn; // 当前子块下标
            uint8_t *yBlock = yBlocks[blockIndex];
            uint8_t *uBlock = uBlocks[blockIndex];
            uint8_t *vBlock = vBlocks[blockIndex];
            for (int row = 0; row < 8; row++) {
                for (int column = 0; column < 8; column++) {
                    int indexInSubBlock = row * 8 + column; // 块中数据位置
                    int realPosX = blockColumn * 8 + column; // 在完整YUV数据中的X位置
                    int realPosY = blockRow * 8 + row; // 在完整YUV数据中的Y位置
                    int indexInOriginData = realPosY * w + realPosX; // 在原始数据中的位置
                    if (realPosX >= w || realPosY >= h) {
                        // 补齐数据
                        yBlock[indexInSubBlock] = 0;
                        uBlock[indexInSubBlock] = 0;
                        vBlock[indexInSubBlock] = 0;
                    } else {
                        yBlock[indexInSubBlock] = y[indexInOriginData];
                        uBlock[indexInSubBlock] = u[indexInOriginData];
                        vBlock[indexInSubBlock] = v[indexInOriginData];
                    }
                }
            }
        }

    }
    return true;
}

分块后的结果类似下面这样,假设源图像像素宽高为64x64,颜色转换并分块后将变成YUV三个通道,且每通道按8x8进行拆分:

2776275181-5e58dfe19ac57_articlex

DCT

JPEG 里要对数据压缩,就需要先要做一次 DCT 变换。数学方面的细节不是目前的重点,只需要知道这个变换是将数据域从时(空)域变换到频域,把图片里点和点间的规律呈现出来,是为了更方便后续的压缩的。

先贴一下公式,对数学原理感兴趣的话可以扩展看看JPEG编码&算术编码、LZW编码等资料:

2991864350-5e58dfe23e37c_articlex

DCT变换与图像压缩、去燥里面还讲到了为什么JPEG选择DCT而不选择DFT的原因。

再贴一下代码:

/********* 外部逻辑 *********/
bool JpegEncoder::blocksFDCT(const uint8_t (*yBlocks)[64], const uint8_t (*uBlocks)[64], const uint8_t (*vBlocks)[64],
                             const unsigned int &w, const unsigned int &h,
                             int yDCT[][64], int uDCT[][64], int vDCT[][64]) {
    int wBlockSize = w / 8 + (w % 8 == 0 ? 0 : 1);
    int hBlockSize = h / 8 + (h % 8 == 0 ? 0 : 1);
    int blockSize = wBlockSize * hBlockSize;
    std::shared_ptr<JpegFDCT> fdct = std::make_shared<JpegFDCT>();
    for (int blockIndex = 0; blockInd
  • 0
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值