数据压缩原理与应用 | 实验三 | 基于SSIM迭代的JPEG编解码

张开发
2026/4/20 2:46:34 15 分钟阅读

分享文章

数据压缩原理与应用 | 实验三 | 基于SSIM迭代的JPEG编解码
目录一、JPEG原理概述二、文件格式设计三、实验代码基础算法量化表迭代选择与基于SSIM迭代恢复DCT系数算法四、结果分析数据集一览结果分析五、经验总结六、碎碎念一、JPEG原理概述编码先将整个图片分为8*8的N个块然后遍历块先进行DCT变换再用量化表对DCT变换得到的系数表进行归零压缩然后用zigzan扫描压缩表以提高游程编码的压缩效率。在写入文件时把shape和块数写在文件头以便解码。需要注意的是本实验没有用huffman编码所以在性能相近时该算法的压缩比会低于windows和opencv的jpeg压缩算法。本实验定义了数个量化表用于量化表的选择并通过ssim分数进行DCT系数还原的迭代。解码逆着编码过程来即可。本实验设计了量化表选择迭代与基于SSIM的DCT系数恢复迭代两种算法但代码中的量化表设置方式错误设置了高中低三种质量的量化表而非针对不同特点的图片设置不同量化表导致所有图片最终都会选择高质量的量化表。对于DCT系数恢复迭代本实验采用三通道计算SSIM的方式YUV占比分别为0.8 0.1 0.1采用了0.96倍的量化后SSIM进行迭代每次将所有块的DCT系数排序由最大端开始恢复一定比例的DCT系数当SSIM超过0.96*量化后SSIM后退出迭代。二、文件格式设计本实验设计了一种名为enjpg的文件格式该格式的文件头包含图片的shape、图片含有的块数以及每个块的DCT系数。三、实验代码由于利用AI多次修改代码导致框架混乱代码利用率底下且嵌套冗杂所以只提供完整代码不提供分段代码。基础算法import numpy as np import cv2 import os import struct from skimage.metrics import structural_similarity as ssim Q np.array([ [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] ]) def dct_2d(block): return cv2.dct(block.astype(np.float32)) def idct_2d(block): return cv2.idct(block.astype(np.float32)) def quantify(block): return np.round(block / Q).astype(int) def dequantify(block): return (block * Q).astype(np.float32) def zigzag_scan(block): h, w block.shape result [] for s in range(15): if s % 2 0: for i in range(s 1): x, y i, s - i if x h and y w: #假设矩阵无限长只在真存在时才算数 result.append(block[x, y]) else: for i in range(s 1): x, y s - i, i if x h and y w: result.append(block[x, y]) return result def zigzag_inverse(arr): block np.zeros((8, 8)) idx 0 for s in range(15): if s % 2 0: for i in range(s 1): x, y i, s - i if x 8 and y 8: block[x, y] arr[idx] idx 1 else: for i in range(s 1): x, y s - i, i if x 8 and y 8: block[x, y] arr[idx] idx 1 return block def rle_encode(array): result [] zero_count 0 for value in array: if value 0: zero_count 1 else: result.append((zero_count, int(value))) zero_count 0 result.append((0, 0)) return result def rle_decode(rle): result [] for zeros, value in rle: if (zeros, value) (0, 0): break result.extend([0] * zeros) result.append(value) while len(result) 64: #把(0,0)补上 result.append(0) return result def block_cut(image): h, w image.shape pad_h (8 - h % 8) % 8 pad_w (8 - w % 8) % 8 image_padded np.pad(image, ((0, pad_h), (0, pad_w)), modeconstant) H, W image_padded.shape blocks [] for i in range(0, H, 8): for j in range(0, W, 8): blocks.append(image_padded[i:i8, j:j8]) return blocks, H, W def block_merge(blocks, H, W): img np.zeros((H, W), dtypenp.float32) k 0 for i in range(0, H, 8): for j in range(0, W, 8): img[i:i8, j:j8] blocks[k] k 1 return img def encode_channel(channel): channel channel.astype(np.float32) - 128 blocks, H, W block_cut(channel) encoded [] for block in blocks: dct_block dct_2d(block) q_block quantify(dct_block) zz zigzag_scan(q_block) rle rle_encode(zz) encoded.append(rle) return encoded, H, W def decode_channel(encoded, H, W): blocks [] for rle in encoded: zz rle_decode(rle) block zigzag_inverse(zz) deq dequantify(block) idct_block idct_2d(deq) idct_block 128 idct_block np.clip(idct_block, 0, 255) blocks.append(idct_block) return block_merge(blocks, H, W) def write_channel(f, encoded): for block in encoded: for zeros, value in block: f.write(struct.pack(b, zeros)) f.write(struct.pack(b, value)) def read_channel(f, num_blocks): encoded [] for _ in range(num_blocks): block [] while True: zeros struct.unpack(b, f.read(1))[0] value struct.unpack(b, f.read(1))[0] block.append((zeros, value)) if (zeros, value) (0, 0): break encoded.append(block) return encoded def jpeg_encode(image_path, save_path): img cv2.imread(image_path) y_cr_cb cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) Y, Cr, Cb cv2.split(y_cr_cb) Y_ed, H, W encode_channel(Y) Cr_ed, _, _ encode_channel(Cr) Cb_ed, _, _ encode_channel(Cb) num_blocks len(Y_ed) with open(save_path, wb) as f: # 写头信息 f.write(struct.pack(I, H)) f.write(struct.pack(I, W)) f.write(struct.pack(I, num_blocks)) # 写三个通道 write_channel(f, Y_ed) write_channel(f, Cr_ed) write_channel(f, Cb_ed) Y_rec decode_channel(Y_ed, H, W) Cr_rec decode_channel(Cr_ed, H, W) Cb_rec decode_channel(Cb_ed, H, W) y_cr_cb_rec cv2.merge([ Y_rec.astype(np.uint8), Cr_rec.astype(np.uint8), Cb_rec.astype(np.uint8) ]) result cv2.cvtColor(y_cr_cb_rec, cv2.COLOR_YCrCb2BGR) psnr_val calc_psnr(img, result) ssim_val calc_ssim(img, result) print(fPSNR{psnr_val:.2f} SSIM{ssim_val:.4f}) def jpeg_decode(load_path, output_path): with open(load_path, rb) as f: H struct.unpack(I, f.read(4))[0] W struct.unpack(I, f.read(4))[0] num_blocks struct.unpack(I, f.read(4))[0] Y_enc read_channel(f, num_blocks) Cr_enc read_channel(f, num_blocks) Cb_enc read_channel(f, num_blocks) Y decode_channel(Y_enc, H, W) Cr decode_channel(Cr_enc, H, W) Cb decode_channel(Cb_enc, H, W) y_cr_cb cv2.merge([ Y.astype(np.uint8), Cr.astype(np.uint8), Cb.astype(np.uint8) ]) result cv2.cvtColor(y_cr_cb, cv2.COLOR_YCrCb2BGR) cv2.imwrite(output_path, result) def calc_psnr(img1, img2): mse np.mean((img1.astype(np.float32) - img2.astype(np.float32)) ** 2) if mse 0: return 100 return 10 * np.log10((255 ** 2) / mse) def calc_ssim(img1, img2): img1_ycrcb cv2.cvtColor(img1, cv2.COLOR_BGR2YCrCb) img2_ycrcb cv2.cvtColor(img2, cv2.COLOR_BGR2YCrCb) Y1, Cr1, Cb1 cv2.split(img1_ycrcb) Y2, Cr2, Cb2 cv2.split(img2_ycrcb) ssim_y ssim(Y1, Y2) ssim_cr ssim(Cr1, Cr2) ssim_cb ssim(Cb1, Cb2) #模拟人眼 return 0.8 * ssim_y 0.1 * ssim_cr 0.1 * ssim_cb if __name__ __main__: input_folder pictures compressed_folder compressed_enjpg decompressed_folder decompress_jpg for filename in os.listdir(input_folder): if filename.lower().endswith((.bmp, .jpg, .jpeg, .png)): input_path os.path.join(input_folder, filename) name os.path.splitext(filename)[0] compressed_path os.path.join(compressed_folder, name .enjpg) jpeg_encode(input_path, compressed_path) original_size os.path.getsize(input_path) compressed_size os.path.getsize(compressed_path) print(f{filename} 压缩完成 | 原始:{original_size} 压缩:{compressed_size} 压缩比:{original_size/compressed_size}\n) for filename in os.listdir(compressed_folder): if filename.endswith(.enjpg): input_path os.path.join(compressed_folder, filename) name os.path.splitext(filename)[0] output_path os.path.join(decompressed_folder, name _normal.bmp) jpeg_decode(input_path, output_path) print(f{filename} 解压完成 → {output_path})量化表迭代选择与基于SSIM迭代恢复DCT系数算法import numpy as np import cv2 import os import struct from skimage.metrics import structural_similarity as ssim import shutil QUANT_TABLES { standard: np.array([ [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] ]), high_quality: np.array([ [8,6,5,8,12,20,25,30], [6,6,7,9,13,29,30,27], [7,6,8,12,20,28,34,28], [7,8,11,14,25,43,40,31], [9,11,18,28,34,54,51,38], [12,17,27,32,40,52,56,46], [24,32,39,43,51,60,60,50], [36,46,47,49,56,50,51,49] ]), high_compress: np.array([ [32,22,20,32,48,80,102,122], [24,24,28,38,52,116,120,110], [28,26,32,48,80,114,138,112], [28,34,44,58,102,174,160,124], [36,44,74,112,136,218,206,154], [48,70,110,128,162,208,226,184], [98,128,156,174,206,242,240,202], [144,184,190,196,224,200,206,198] ]), } Q QUANT_TABLES[standard] def dct_2d(block): return cv2.dct(block.astype(np.float32)) def idct_2d(block): return cv2.idct(block.astype(np.float32)) def quantify(block): return np.round(block / Q).astype(int) def dequantify(block): return (block * Q).astype(np.float32) def zigzag_scan(block): h, w block.shape result [] for s in range(15): if s % 2 0: for i in range(s 1): x, y i, s - i if x h and y w: result.append(block[x, y]) else: for i in range(s 1): x, y s - i, i if x h and y w: result.append(block[x, y]) return result def zigzag_inverse(arr): block np.zeros((8, 8)) idx 0 for s in range(15): if s % 2 0: for i in range(s 1): x, y i, s - i if x 8 and y 8: block[x, y] arr[idx] idx 1 else: for i in range(s 1): x, y s - i, i if x 8 and y 8: block[x, y] arr[idx] idx 1 return block def rle_encode(array): result [] zero_count 0 for value in array: if value 0: zero_count 1 else: result.append((zero_count, int(value))) zero_count 0 result.append((0, 0)) return result def rle_decode(rle): result [] for zeros, value in rle: if (zeros, value) (0, 0): break result.extend([0] * zeros) result.append(value) while len(result) 64: result.append(0) return result def block_cut(image): h, w image.shape pad_h (8 - h % 8) % 8 pad_w (8 - w % 8) % 8 image_padded np.pad(image, ((0, pad_h), (0, pad_w)), modeconstant) H, W image_padded.shape blocks [] for i in range(0, H, 8): for j in range(0, W, 8): blocks.append(image_padded[i:i8, j:j8]) return blocks, H, W def block_merge(blocks, H, W): img np.zeros((H, W), dtypenp.float32) k 0 for i in range(0, H, 8): for j in range(0, W, 8): img[i:i8, j:j8] blocks[k] k 1 return img def decode_channel(encoded, H, W): blocks [] for rle in encoded: zz rle_decode(rle) block zigzag_inverse(zz) deq dequantify(block) idct_block idct_2d(deq) idct_block 128 idct_block np.clip(idct_block, 0, 255) blocks.append(idct_block) return block_merge(blocks, H, W) def write_channel(f, encoded): for block in encoded: for zeros, value in block: f.write(struct.pack(b, zeros)) f.write(struct.pack(b, value)) def read_channel(f, num_blocks): encoded [] for _ in range(num_blocks): block [] while True: zeros struct.unpack(b, f.read(1))[0] value struct.unpack(b, f.read(1))[0] block.append((zeros, value)) if (zeros, value) (0, 0): break encoded.append(block) return encoded def encode_blocks(blocks): encoded [] for block in blocks: zz zigzag_scan(block) rle rle_encode(zz) encoded.append(rle) return encoded def select_best_quant_table(original_img, Y, Cr, Cb): global Q best_ssim -1 best_name standard best_table QUANT_TABLES[standard] # 对每一套量化表做一次完整压缩解压计算 SSIM for table_name, table in QUANT_TABLES.items(): Q table # DCT Y_blocks, H, W block_cut(Y.astype(np.float32)-128) Cr_blocks, _, _ block_cut(Cr.astype(np.float32)-128) Cb_blocks, _, _ block_cut(Cb.astype(np.float32)-128) Y_dct [dct_2d(b) for b in Y_blocks] Cr_dct [dct_2d(b) for b in Cr_blocks] Cb_dct [dct_2d(b) for b in Cb_blocks] # 量化 Y_q [quantify(b) for b in Y_dct] Cr_q [quantify(b) for b in Cr_dct] Cb_q [quantify(b) for b in Cb_dct] # 重建 def recon_channel(q_blocks): out [] for b in q_blocks: deq dequantify(b) idct_block idct_2d(deq) 128 idct_block np.clip(idct_block, 0, 255) out.append(idct_block) return block_merge(out, H, W) Y_rec recon_channel(Y_q) Cr_rec recon_channel(Cr_q) Cb_rec recon_channel(Cb_q) # 合成图像 rec_ycc cv2.merge([Y_rec.astype(np.uint8), Cr_rec.astype(np.uint8), Cb_rec.astype(np.uint8)]) rec_bgr cv2.cvtColor(rec_ycc, cv2.COLOR_YCrCb2BGR) # 计算质量 ssim_val calc_ssim(original_img, rec_bgr) psnr_val calc_psnr(original_img, rec_bgr) print(f量表: {table_name:12} | SSIM{ssim_val:.4f} | PSNR{psnr_val:.2f}) if ssim_val best_ssim: best_ssim ssim_val best_name table_name best_table table # 最终切换到最优表 Q best_table print(最优量化表, best_name, f | SSIM{best_ssim:.4f}) return Q, best_ssim def jpeg_encode(image_path, save_path, ssim_ratio): name os.path.splitext(os.path.basename(image_path))[0] base_dir os.path.dirname(save_path) img cv2.imread(image_path) y_cr_cb cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) Y, Cr, Cb cv2.split(y_cr_cb) _, best_ssim select_best_quant_table(img, Y, Cr, Cb) ssim_min best_ssim * ssim_ratio def channel_dct(channel): channel channel.astype(np.float32) - 128 blocks, H, W block_cut(channel) blocks_dct [dct_2d(block) for block in blocks] return blocks_dct, H, W Y_dct, H, W channel_dct(Y) Cr_dct, _, _ channel_dct(Cr) Cb_dct, _, _ channel_dct(Cb) def quantize_blocks(blocks): return [quantify(b) for b in blocks] Y_q quantize_blocks(Y_dct) Cr_q quantize_blocks(Cr_dct) Cb_q quantize_blocks(Cb_dct) Y_opt, Cr_opt, Cb_opt dct_iterative_restore_3channel( Y_q, Cr_q, Cb_q, (H, W), img, base_dir, name, ssim_min) Y_ed encode_blocks(Y_opt) Cr_ed encode_blocks(Cr_opt) Cb_ed encode_blocks(Cb_opt) with open(save_path, wb) as f: f.write(struct.pack(I, H)) f.write(struct.pack(I, W)) f.write(struct.pack(I, len(Y_ed))) write_channel(f, Y_ed) write_channel(f, Cr_ed) write_channel(f, Cb_ed) def jpeg_decode(load_path, output_path): with open(load_path, rb) as f: H struct.unpack(I, f.read(4))[0] W struct.unpack(I, f.read(4))[0] num_blocks struct.unpack(I, f.read(4))[0] Y_enc read_channel(f, num_blocks) Cr_enc read_channel(f, num_blocks) Cb_enc read_channel(f, num_blocks) Y decode_channel(Y_enc, H, W) Cr decode_channel(Cr_enc, H, W) Cb decode_channel(Cb_enc, H, W) y_cr_cb cv2.merge([Y.astype(np.uint8), Cr.astype(np.uint8), Cb.astype(np.uint8)]) result cv2.cvtColor(y_cr_cb, cv2.COLOR_YCrCb2BGR) cv2.imwrite(output_path, result) def calc_psnr(img1, img2): mse np.mean((img1.astype(np.float32) - img2.astype(np.float32)) ** 2) if mse 0: return 100 return 10 * np.log10((255 ** 2) / mse) def calc_ssim(img1, img2): img1_ycrcb cv2.cvtColor(img1, cv2.COLOR_BGR2YCrCb) img2_ycrcb cv2.cvtColor(img2, cv2.COLOR_BGR2YCrCb) Y1, Cr1, Cb1 cv2.split(img1_ycrcb) Y2, Cr2, Cb2 cv2.split(img2_ycrcb) ssim_y ssim(Y1, Y2) ssim_cr ssim(Cr1, Cr2) ssim_cb ssim(Cb1, Cb2) return 0.8 * ssim_y 0.1 * ssim_cr 0.1 * ssim_cb def dct_iterative_restore_3channel(Y_blocks_dct, Cr_blocks_dct, Cb_blocks_dct, img_shape, original_img, save_root, image_name, ssim_min, restore_ratio0.01): iter_dir os.path.join(str(save_root), str(image_name)) if os.path.exists(iter_dir): shutil.rmtree(iter_dir) os.makedirs(iter_dir, exist_okTrue) original_Y [b.flatten() for b in Y_blocks_dct] original_Cr [b.flatten() for b in Cr_blocks_dct] original_Cb [b.flatten() for b in Cb_blocks_dct] def init_blocks(blocks): out [] for b in blocks: flat b.flatten() new np.zeros_like(flat) new[0] flat[0] out.append(new) return out current_Y init_blocks(Y_blocks_dct) current_Cr init_blocks(Cr_blocks_dct) current_Cb init_blocks(Cb_blocks_dct) all_coefficients [] index_map [] def collect(blocks, channel): for block_idx, block in enumerate(blocks): for i in range(1, 64): val abs(block[i]) weight 1.0 if channel Y else 0.5 all_coefficients.append(val * weight) index_map.append((channel, block_idx, i)) collect(original_Y, Y) collect(original_Cr, Cr) collect(original_Cb, Cb) all_coefficients np.array(all_coefficients) sorted_idx np.argsort(all_coefficients)[::-1] total len(sorted_idx) restore_step max(1, int(total * restore_ratio)) def batch_restore(temp_blocks, orig_blocks, block_idx, positions): if not block_idx: return block_idx np.array(block_idx) positions np.array(positions) unique_blocks np.unique(block_idx) for b in unique_blocks: mask (block_idx b) pos positions[mask] temp_blocks[b][pos] orig_blocks[b][pos] def recon(blocks): out [] for b in blocks: block_2d b.reshape(8, 8) deq dequantify(block_2d) img idct_2d(deq) 128 img np.clip(img, 0, 255) out.append(img) return block_merge(out, img_shape[0], img_shape[1]) for start in range(0, total, restore_step): temp_Y [b.copy() for b in current_Y] temp_Cr [b.copy() for b in current_Cr] temp_Cb [b.copy() for b in current_Cb] Y_idx, Y_pos [], [] Cr_idx, Cr_pos [], [] Cb_idx, Cb_pos [], [] batch sorted_idx[start:start restore_step] for idx in batch: channel, block_idx, pos index_map[idx] if channel Y: Y_idx.append(block_idx) Y_pos.append(pos) elif channel Cr: Cr_idx.append(block_idx) Cr_pos.append(pos) else: Cb_idx.append(block_idx) Cb_pos.append(pos) batch_restore(temp_Y, original_Y, Y_idx, Y_pos) batch_restore(temp_Cr, original_Cr, Cr_idx, Cr_pos) batch_restore(temp_Cb, original_Cb, Cb_idx, Cb_pos) Y_rec recon(temp_Y) Cr_rec recon(temp_Cr) Cb_rec recon(temp_Cb) recon_img cv2.merge([ Y_rec.astype(np.uint8), Cr_rec.astype(np.uint8), Cb_rec.astype(np.uint8) ]) recon_img cv2.cvtColor(recon_img, cv2.COLOR_YCrCb2BGR) iter_id start // restore_step if iter_dir is not None: Y_blocks_2d [b.reshape(8, 8) for b in temp_Y] Cr_blocks_2d [b.reshape(8, 8) for b in temp_Cr] Cb_blocks_2d [b.reshape(8, 8) for b in temp_Cb] enjpg_path os.path.join(iter_dir, fiter_{iter_id:03d}.enjpg) bmp_path os.path.join(iter_dir, fiter_{iter_id:03d}.bmp) internal_endecode(Y_blocks_2d, Cr_blocks_2d, Cb_blocks_2d, img_shape[0], img_shape[1], enjpg_path, bmp_path) ssim_val calc_ssim(original_img, recon_img) psnr_val calc_psnr(original_img, recon_img) print(fSSIM{ssim_val:.4f}, PSNR{psnr_val:.4f}) if ssim_val ssim_min: print(达到目标质量停止恢复) return ([b.reshape(8,8) for b in temp_Y], [b.reshape(8,8) for b in temp_Cr], [b.reshape(8,8) for b in temp_Cb]) current_Y, current_Cr, current_Cb temp_Y, temp_Cr, temp_Cb return ([b.reshape(8,8) for b in current_Y], [b.reshape(8,8) for b in current_Cr], [b.reshape(8,8) for b in current_Cb]) def internal_endecode(Y_blocks, Cr_blocks, Cb_blocks, H, W, save_enjpg_path, save_img_path): Y_ed encode_blocks(Y_blocks) Cr_ed encode_blocks(Cr_blocks) Cb_ed encode_blocks(Cb_blocks) with open(save_enjpg_path, wb) as f: f.write(struct.pack(I, H)) f.write(struct.pack(I, W)) f.write(struct.pack(I, len(Y_ed))) write_channel(f, Y_ed) write_channel(f, Cr_ed) write_channel(f, Cb_ed) jpeg_decode(save_enjpg_path, save_img_path) if __name__ __main__: input_folder pictures compressed_folder compressed_enjpg decompressed_folder decompress_jpg ssim_ratio 0.96 os.makedirs(input_folder, exist_okTrue) os.makedirs(compressed_folder, exist_okTrue) os.makedirs(decompressed_folder, exist_okTrue) for filename in os.listdir(input_folder): if filename.lower().endswith((.bmp, .jpg, .jpeg, .png)): input_path os.path.join(input_folder, filename) name os.path.splitext(filename)[0] compressed_path os.path.join(compressed_folder, name .enjpg) jpeg_encode(input_path, compressed_path, ssim_ratio) original_size os.path.getsize(input_path) compressed_size os.path.getsize(compressed_path) print(f{filename} 压缩完成 | 原始:{original_size} 压缩:{compressed_size} 压缩比:{original_size/compressed_size}\n) for filename in os.listdir(compressed_folder): if filename.endswith(.enjpg): input_path os.path.join(compressed_folder, filename) name os.path.splitext(filename)[0] output_path os.path.join(decompressed_folder, name .bmp) jpeg_decode(input_path, output_path) print(f{filename} 解压完成 → {output_path})四、结果分析数据集一览结果分析实验结果为使用high_quality量化表DCT系数迭代恢复算法所得到的结果。对于实验所用的图片压缩结果均较为相似除了全图细节丰富的finger.bmp其余图片的压缩比差距较小。相比于直接用普通量化表压缩的图片压缩比几乎都有个位数的提升即文件体积缩小了约30%而ssim反而有所上升这可能是由于采用high_quality量化表对低频和中频的压缩减少导致在恢复过程中ssim分数增长较快在未恢复全部系数时ssim分数反而超过了normal算法。需要注意的是本次实验没有使用huffman编码所以该算法的压缩文件压缩比会低于windows和opencv自带的jpeg压缩算法。五、经验总结本次实验没有用huffman编码所以JPEG的基础算法并不难利用AI很快就能搞定。感知优化部分采用了基于SSIM迭代恢复DCT系数的方式没有采用Guetzli中的Butteraugli评价标准因为Butteraugli是2017年提出的只支持C不支持Python直接调用。加入这部分后代码变得十分繁杂嵌套反复即便看懂了AI提供的代码再做修改仍然是越来越难看懂。六、碎碎念加小功能函数前一定要理清逻辑

更多文章