自学内容网 自学内容网

深入学习JPEG压缩原理与过程

        最近在研究数字水印技术,其中图像隐写技术是很重要的内容。而图像隐写技术中,如何抵抗JPEG质量压缩、尺寸缩放是比较多人研究的方向之一。最近我也在学习这方面的知识,但是学着学着,发现还是要深入理解一下JPEG的压缩过程才行,所以整理了下面学习内容。

一、JPEG压缩概述

        JPEG(Joint Photographic Experts Group)是一种有损压缩标准,广泛应用于数码照片和复杂图像的存储与传输。与无损压缩不同,有损压缩通过舍弃部分图像细节来显著减少文件大小,但在视觉上对图像质量的影响较小。这使得JPEG成为网络图像、数码摄影等领域的首选压缩格式。

        举个实际的例子,我们在手机上拍摄一张高分辨率的风景照片,未经压缩的图像可能占用数兆字节(MB)的存储空间。然而,当您将这张照片分享至社交媒体时,系统会自动将其压缩为几百KB的文件,既节省了存储空间,又确保了快速的上传和加载速度。这一过程中,JPEG压缩技术发挥了关键作用。

        JPEG 是一种有损图像压缩技术。

二、JPEG压缩的基本步骤

        JPEG压缩过程包含多个步骤,每一步都对最终的压缩效果和图像质量产生重要影响。以下是主要步骤的详细解析:

  1. 颜色空间转换(RGB转YCbCr)
  2. 下采样(CbCr通道)
  3. 分块(8x8分块处理)
  4. DCT计算
  5. 量化
  6. ZigZag扫描
  7. 熵编码

1. 颜色空间转换

        颜色空间是用于描述颜色的方法,常见的有RGB和YCbCr。JPEG压缩的第一步是将图像从RGB颜色空间转换为YCbCr颜色空间

  • RGB颜色空间:由红(R)、绿(G)、蓝(B)三个通道组成,用于显示设备。
  • YCbCr颜色空间:将RGB分解为亮度(Y)和色度(Cb、Cr)分量,分离了图像的明暗信息和色彩信息。

        这种转换的原因在于,人眼对亮度信息(Y分量)比对色度信息(Cb和Cr分量)更敏感。通过分离这两部分信息,JPEG可以针对性地进行压缩,提高整体效率。

转换公式:

        

代码示例(Python):

import numpy as np

def rgb_to_ycbcr(image):
    transform_matrix = np.array([
        [0.299, 0.587, 0.114],
        [-0.1687, -0.3313, 0.5],
        [0.5, -0.4187, -0.0813]
    ])
    ycbcr = image.dot(transform_matrix.T) + np.array([0, 128, 128])
    return ycbcr

        这一步骤不仅为后续的压缩奠定了基础,也使得压缩更加符合人眼视觉特性。

2. 下采样(可选)

        由于人眼对色度信息(Cb和Cr)没有对亮度信息(Y)那么敏感,JPEG通常对色度分量进行下采样,即减少其分辨率,从而进一步减小数据量。

常见的下采样格式:

  • 4:4:4:无下采样,Y、Cb、Cr三者具有相同的分辨率。
  • 4:2:2:水平下采样一半,Y保持原分辨率,Cb、Cr变为原来的一半。
  • 4:2:0:水平和垂直各下采样一半,Y保持原分辨率,Cb、Cr变为原来的一共四分之一。

        举个例子,假设一幅图像的分辨率为800x600,采用4:2:0下采样后:

  • Y分量保持800x600。
  • Cb和Cr分量各自变为400x300。

代码示例(Python):

def downsample(ycbcr, factor=(2, 2)):
    Y, Cb, Cr = ycbcr[:,:,0], ycbcr[:,:,1], ycbcr[:,:,2]
    Cb_down = Cb[::factor[0], ::factor[1]]
    Cr_down = Cr[::factor[0], ::factor[1]]
    return Y, Cb_down, Cr_down

        下采样不仅减少了数据量,还加快了后续处理的速度,但在高压缩率下可能引入色彩失真。因此,在选择下采样比例时需要权衡图像质量与压缩效率。

3. 分块

        将图像分割成多个8x8像素的小块是JPEG压缩中的关键步骤。这一设计选择既考虑了计算复杂度,又兼顾了压缩效率。

DCT计算复杂度与8x8块的选择

        离散余弦变换(DCT)是一种将图像从空间域转换到频域的数学变换,用于分离图像中的不同频率成分。然而,DCT的计算复杂度相对较高,特别是对大块图像进行DCT变换时,所需的计算资源和时间都会显著增加。

采用8x8块作为处理单元的原因如下:

  1. 计算效率:8x8的块大小在保持较低计算复杂度的同时,提供了足够的频率分辨率,使得低频和高频信息得以有效分离。
  2. 压缩效率:通过分块,DCT能够更有效地压缩局部图像特征,减少整体数据量。
  3. 视觉效果:8x8块大小在高压缩率下能够减轻块效应(block artifacts),保持图像的视觉连贯性。

        想象您在绘制一幅大画。如果将整个画布作为一个整体绘制,任何细微的调整都需要对整个画布进行大量修改,效率低下。而将画布分割成多个8x8的小块,您可以分别绘制和调整每一部分,既高效又灵活。

代码示例(Python):

def split_into_blocks(channel, block_size=8):
    h, w = channel.shape
    blocks = []
    for i in range(0, h, block_size):
        for j in range(0, w, block_size):
            block = channel[i:i+block_size, j:j+block_size]
            if block.shape == (block_size, block_size):
                blocks.append(block)
    return blocks

        通过将图像分割为8x8块,程序员可以更直观地处理每个块的压缩与解压缩,提高算法的模块化和可维护性。

4. 离散余弦变换(DCT)

        离散余弦变换(DCT)是JPEG压缩中的核心步骤,将每个8x8像素块从空间域(像素值)转换到频域(频率成分)。DCT能够将图像的信息分解为不同频率的分量,帮助在后续步骤中有效地压缩图像数据。

        低频、中频、高频的定义与作用

        经过DCT变换后,每个8x8块会生成64个DCT系数。这些系数根据其在8x8矩阵中的位置,可以分为低频、中频和高频:

  • 低频系数(Top-left部分):

    • 代表的内容:图像的基本形状和颜色渐变,主要决定了图像的整体亮度和粗略细节。
    • 作用:低频信息对人眼视觉的重要性较高,因此在量化过程中通常保留更多的低频系数,以保持图像的主要结构和色彩。
  • 中频系数(中间区域):

    • 代表的内容:图像的中等细节,如纹理和边缘。
    • 作用:中频信息对图像的局部细节和纹理影响明显,适度保留这些系数能够在保持图像质量的同时实现有效压缩。
  • 高频系数(Bottom-right部分):

    • 代表的内容:细小细节和噪声,如图像的微小纹理和杂色。
    • 作用:高频信息对图像的视觉影响较小,容易被人眼忽略。因此,在量化过程中,这些系数可以被大幅度简化甚至置零,从而实现更高的压缩率而几乎不影响视觉质量。

示意图:

dct_order = [
        [01, 02, 06, 07, 15, 16, 28, 29],
        [03, 05, 08, 14, 17, 27, 30, 43],
        [04, 09, 13, 18, 26, 31, 42, 44],
        [10, 12, 19, 25, 32, 41, 45, 53],
        [11, 20, 24, 33, 40, 46, 52, 54],
        [21, 23, 34, 39, 47, 51, 55, 60],
        [22, 35, 38, 48, 50, 56, 59, 61],
        [36, 37, 49, 57, 58, 62, 63, 64]
    ]

 上面示意图中,值越小,频率越低。

低中高频参考示意图:

        通过将不同频率的信息分离,DCT使得后续的量化和编码步骤能够有针对性地处理不同类型的数据,实现高效压缩。

DCT计算公式

其中,

代码示例(Python,使用库):

from scipy.fftpack import dct

def apply_dct(block):
    return dct(dct(block.T, norm='ortho').T, norm='ortho')

自定义DCT实现(Python):

import numpy as np

def custom_dct(block):
    N = 8
    dct_matrix = np.zeros((N, N))
    for u in range(N):
        for v in range(N):
            sum_val = 0.0
            for x in range(N):
                for y in range(N):
                    sum_val += block[x][y] * \
                               np.cos(((2*x + 1) * u * np.pi) / (2 * N)) * \
                               np.cos(((2*y + 1) * v * np.pi) / (2 * N))
            alpha_u = 1 / np.sqrt(2) if u == 0 else 1
            alpha_v = 1 / np.sqrt(2) if v == 0 else 1
            dct_matrix[u][v] = 0.25 * alpha_u * alpha_v * sum_val
    return dct_matrix

        理解DCT的计算过程有助于程序员优化算法性能,特别是在处理大规模图像数据时。

5. 量化

        量化是JPEG压缩中最关键的有损步骤,通过减少DCT系数的精度来实现数据压缩。量化过程利用预定义的量化表,根据人眼对不同频率信息的敏感度,舍弃一些高频信息。

标准量化表

        JPEG标准定义了两种主要的量化表:亮度量化表(Luminance Quantization Table)色度量化表(Chrominance Quantization Table)。这些表经过优化设计,考虑了人眼对不同频率信息的敏感度,低频信息对视觉影响较大,而高频信息则相对不敏感。

标准亮度量化表:

标准色度量化表:

解释:

  • 低频区域(左上角):量化值较小,表示对这些频率成分保留更多细节。
  • 高频区域(右下角):量化值较大,允许较多信息丢弃,以减少数据量。
质量因子与量化表调整

        用户在保存JPEG图像时,可以选择不同的质量因子(Quality Factor, Q),通常在1到100之间。质量因子的不同会影响量化表的缩放,从而调节图像的压缩率和质量。

质量因子与缩放因子
  • 质量因子 Q < 50

  • 质量因子 Q > 50

  • 量化表缩放公式:

        其中,T(i,j)  是标准量化表的元素,T'(i,j)  是缩放后的量化值。

限制:

  • 缩放后的量化值通常被限制在1到255之间,以避免过度量化(量化值过小)或不足量化(量化值过大)。

质量因子与量化表调整步骤:

  1. 确定质量因子 Q:用户选择一个质量设置,通常在1到100之间。
  2. 计算缩放因子
    • 如果 Q< 50,则 Scale = 5000 / Q;
    • 如果 Q > 50,则 Scale = 200 - 2 * Q;
  3. 缩放标准量化表
    • 对于每个量化表元素 T(i,j) 
    • 裁剪:保证 1≤ T(i,j) ≤255
  4. 应用缩放后的量化表:使用缩放后的量化表 Ti,j′T'_{i,j}Ti,j′​ 进行量化操作

代码示例(Python):

import numpy as np

def generate_quantization_table(standard_table, quality):
    if quality < 1:
        quality = 1
    elif quality > 100:
        quality = 100

    if quality < 50:
        scale = 5000 / quality
    else:
        scale = 200 - 2 * quality

    scaled_table = []
    for row in standard_table:
        scaled_row = []
        for element in row:
            scaled = round((element * scale) / 100)
            scaled = max(1, min(scaled, 255))  # Clamp between 1 and 255
            scaled_row.append(scaled)
        scaled_table.append(scaled_row)

    return np.array(scaled_table)

不同质量因子的量化效果
  • 高质量(Q > 75)
    • 量化表元素较小,保留更多低频信息。
    • 图像质量较高,压缩率较低。
  • 中等质量(50 ≤ Q ≤ 75)
    • 量化表元素适中,平衡图像质量与压缩率。
  • 低质量(Q < 50)
    • 量化表元素较大,更多高频信息被舍弃。
    • 图像质量较低,压缩率较高。

实际效果举例:

假设用户选择质量因子 Q = 75,以下是部分量化表元素的计算过程:

  • 原始亮度量化表值:16

  • 原始色度量化表值:17

上述结果显示,质量因子的调整使量化表中的值发生变化,从而影响DCT系数的量化处理。

量化示例代码

def quantize_channel(channel_blocks, quant_table):
    quantized_blocks = []
    for block in channel_blocks:
        dct_block = apply_dct(block - 128)  # JPEG uses shifted pixel values (-128 to 127)
        quant_block = quantize_block(dct_block, quant_table)
        quantized_blocks.append(quant_block)
    return quantized_blocks

解释:

  • 偏移像素值:在进行DCT之前,将像素值从0-255范围偏移到-128到127范围,以便DCT系数具有正负值,便于表示图像中的亮暗变化。
  • 量化过程:每个DCT系数除以对应量化表值并四舍五入,以减少精度,从而实现数据压缩。

三、实现Zig-Zag扫描

        Zig-Zag扫描将量化后的8x8 DCT系数按照特定的顺序排列成一个一维序列。这种扫描方式能有效地将大量的零值集中到序列的末尾,提高后续压缩算法的效率,特别是在高压缩率下,许多高频系数被量化为零,使得熵编码部分能更高效地压缩数据。

Zig-Zag扫描顺序

        Zig-Zag扫描的目的是为了将DCT系数中尽可能多的零聚集到序列的后部,充分利用高频系数被量化为零的特点,从而提升熵编码的压缩效率。

        以下是Zig-Zag扫描在8x8块中的遍历顺序:

dct_order = [
        [01, 02, 06, 07, 15, 16, 28, 29],
        [03, 05, 08, 14, 17, 27, 30, 43],
        [04, 09, 13, 18, 26, 31, 42, 44],
        [10, 12, 19, 25, 32, 41, 45, 53],
        [11, 20, 24, 33, 40, 46, 52, 54],
        [21, 23, 34, 39, 47, 51, 55, 60],
        [22, 35, 38, 48, 50, 56, 59, 61],
        [36, 37, 49, 57, 58, 62, 63, 64]
    ]

 

通过这种顺序,低频系数位于序列的前部,非零的系数集中在前部,而高频系数(多为零)则逐渐聚集到序列的后部。

Zig-Zag扫描示例代码

def zigzag_scan(block):
    zigzag_order = [
        (0,0),(0,1),(1,0),(2,0),(1,1),(0,2),(0,3),(1,2),
        (2,1),(3,0),(4,0),(3,1),(2,2),(1,3),(0,4),(0,5),
        (1,4),(2,3),(3,2),(4,1),(5,0),(6,0),(5,1),(4,2),
        (3,3),(2,4),(1,5),(0,6),(0,7),(1,6),(2,5),(3,4),
        (4,3),(5,2),(6,1),(7,0),(7,1),(6,2),(5,3),(4,4),
        (3,5),(2,6),(1,7),(2,7),(3,6),(4,5),(5,4),(6,3),
        (7,2),(7,3),(6,4),(5,5),(4,6),(3,7),(4,7),(5,6),
        (6,5),(7,4),(7,5),(6,6),(5,7),(6,7),(7,6),(7,7)
    ]
    return [block[i][j] for i,j in zigzag_order]

解释:

  • 目的: Zig-Zag扫描通过特定顺序排列DCT系数,使得大量的高频零值集中在序列的末尾。这种排列优化了后续的游程编码和霍夫曼编码的效率,因为连续的零值更容易被压缩。

  • 示例:
    假设一个量化后的8x8 DCT块如下:

Zig-Zag扫描后的一维序列:

16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,…,016, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \dots, 016,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,…,0

(后续系数中会有更多的0,特别是在高频部分)

这种排列极大地提高了压缩效率,因为大量连续的零值可以通过游程编码(RLE)更有效地压缩,减少了最终的存储和传输所需的数据量。

四、熵编码详解

        熵编码是JPEG压缩的最后一步,旨在进一步压缩数据。通过利用不同符号的出现频率差异,熵编码能实现高效的无损压缩。JPEG主要使用**霍夫曼编码(Huffman Coding)**来进行熵编码。

霍夫曼编码基础

        霍夫曼编码是一种基于贪心算法的可变长度编码方法,用于为每个输入符号分配一个唯一的二进制码。出现频率高的符号分配短码,频率低的符号分配长码,从而减少整体编码长度。

步骤:

  1. 统计频率:计算每个符号出现的频率。
  2. 构建霍夫曼树
    • 将所有符号视为叶子节点,按频率排序。
    • 每次选取两个最低频率的节点,创建一个新的内部节点,其频率为两个子节点频率之和。
    • 重复此过程,直到所有节点合并为一棵霍夫曼树。
  3. 生成编码
    • 从霍夫曼树根节点开始,左分支为0,右分支为1
    • 每个叶子节点的路径即为该符号的霍夫曼编码。

示例:

假设有以下符号及其频率:

  • A: 45
  • B: 13
  • C: 12
  • D: 16
  • E: 9
  • F: 5

构建霍夫曼树并生成编码:

  • A: 0
  • B: 101
  • C: 100
  • D: 111
  • E: 1101
  • F: 1100

差分脉冲编码调制(DPCM)

  • DC系数编码:每个8x8块的第一个系数(DC系数)代表图像块的平均亮度。为了减少存储量,JPEG采用DPCM编码,即存储当前块的DC系数与前一个块的DC系数之间的差值,而不是直接存储DC系数本身。

示例:

  • 第一个块的DC系数为100,直接编码为100。
  • 第二个块的DC系数为102,与前一个块的差值为+2,只需编码差值+2。
  • 第三个块的DC系数为101,与前一个块的差值为-1,只需编码差值-1。

代码示例(Python):

def dpcm_encode(dc_coefficients):
    diff = [dc_coefficients[0]]
    for i in range(1, len(dc_coefficients)):
        diff.append(dc_coefficients[i] - dc_coefficients[i-1])
    return diff

通过这种方式,连续块之间的DC系数变化通常较小,从而减少了需要编码的数据量。

游程长度编码(RLE)

  • AC系数编码:除了DC系数(即0,0处),剩余的63个系数称为AC系数,代表图像块的细节信息。由于高频部分通常包含较多的零,RLE通过记录连续的零值来压缩数据。

编码规则:

  • 记录一个零序列的长度(Run Length)和下一个非零值的大小(Size)。
  • 如果连续零的数量超过15,则使用特定符号表示多个连续的零。

示例:

假设一个AC序列如下:

[4,0,0,0,0,0,6,0,0,−3,0,…]

可以表示为:

(0,4),(5,6),(2,−3),…(0,4), (5,6), (2,-3), \dots(0,4),(5,6),(2,−3),…

其中:

  • (0,4) 表示前有0个零,直接记录值4;
  • (5,6) 表示前有5个零,记录值6;
  • (2,-3) 表示前有2个零,记录值-3。

代码示例(Python):

def rle_encode(ac_coefficients):
    rle = []
    zero_count = 0
    for coeff in ac_coefficients:
        if coeff == 0:
            zero_count += 1
        else:
            while zero_count > 15:
                rle.append((15, 0))  # ZRL: 15 zeros followed by a zero coefficient
                zero_count -= 16
            rle.append((zero_count, coeff))
            zero_count = 0
    if zero_count > 0:
        rle.append((0, 0))  # End of Block
    return rle

这种方法显著减少了需要编码的零值,提高了压缩效率。

霍夫曼编码示例代码

import heapq
from collections import defaultdict

def build_huffman_tree(symbols):
    frequency = defaultdict(int)
    for symbol in symbols:
        frequency[symbol] += 1
    heap = [[freq, [symbol, ""]] for symbol, freq in frequency.items()]
    heapq.heapify(heap)
    while len(heap) > 1:
        lo = heapq.heappop(heap)
        hi = heapq.heappop(heap)
        for pair in lo[1:]:
            pair[1] = '0' + pair[1]
        for pair in hi[1:]:
            pair[1] = '1' + pair[1]
        heapq.heappush(heap, [lo[0] + hi[0]] + lo[1:] + hi[1:])
    return sorted(heapq.heappop(heap)[1:], key=lambda p: (len(p[1]), p))

def huffman_encode(symbols):
    huff_tree = build_huffman_tree(symbols)
    huff_dict = {symbol: code for symbol, code in huff_tree}
    encoded = ''.join([huff_dict[symbol] for symbol in symbols])
    return encoded, huff_dict

def huffman_decode(encoded, huff_dict):
    reverse_dict = {v: k for k, v in huff_dict.items()}
    current_code = ""
    decoded_symbols = []
    for bit in encoded:
        current_code += bit
        if current_code in reverse_dict:
            decoded_symbols.append(reverse_dict[current_code])
            current_code = ""
    return decoded_symbols

解释:

  • 构建霍夫曼树:根据符号的频率构建霍夫曼树,并生成每个符号的霍夫曼编码。
  • 编码过程:将符号序列转换为霍夫曼编码的比特流。
  • 解码过程:根据霍夫曼编码表还原原始符号序列。

通过这种方式,频繁出现的符号被赋予更短的编码,而不常见的符号则使用较长的编码,从而整体上减少了数据量。

五、JPEG解压过程简述

        JPEG解压过程基本上是压缩步骤的逆过程,包括以下步骤:

  1. 熵解码:还原霍夫曼编码的数据,获取RLE符号。
  2. 游程解码:将RLE符号还原为一维的AC系数序列。
  3. 差分解码(DPCM):将DC系数的差值还原为实际的DC系数。
  4. 反Zig-Zag扫描:将一维序列还原为8x8的DCT系数块。
  5. 逆量化:将量化后的DCT系数乘以量化表的值,恢复DCT系数的近似原值。
  6. 逆DCT:将频域数据转换回空间域,恢复图像的像素值。
  7. 颜色空间转换回RGB:将YCbCr颜色空间转换回RGB,得到最终的彩色图像。

        注意:由于量化过程中的信息丢失,解压后的图像与原始图像存在细微差异,但这种差异通常对人眼来说是不可察觉的。

六、总结

        深入学习了JPEG压缩原理之后,不禁被这一系列精妙的步骤所惊叹!之前实在是没有想在这里面会有那么多知识和技巧,感觉自己的知识又增加了。

关键要点回顾:

  • 颜色空间转换:从RGB到YCbCr,提高压缩效率。
  • 下采样:减少色度分量分辨率,进一步降低数据量。
  • 分块与DCT:将图像分割成8x8块,通过DCT转换到频域。
  • 量化:使用预定义量化表,根据质量因子调节DCT系数精度。
  • Zig-Zag扫描:优化DCT系数序列,集中零值,提高压缩效率。
  • 熵编码:利用霍夫曼编码对DC和AC系数进行高效编码。

.


原文地址:https://blog.csdn.net/eieihihi/article/details/143068225

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!