
简介这是一份用于学习和复现JPEG编解码流程的Matlab工程资源面向数字图像处理初学者或需要完成课程设计、实验报告的读者。资源以Matlab源码为核心内置4个.m脚本覆盖JPEG压缩中的DCT变换、量化、霍夫曼编码等关键模块同时附带2个文本说明文件便于对照算法原理阅读代码另有1张bmp测试图像可直接运行验证解码效果。压缩包整体仅57KB共7个文件结构精简适合快速上手和二次修改。目前已有142人浏览学习适合正在接触jpeg标准、想通过具体代码理解压缩解码细节的读者。通过阅读源码与运行测试能够掌握DCT2D变换、Huffman编码等实现思路并借助说明文档厘清JPEG编码的整体流程为后续深入图像压缩研究打下基础。1. 拿到 jpeg.rar 之后JPEG 编解码在 MATLAB 里到底在做什么解压一个常见的 jpeg.rar里面通常是 encode.m、decode.m、一张测试图和几句“JPEG 编解码”的说明。这个题目在 MATLAB 图像处理大作业里出现频率很高但它不是让你调个 imwrite 就交差的事。JPEG 压的是人眼不敏感的信息先把图像切成 8×8 块做离散余弦变换把能量赶到低频区再用量化表把高频细节近似掉最后用 Huffman 编码把系数流压成紧凑的比特流。MATLAB 写 JPEG 编解码价值不在性能而在中间过程可见——DCT 系数、量化矩阵、Zig-Zag 顺序都能单独拿出来查。本文按编码、解码、质量评估的顺序把整条链路过一遍参数给到能直接跑的程度。2. JPEG 编码第一步8×8 分块与二维 DCT 的边界处理2.1 为什么 JPEG 选 8×8 而不是整图做 DCT对整张图做 DCT 理论上能量集中度更好但量化误差会扩散到全图边缘处还会出现明显的振铃伪影。JPEG 在制定标准时测试过 16×16、32×32 分块块越大编码增益提升越有限反而让硬件实现的缓冲区成倍增长。8×8 是压缩率、实现复杂度和误差可见性之间的折中量化噪声被限制在 8×8 范围内振铃半径只有几个像素肉眼不容易察觉。这个选择也直接影响了 MATLAB 实现。如果你在代码里对整张图直接调用dct2压缩率可能更好看但解码端必须用同一尺寸做逆变换而且量化表是 8×8 的无法直接套用。标准的分块流程是先裁掉非 8 倍数的边缘行和列再逐块处理。2.2 直流电平移位像素减 128 之后再进 dct2JPEG 编码的第一步不是 DCT而是 DC 电平移位。imread读进来的 uint8 灰度图范围是 0 到 255JPEG 标准规定输入范围是 -128 到 127也就是每个像素先减 128让中间灰度 128 变成 0。很多人在这里直接im2double(img)拿 0 到 1 的浮点去做 DCT解码时再加回来图像也能重建但 DC 系数整体偏移后续的差分编码会出现不必要的符号波动。img imread(lena.png); if size(img, 3) 3 img rgb2gray(img); % 转灰度返回 uint8 end [H, W] size(img); H H - mod(H, 8); % 裁掉不是 8 的倍数的行 W W - mod(W, 8); % 裁掉不是 8 的倍数的列 img img(1:H, 1:W); imgD double(img) - 128; % DC 电平移位范围变成 [-128, 127] blocks zeros(8, 8, H/8 * W/8); % 第三维存所有块的 DCT 系数 idx 0; for r 1:8:H for c 1:8:W idx idx 1; blocks(:, :, idx) dct2(imgD(r:r7, c:c7)); end enddct2是 Image Processing Toolbox 里的标准二维 DCT 函数默认按 8×8 块计算。用完dct2后blocks(1,1,k)是第 k 个块的 DC 系数代表整块平均亮度其余 63 个是 AC 系数越靠近右下角代表越高频的纹理细节。这里裁边缘而不是填充是因为解码端也要按同样的尺寸规则还原填充会让两侧像素在块边界上产生别人不会有的过渡带。mod(H, 8)的写法比floor(H/8)*8更直观两个效果相同。2.3 分块循环之外检查 DCT 是否正确编码侧写完第一版后先别急着接量化用idct2直接重建一次看效果。如果只做 DCT 不做任何量化重建图像和原图的 PSNR 应该在 50 dB 以上达不到这个值说明 DCT 链路或电平移位写错了。imgRecon zeros(H, W); idx 0; for r 1:8:H for c 1:8:W idx idx 1; imgRecon(r:r7, c:c7) idct2(blocks(:, :, idx)) 128; end end imgRecon min(max(imgRecon, 0), 255); mseVal mean((imgRecon(:) - double(img(:))).^2); psnrVal 10 * log10(255^2 / mseVal); fprintf(DCT 重建 PSNR: %.2f dB\n, psnrVal);DCT 系数坐标的含义可以对着下表查。调试时如果重建图像整体模糊多半不是 DCT 的问题而是后续量化步长调得太大如果图像出现周期性的块边界优先查分块的索引顺序。系数坐标含义典型特点(1,1)DC 系数数值最大代表块平均亮度(1,2)、(2,1)低频率 AC能量占比高量化步长小中间区域中频率 AC决定纹理细节右下角高频率 AC多数接近 0量化步长最大3. 量化表与 Zig-Zag 重排JPEG 压缩率真正来自哪里3.1 标准亮度量化表与质量因子缩放DCT 本身不产生任何压缩系数还是 64 个浮点数。真正的信息丢失发生在量化环节每个系数除以量化步长再取整步长大的位置丢掉的信息就多。JPEG 标准在 Annex K 里给出了一张默认亮度量化表左上角步长小右下角步长大这和人眼对低频敏感、对高频不敏感的特性一致。Q50 [ 16 11 10 16 24 40 51 61; 12 12 14 19 26 58 60 55; 14 13 16 24 40 57 69 56; 14 17 22 29 51 87 80 62; 18 22 37 56 68 109 103 77; 24 35 55 64 81 104 113 92; 49 64 78 87 103 121 120 101; 72 92 95 98 112 100 103 99 ]; function Q scale_qtable(Q50, qf) if qf 50 s (100 - qf) / 50; else s 50 / qf; end Q max(1, floor((Q50 * s 50) / 100)); endqf是质量因子范围 0 到 100。qf50时量化表原样使用qf100时所有步长变成 1接近无损qf越小右下角被放大得越多压缩率越高块效应也越明显。这个缩放公式对应 libjpeg 的默认行为ceil或round在这里会跟其它实现产生 ±1 的偏差按标准写法用floor。3.2 量化的 MATLAB 写法取整方向的坑量化本身一句话就够但取整方向有一个高频出现的坑。JPEG 标准对量化后的系数取最接近的整数而 MATLAB 的round在 0.5 处远离零取整这跟 C 语言里的lrintf以及部分硬件 JPEG 模块的“向零取整”行为不完全一致。网上很多代码用round也跑得通但如果你要和 libjpeg 或 stm32h743 这类硬件 JPEG 编解码器对齐输出就会差出几个灰度级。兼容写法是用fix配合符号处理或者直接接受 ±1 的差异。quantBlock fix(blocks(:, :, idx) ./ Q) ... (abs(fix(blocks(:, :, idx) ./ Q)) abs(blocks(:, :, idx) ./ Q) ... abs(blocks(:, :, idx) ./ Q) 0);更常见的做法是不纠结边界舍入直接用round然后在解码端不额外做补偿。如果你的目标是课设或算法验证round完全足够如果目标是生成一份能和cjpeg对得上结果的数据集再回来处理舍入语义。提示调试时先固定qf100量化表全 1这时候压缩是无损的除舍入外。如果连qf100都出现明显的图像劣化问题不在量化而在前面的 DCT 链路。3.3 Zig-Zag 扫描把 8×8 系数拉成一维量化后的系数矩阵里右下角高频区域会出现大量 0。8×8 矩阵按行优先扫描时零散落在各处很难压缩。Zig-Zag 扫描按从低频到高频的斜线顺序读出 64 个系数让零集中成长串后面接零游程编码时就划算得多。zigzag_idx [ 1 2 6 7 15 16 28 29; 3 5 8 14 17 27 30 43; 4 9 13 18 26 31 42 44; 10 12 19 25 32 41 45 54; 11 20 24 33 40 46 53 55; 21 23 34 39 47 52 56 61; 22 35 38 48 51 57 60 62; 36 37 49 50 58 59 63 64 ]; % quantBlock 是 8×8 量化系数 vec quantBlock(zigzag_idx); % 64×1按 zig-zag 顺序取出zigzag_idx矩阵里存的是输出顺序比如zigzag_idx(1,1)1表示第一个读出的元素是quantBlock(1,1)zigzag_idx(1,2)2表示第二个读出quantBlock(1,2)zigzag_idx(2,2)5表示第五个读出quantBlock(2,2)。解码时用inv_idx(zigzag_idx) 1:64就能把一维向量还原成 8×8 矩阵。顺带一提网安比赛里常见的 JPEG 隐写题就是在量化后的系数低位上做改动。因为量化本身已经丢掉了部分精度改动一两个 LSB 在解码端几乎不可见。4. DC 差分与 Huffman 熵编码把系数流变成比特流4.1 DC 系数为什么先做差分64 个系数里第一个是 DC 系数它代表 8×8 块的平均亮度。自然图像相邻块的平均亮度变化很小所以 JPEG 不对 DC 系数本身编码而是编码当前块 DC 减去上一块 DC 的差值。这个差值绝大多数落在 -15 到 15 之间比原始 DC 的取值范围小得多编码表可以做得更短。dc squeeze(blocks(1, 1, :)); % 所有块的 DC 系数 dcDiff [dc(1); diff(dc)]; % 第一个块原值后续块存差值 % 按 JPEG 的 SSSS 分类把差值归入类别 dcCat zeros(size(dcDiff)); for i 1:length(dcDiff) d abs(dcDiff(i)); if d 0 dcCat(i) 0; else dcCat(i) floor(log2(d)) 1; end enddcCat是每个 DC 差值的分类号专业叫法是 SSSS。分类号 0 表示差值为 0分类号 1 表示 ±1分类号 2 表示 -3 到 -2 和 2 到 3。JPEG 标准里每个分类号对应一段取值范围实际差值本身用固定位长的附加位存储。SSSS取值范围附加位长度0001-1, 112-3..-2, 2..323-7..-4, 4..734-15..-8, 8..1545-31..-16, 16..3154.2 AC 系数零游程加 EOBAC 系数有 63 个Zig-Zag 之后大部分是 0。JPEG 把连续 0 的个数记成 run把非 0 系数按同样的 SSSS 分类记成 size组合成(run, size)的符号再跟非 0 系数的附加位一起输出。63 个 AC 系数的末尾如果都是 0用一个 EOBEnd of Block符号收尾即可。acSeq zeros(63, size(blocks, 3)); for i 1:size(blocks, 3) b quantBlock; % 替换成当前块的量化系数 acSeq(:, i) b(zigzag_idx(2:64)); endacSeq每行是一个块的所有 AC 系数。实际编码时遍历这个向量统计连续 0 的个数遇到非 0 系数就输出一组(run, size)。这里有一个编解码器必须一致的细节run 最大是 15如果连续 0 超过 15 个要拆成多组(15, 0)也就是 ZRL 符号不能直接把 run 记成 16 或更大。4.3 MATLAB 里用 huffmandict 还是标准表JPEG 标准给了一套固定 Huffman 表所有解码器都能认。MATLAB 的 Communications Toolbox 提供了huffmandict、huffmanenco、huffmandeco可以给任意符号集建表。% symbols 必须是列向量probs 是每个符号出现的概率 symbols (0:10); probs histcounts(dcCat, 0:11) / numel(dcCat); [dict, avgLen] huffmandict(symbols, probs); encoded huffmanenco(dcCat, dict); decoded huffmandeco(encoded, dict);huffmandict返回的dict是 cell 数组每行第一个元素是符号第二个是对应的码字。avgLen是平均码长可以用来估算压缩率。注意symbols和probs的维度必须匹配一个常见的报错是Dimensions of arrays being concatenated are not consistent原因是histcounts返回行向量而symbols是列向量转置对齐就好。JPEG 标准表是固定写死的适合生成标准兼容码流huffmandict自建表压缩率略好但需要把表写进自定义文件头。课设和算法验证用自建表更直观看不到标准表那一百多个常量的来源。5. 解码链路反量化、IDCT 与重建质量评估5.1 解码顺序和编码完全相反解码端拿到的输入是量化后的系数块先做反 Zig-Zag 还原成 8×8 矩阵再反量化乘回量化表然后idct2加 128。顺序不能颠倒先反量化再 IDCT和编码端的 DCT 后再量化严格互逆。function imgOut jpeg_decode(qblocks, Q) [bh, bw, n] size(qblocks); rows sqrt(n); cols sqrt(n); % 保存编码时的分块数 imgOut zeros(bh * cols, bw * cols); % 按分块数拼回整图 k 0; inv_idx zeros(8, 8); inv_idx(zigzag_idx) 1:64; % 反 Zig-Zag 索引表 for r 1:8:size(imgOut, 1) for c 1:8:size(imgOut, 2) k k 1; vec qblocks(:, :, k); vec vec(zigzag_idx); % 一维顺序 mat zeros(8, 8); mat(inv_idx) vec; % 还原 8×8 block idct2(mat .* Q) 128; % 反量化 IDCT 电平回移 imgOut(r:r7, c:c7) block; end end imgOut min(max(imgOut, 0), 255); endinv_idx是反 Zig-Zag 的核心inv_idx(zigzag_idx) 1:64把 1 到 64 的位置映射回 8×8 矩阵之后任意一维向量都能通过mat(inv_idx) vec还原成正确矩阵。最后min(max(...))截断到 0-255这个步骤不能省因为idct2输出在块边界处可能略超范围不截断的话图像会有偏白的噪点。5.2 PSNR 和 SSIM压缩质量的量化评价编码器调参时不能只看眼睛用客观指标更可靠。PSNR 反映像素级误差SSIM 反映结构相似度两个指标结合看一个管数值偏差一个管视觉感受。mseVal mean((imgOut(:) - double(img(:))).^2); psnrVal 10 * log10(255^2 / mseVal); ssimVal ssim(uint8(imgOut), img); fprintf(MSE%.2f PSNR%.2f dB SSIM%.3f\n, mseVal, psnrVal, ssimVal);PSNR 的公式里 255^2 是峰值信号能量前提是图像范围 0 到 255。imgOut已经是 double 且值域在 0-255直接和double(img)相减没问题。ssim函数要求两个输入类型一致uint8(imgOut)和原图img都是 uint8可以直接传。质量因子PSNR 参考范围主观感受9038-42 dB肉眼难以区分7533-36 dB放大可见轻微振铃5030-32 dB适合网络传输2026-28 dB块效应明显这张表是自然图像的典型值不是精确保证值。纯色图 PSNR 会偏高纹理密集的图像会偏低看相对趋势比记绝对值更有用。5.3 用 imwrite 生成基准来校准自己的解码器一个实用的验证方法是用 MATLAB 自带的imwrite生成标准 JPEG再用自己的解码器读同一份系数。imwrite走的是 libjpeg 或 MATLAB 内置编码器输出是标准 JPEG 文件。你不需要解析它的码流只要看解码结果和自己的实现是否一致。imwrite(img, ref.jpg, Quality, 50); refImg imread(ref.jpg);然后把refImg和你的解码器输出对比 MSE。差异超过 5 个灰度级说明量化表或舍入逻辑有问题差异在 1 到 2 个灰度级是正常的——浮点 DCT 和整数近似 DCT 之间本来就有微小偏差。这个对比能定位问题在哪一层如果qf50时差异大重点检查量化表和取整方式如果qf100时差异依然大问题在 DCT 本身。6. 让 MATLAB 里的 JPEG 解码提速矩阵化批量处理JPEG 编解码循环里的瓶颈是那个套着dct2和idct2的 8×8 块循环。一幅 1080p 图像约有 32000 个块每个块都要调用一次函数、做一次二维变换MATLAB 的循环开销全花在这一步。换成一次性矩阵乘法速度能快 10 倍以上。T8 dctmtx(8); % 8×8 DCT 变换矩阵 X reshape(blocks, 64, []); % 每列是一个块的系数 Y kron(T8, T8) * X; % 一次性算完所有块的 DCT blocksMat reshape(Y, 8, 8, []); % 还原成 8×8×N 的三维数组dctmtx(8)返回的矩阵 D 满足D * X * D等价于二维 DCT。kron(T8, T8)是张量积把二维变换展开成 64×64 的单次矩阵乘法一次性作用于所有块。解码端做 IDCT 时换成kron(T8, T8)再乘即可。这个做法占用内存约 64×32000×8 字节也就是 16MB 量级桌面端完全可接受。量化那一步同样可以批量处理qblocks fix(reshape(blocks, 64, []) ./ Q(:)) ; % Q 展成一列广播除法 qblocks reshape(qblocks, 8, 8, []);Q(:)是 64×1 的量化表列向量reshape(blocks, 64, [])是 64×N 矩阵两者做除法时 MATLAB 自动广播一次完成所有块的量化不需要写循环。最后提醒三个常见问题。第一uint8和double混用imread返回 uint8运算前先转 double重建完再转回 uint8中间不要来回切否则负数会被截断。第二边缘尺寸编码时裁掉的行列解码端必须用同一规则否则图像错位几个像素很难排查。第三舍入行为round在 MATLAB 里对 0.5 是远离零取整标准 JPEG 参考实现的行为不完全一致如果你要和imwrite或cjpeg的码流做 bit-exact 对比先把量化取整改成符合目标实现的语义再检查 Huffman 表。把这段批量代码存成jpegBatch.m量化表和 Zig-Zag 部分不需要改动解码链路直接就能跑出提速后的结果。本文还有配套的精品资源点击获取