
有人拿着从网页上右键另存的一张 512×512 人像测试图跑 PSNR 对比结果和论文里的参考值差了将近 3dB排查了一下午算法参数最后发现问题出在基准图本身——那张图被网页端重新压过一遍块效应已经渗进了每一处平滑区域。这事我在不同的团队里见过不止一次所以当我看到Lena 图片资源库这个标题时第一反应不是这不就是一张图吗而是一个合格的 Lena 图片资源库到底该长什么样。这张图在图像处理、压缩编码、去噪、超分辨率这些方向被当成事实标准用了三十多年几乎每个做底层视觉的人都拿它跑过第一个 Demo。但它好用不等于随便拿一张就能用资源库的价值恰恰在于把随便拿一张这件事变成有据可查、有校验、可复现。下面这些内容适合刚进实验室要跑第一个实验的同学也适合已经在写论文、被审稿人问过你基准图哪来的的从业者。1. 这张512×512的测试图凭什么在底层视觉圈活了三十多年1.1 它被选中的真实原因一张图凑齐了所有难处理的图像特征很多人以为它出名是因为有名其实最初的选型逻辑非常工程化。图像处理算法在七八十年代要验证的是滤波、变换、量化这一套东西而一套验证素材最理想的状态是单张图里同时包含低频大片平滑区、中频规则纹理、高频锐利边缘以及接近真实人眼敏感度的人脸区域。这张图刚好全占了肩部和背景的大面积渐变属于低频适合检验量化是否产生可见的阶梯帽檐的羽毛是典型的高频密集纹理任何一点滤波器的过平滑都会在这里暴露;帽子上的环状装饰是周期性规则纹理最容易诱发摩尔纹和振铃;而人像的皮肤过渡区介于低频和高频之间是检验色度子采样、肤色校正、块效应最敏感的区域。换成纯几何图测不出自然图像的统计特性换成纯风景图人脸区域的视觉敏感度又覆盖不到。一张图能把这几类特征同时摆出来在那个存储和算力都极其紧张的年代实用价值远大于好看。1.2 512×512 这个尺寸不是随手定的尺寸的确定带着很强的时代烙印。一张 512×512 的 8 比特三通道彩图原始数据量是 512×512×3 786,432 字节也就是 768KB灰度版本是 262,144 字节正好 256KB。这个数字在当年是有讲究的它是 2 的整数次幂做 DCT、小波分解、金字塔下采样时不需要考虑边界填充带来的额外复杂度它又刚好能被 8×8 和 16×16 的块完整整除块划分实验不会留下残块同时它小到能在内存只有几百 KB 的机器上反复读写不大到让一次实验跑一整夜。属性数值对实验的意义尺寸512×512可被 8/16/32/64 整除块类算法无残块彩色原始体积768KB当年可放入内存反复读写灰度原始体积256KB便于快速迭代位深8 bit/通道覆盖 0-255 全动态范围无明显过曝欠曝通道RGB 三通道可同时验证亮度与色度处理这三条合起来就是它能在几十年里一直作为第一张跑通图的原因。你要理解一件事它不是标准它是习惯。没有任何标准组织规定过它纯粹是社区默契。1.3 它测不出来的东西比它能测出来的更值得警惕用久了必须清楚它的盲区否则论文里很容易出现在 Lena 上效果好但实际上算法已经偏了的情况。第一它没有大面积暗部噪声在低照度下的表现完全测不出来做夜景降噪的拿它调参基本等于没调。第二它的动态范围偏中等高光和深阴影都被裁剪掉了做 HDR 色调映射的用它没有意义。第三它的纹理种类虽然多但全是自然纹理缺少文字、线条、建筑这类人造结构做文档图像增强或者屏幕内容编码的根本不该用它。第四也是最容易被忽略的一点只有一张图样本量为 1任何结论都不具备统计意义。我见过有人拿这一张图的 PSNR 差 0.1dB 就宣称方法更优这在审稿里会被直接质疑。比较稳妥的做法是把它当冒烟测试用——算法能不能跑、有没有明显崩溃、中间结果可不可视化用它最快真正的量化结论必须换到多图集合上比如经典的图像处理测试图集或者自己构造的合成数据集。这一点我在实际项目里被坑过早期用单图调出来的参数换到真实业务图上全线崩盘因为那张图恰好没有真实数据里的暗部噪声和强压缩伪影。2. 一个真正能用的 Lena 图片资源库目录里该放些什么2.1 基础层无损原图与它的灰度派生资源库的第一层必须是根也就是未经任何有损处理的位图。这里有个硬性要求存储格式只能是 PNG、TIFF、BMP 这类无损容器绝对不能放 JPG。原因很直白JPG 是有损的量化表一旦烧进去就无法还原你后面所有的实验基准都被污染了。很多所谓的免费下载页面给的其实是质量 90 左右的 JPG用它当 ground truth 算 PSNR天花板直接压到 40dB 出头算法再好也测不出来。灰度版本也要单独收一份而且要注明生成方式。工业界常见两种一种按 ITU-R BT.601 的加权 0.299R 0.587G 0.114B另一种按 BT.709 的 0.2126R 0.7152G 0.0722B。两者算出来的灰度图差异肉眼可见如果你对比的论文用的是 601 而你用 709PSNR 能差零点几 dB足够让复现失败。我建议资源库里两种都存文件名后缀标注清楚例如lena_gray_601.png和lena_gray_709.png。有一个细节值得单独说原始彩色图本身也有一份官方版本和无数衍生版本。有人在历史上对它做过色彩校正、锐化、重新扫描导致不同来源的同一张图色彩分布不一样。资源库必须保留一份被广泛引用的基准版本并在清单里写明它的来源标注否则你根本无法判断别人的实验用的是哪一版。2.2 派生层多分辨率、质量梯度与退化版本第二层是实验素材这一层才是资源库真正的价值所在。手工去造这些版本既费时又容易造得前后不一致资源库一次生成好所有人拿到的都是同一套。类别建议收录的档位生成方式说明典型用途多分辨率512 / 256 / 128 / 64面积平均下采样非最近邻金字塔、多尺度实验JPEG 质量梯度Q95/85/75/50/25/10固定量化表记录实际码率压缩伪影去除高斯噪声σ5/10/25/50固定随机种子去噪、盲去噪椒盐噪声密度 1%/5%/10%固定随机种子中值滤波类验证高斯模糊σ0.5/1.0/2.0固定核大小与边界处理去模糊、锐化下采样-上采样×2、×4先 bicubic 下采样再上采样超分退化建模这里有个坑必须提前说多分辨率版本一定是下采样生成的不是原生拍摄的高分辨率。有些资料站会提供 1024×1024 的高清 Lena那基本都是插值放大的结果拿去当超分任务的 ground truth 等于自己给自己造了一个不存在的真值。资源库在命名上就该写清楚比如lena_512_down2_256.png让人一眼看出这是派生而不是原生。噪声版本一定要存随机种子。我见过一个团队换人之后重跑实验去噪结果对不上查了两天发现是加噪时没固定种子每次跑都是新噪声。这种低级问题在资源库里一次性解决掉写进生成脚本种子作为参数固定下来后面所有人都省事。2.3 清单层manifest 与校验文件第三层最容易被忽略但恰恰是资源库和一堆图片的分界线。至少要有一份 JSON 清单记录每个文件的相对路径、尺寸、通道数、位深、色彩空间、文件大小、SHA256以及派生关系父文件是谁、用了什么参数。{ asset: lena_color_512.png, width: 512, height: 512, channels: 3, bit_depth: 8, color_space: sRGB, bytes: 473831, sha256: …, derived_from: null }有了这份清单别人下载之后跑一个校验脚本就能知道文件有没有损坏、有没有被中间环节重新压缩过。这一点在大文件分享过程中尤其重要某些分享渠道会对图片做自动转码或者加水印一旦发生尺寸和哈希都会变脚本一跑就露馅。3. 文件到手先别急着写代码三个必须先做的校验动作3.1 哈希校验确认文件真的没在中途被加工过下载完第一件事是算哈希和清单对比。这不是形式主义我遇到过太多次分享链路对图片做了不可见的重新编码。校验方式很简单# Linux / macOS shasum -a 256 lena_color_512.png # Windows PowerShell Get-FileHash .\lena_color_512.png -Algorithm SHA256哈希对不上先别急着怀疑文件损坏八成是被某个环节重新编码了。这时候正确的做法不是凑合用而是换一个下载源重新取。压缩包里如果有多个文件写个循环一次性全查import hashlib, json, pathlib def sha256_of(path): h hashlib.sha256() with open(path, rb) as f: for chunk in iter(lambda: f.read(1 20), b): h.update(chunk) return h.hexdigest() manifest json.load(open(manifest.json, encodingutf-8)) bad [] for item in manifest: p pathlib.Path(item[asset]) if not p.exists() or sha256_of(p) ! item[sha256]: bad.append(str(p)) print(异常文件:, bad if bad else 无)3.2 尺寸、通道与真实编码格式的交叉验证哈希对上只能说明文件没变不能说明文件是对的。第二步要验证它是不是你以为的那个东西。扩展名是最不可信的信息lena.png完全可能内部是 JPEG 数据被改了后缀。用库读一下真实编码from PIL import Image im Image.open(lena_color_512.png) print(im.format, im.size, im.mode, im.info.keys()) # 期望类似: PNG (512, 512) RGB dict_keys([dpi])如果im.format打印出JPEG那这个文件就是假的 PNG。如果im.mode是P或L而不是RGB说明它是调色板图或灰度图通道数不对。这一步看着傻但能挡掉大量看着像其实不是的问题。3.3 色彩空间与嵌入配置的排查第三件事最隐蔽检查有没有嵌 ICC 配置文件、有没有 EXIF 里的方向标记、有没有 alpha 通道。ICC 配置会影响不同软件里的显示和像素读取结果EXIF 方向标记会让某些读取库自动旋转 90 度导致你的尺寸检查莫名其妙失败多出来的 alpha 通道会让shape变成(512, 512, 4)直接打断后续的三通道假设。异常现象根因处理办法shape是 4 通道带 alpha 通道显式转 RGB 或丢弃 alpha读取后尺寸变 512×512 以外的值EXIF 方向被自动应用读取时禁用自动旋转不同软件里颜色明显偏嵌入了非 sRGB 的 ICC统一转到 sRGB 后再处理平滑区有细密网格曾被 JPEG 压缩换源不要试图修复哈希每次都不同分享链路自动转码换分发方式我个人的习惯是资源库里不放带任何附加信息的文件转成最朴素的 PNG剥掉 EXIF 和 ICC只标一个假定 sRGB这样跨工具链的行为最稳定。4. 拿它跑客观质量评测时五个反复翻车的细节4.1 基准图本身带损是所有对比失效的头号原因这是最高频的问题前面反复提过但值得单列。判断方法很简单打开图片把画面放大到 400%看肩部那片平滑渐变区。如果有 8×8 的方块痕迹或者颜色过渡出现台阶那就说明它被有损压缩过。这个判断比任何脚本都直观。一旦基准带损你得到的 PSNR 曲线会整体下移并且被压平——因为基准本身已经模糊算法把结果变得越锐利反而离这个模糊基准越远。这时候你会看到一些反直觉的现象视觉效果更好的方法PSNR 反而更低。别急着归因于算法先查基准。4.2 通道顺序和数据类型一个字母之差结果全变RGB 和 BGR 搞混是第二高频。用 OpenCV 读图得到的是 BGR 顺序用 PIL 读到的是 RGB两者混用算出来的 PSNR 会莫名其妙偏低。判断技巧单独算三个通道的 PSNR如果红通道和蓝通道的数值明显对调了那就是通道顺序错了。数据类型的问题同样致命。8 比特整型直接相减会产生下溢回绕255 减 0 变成 1 这种错误不会报错只会让结果混乱。正确做法是先转浮点再算import numpy as np from PIL import Image def psnr(ref_path, test_path): ref np.asarray(Image.open(ref_path).convert(RGB), dtypenp.float64) tst np.asarray(Image.open(test_path).convert(RGB), dtypenp.float64) assert ref.shape tst.shape, (ref.shape, tst.shape) mse np.mean((ref - tst) ** 2) if mse 0: return float(inf) return 10 * np.log10(255.0 ** 2 / mse)注意convert(RGB)这一句不能省它同时解决了调色板模式和 alpha 通道两个问题。4.3 数据类型与归一化范围不统一有的库输出 0-1 的浮点有的输出 0-255 的整型。如果你用 0-1 的图和 0-255 的图比PSNR 会掉到一个荒谬的负数或者几十 dB 的离谱值。资源库的清单里应该明确写清楚像素范围 0-255 的 uint8作为一条全局约定。写脚本时在入口处断言一下assert ref.max() 1.5 and tst.max() 1.5, 疑似 0-1 归一化请统一到 0-2554.4 裁剪对不齐块边界块类算法的评测全部失真做块效应去除、DCT 域处理、分块超分这类算法时如果输入图的左上角位置和基准对不齐块边界就会错位效果会凭空掉一大截。资源库里的裁剪块应该按固定步长、固定起点生成并且把起点坐标写进文件名例如patch_x000_y000_64.png。别用随机裁剪做评测集随机性会让两次实验不可比。4.5 上采样版本冒充原图超分任务的评测整体虚高这条前面提过但值得再强调一次因为它造成的错误最隐蔽也最普遍。判断方法看图像的高频细节真正的高分辨率图在头发丝和羽毛边缘会有自然的、不规则的细节插值放大的图在这些位置会显得平滑但不自然边缘有轻微的糊边。有一个更技术化的判断手段算一下图像的功率谱插值放大的图在高频段会有明显的衰减或者规律的零点。不方便算的话看文件大小也能有个大概——同等尺寸下插值放大的 PNG 通常比真实高清图小得多因为高频信息是人为补出来的熵更低。5. 从资源库到实验流水线把图真正喂进评测脚本5.1 目录约定与批量加载资源库落到本地之后先固定一套目录约定别让人肉记忆路径lena_repo/ raw/ lena_color_512.png lena_gray_601.png lena_gray_709.png scales/ lena_256.png lena_128.png lena_64.png jpeg/ lena_q95.jpg lena_q75.jpg lena_q25.jpg noise/ lena_g10_s1.png lena_g25_s1.png blur/ lena_blur1.0.png patches/ patch_x000_y000_64.png manifest.json加载的时候按 manifest 走不要os.listdir一把梭。文件名的排序在跨平台时不可靠而且派生关系和参数都藏在名字里靠字符串解析很容易出错。5.2 一条能跑的评测流水线下面这段是可直接复制的完整脚本从资源库读取基准批量施加压缩和噪声输出 CSVimport csv, io, json import numpy as np from pathlib import Path from PIL import Image ROOT Path(lena_repo) REF ROOT / raw / lena_color_512.png def load_rgb(p): return np.asarray(Image.open(p).convert(RGB), dtypenp.float64) def psnr(a, b): mse np.mean((a - b) ** 2) return float(inf) if mse 0 else 10 * np.log10(255.0 ** 2 / mse) def jpeg_roundtrip(arr, quality): buf io.BytesIO() Image.fromarray(arr.astype(np.uint8)).save(buf, formatJPEG, qualityquality) buf.seek(0) return np.asarray(Image.open(buf).convert(RGB), dtypenp.float64) def add_gauss(arr, sigma, seed1): rng np.random.default_rng(seed) return np.clip(arr rng.normal(0, sigma, arr.shape), 0, 255) def main(): ref load_rgb(REF) assert ref.shape (512, 512, 3), ref.shape rows [] for q in (95, 85, 75, 50, 25, 10): rows.append({op: fjpeg_q{q}, psnr: round(psnr(ref, jpeg_roundtrip(ref, q)), 3)}) for s in (5, 10, 25, 50): rows.append({op: fgauss_s{s}, psnr: round(psnr(ref, add_gauss(ref, s)), 3)}) with open(eval_result.csv, w, newline, encodingutf-8) as f: w csv.DictWriter(f, fieldnames[op, psnr]) w.writeheader() w.writerows(rows) for r in rows: print(r) if __name__ __main__: main()跑通之后你会得到一条质量-失真基线曲线。这条曲线的价值在于以后任何新算法把它处理后的结果替换进jpeg_roundtrip那一步就能直接读出相对提升。资源库的意义就在这儿——大家用的是同一根尺子。5.3 把能跑通变成能复现的三个习惯第一个习惯是固定随机种子并写进文件名噪声、裁剪、初始化全部这样。第二个习惯是把版本信息落盘manifest 里加一个repo_version字段每次更新资源库就撞一次版本号论文里写清楚用的哪个版本。第三个习惯是保留中间产物别只留最终图处理链每一步的输出都存下来出问题时能二分定位是哪一步引入的偏差。我踩过最深的一个坑是某次换机器后结果对不上最后发现是不同版本的图像库在 JPEG 编码时用了不同的默认色度子采样模式一个用 4:2:0一个用 4:4:4。这个问题靠肉眼完全看不出来只有 PSNR 差 0.3dB 告诉你出事了。从那以后我所有的 JPEG 相关实验都显式指定subsampling参数绝不依赖默认值。6. 关于这张图的来源争议以及我现在的替代做法6.1 为什么越来越多的地方不建议用它这张图的原始素材来自一份商业刊物的插图虽然被图像处理社区无授权使用了三十多年但近些年随着版权意识和肖像权讨论的推进越来越多的学术会议和期刊在投稿规范里明确建议不要使用它一些课程也已经把它从作业里换掉了。这不是技术问题是使用场景和合规意识的变化。作为从业者我觉得没必要在这件事上纠结——它作为测试图的技术价值是真实的但它不是唯一选择也不用非用不可。实际处理原则很简单内部调试、教学演示这类不公开发表的场景用它没问题要投公开论文、要做对外发布的技术报告就换成没有争议的素材。这个界限划清楚两边都不耽误。6.2 我自己现在用的三套替代素材第一套是标准的合成靶图。自己用代码画棋盘格检验块边界同心圆检验频域响应灰阶渐变条检验量化台阶随机噪声块检验熵编码效率。这套图的好处是每一处细节你都知道它应该是什么样出了问题可以直接反推是算法的哪一环。import numpy as np from PIL import Image n 512 y, x np.mgrid[0:n, 0:n] checker (((x // 32) (y // 32)) % 2 * 255).astype(np.uint8) ring ((np.sqrt((x - n/2)**2 (y - n/2)**2) / 8).astype(int) % 2 * 255).astype(np.uint8) Image.fromarray(checker).save(靶图_棋盘格.png) Image.fromarray(ring).save(靶图_同心圆.png)第二套是纹理拼贴图。把平滑区、细密纹理、锐利边缘、规则条纹四类区域拼在一张图里用不同比例拼几张不同版本覆盖不同的特征组合。它的优势是可以按需配比比如专门做去噪的可以多放平滑区做压缩的可以多放高频纹理。第三套是自然图像小集合。从授权明确、可商用的公开图片里挑 20 到 30 张覆盖人像、建筑、风景、文档几个类别分辨率统一到同一档。做量化结论的时候用这一套样本量够结论才有说服力而且不会有来源上的麻烦。6.3 换成新素材之后怎么和旧结论对上这是很多人不敢换的原因怕新旧结果没法比较。实际上不用慌方法是同步跑两份。在你的实验表格里保留一列旧素材的结果再新增一列新集合的结果两组趋势一致就说明算法本身的改进是真实的只是绝对数值不同。如果趋势不一致那才是真问题——说明算法对某类特征过拟合了这比绝对值差异重要得多。我个人的经验是凡是只在那一张图上有效的改进换到二十张图的集合上基本都会被打回原形。早点用多图集合做验证能省掉大量后期返工。那张 512×512 的图到今天仍然有它的位置做冒烟测试、做快速可视化、做教学演示它依然是效率最高的选择。但要说它能支撑一篇论文的结论那还是算了一张图担不起这个责任。最后再分享一个我在整理资源库时养成的习惯每拿到一个官方版本的图和一段别人给的复现脚本先用哈希把图钉死再跑一遍脚本看输出对不对得上。对得上说明你的环境和作者一致对不上先查图再查库版本别一上来就怀疑自己的算法。十次里有七八次问题就出在那张图身上。