把一组「每张只有一部分清楚」的照片,合成一张从头到尾都清楚的照片。
拍微距、手办、静物这类近景时,光圈收得再小也只能让薄薄一层清楚——前面的花瓣清楚了,后面的就糊。 常规做法是把焦点从近到远扫一遍、连拍一串(这叫包围对焦 / focus bracketing), 回来再用软件把每一张里清楚的那部分挑出来拼合。这个工具就干这件事: 全程在你自己的电脑上跑,不上传、不联网、不限张数、不留水印。
你需要准备的:三脚架(至少机位不能动),以及一组同一构图、焦点从近到远逐张推进的照片—— 多数相机自带的「包围对焦 / focus bracketing」就能拍,手动一张张挪焦点也行。 格式不限(JPEG / TIFF / PNG 都吃),RAW 请先转成 TIFF 或 JPEG;张数多一点更保险。
它和现成方案的区别只有一句:画质目标是「比人工精修过的成品更锐」,而不是「能合上就行」。 下面这张表是在同一组素材上量出来的——分母是我在 Affinity Photo 里手工修过的那张成品, 数字大于 1 就是比它更清楚(1.09 就是清楚 9%)。指标是轮廓锐度 (区域内最强 2% 像素的平均梯度幅值),复现命令见下文「更贴合目视的指标」一节:
| 部位 | 背景墙 | 膝盖边缘 | 光滑身体 | 底座叶片 | 面部 | 平坦背景 |
|---|---|---|---|---|---|---|
| 本工具 ÷ 人工精修的成品 | 1.14 | 1.09 | 1.08 | 1.06 | 1.02 | 1.04 |
合成结果(缩略图,1600px)。原始素材 7008×4672、共 50 张。 照片是我自己拍的,拍摄对象的版权说明见 照片与版权。
这些数字怎么量出来的、每个参数为什么是这个值,写在下面的「工作原理」和 开发记录.md 里。只想出图的话,看下一节就够了。
- 到 Releases 下载
mukastack.exe,比如放到
D:\tools\。 - 把那组包围对焦照片放到一个目录,比如
D:\photos\stack\。 - 按 Win 键 → 输入
cmd→ 回车,然后敲:
D:\tools\mukastack.exe "D:\photos\stack" -o "D:\photos\stack\merged.png"
它是命令行工具,没有图形界面,所以直接双击 exe 会一闪而过,要用上面这种方式调用。
跑完之后(50 张 3300 万像素约 3–4 分钟),输出文件就在你 -o 指定的位置。
- 默认参数就是最高画质,什么都不用加。
- 第一次运行时 Windows 的 SmartScreen 可能会拦一下,选「仍要运行」(exe 没有数字签名)。
- 输出是 16 bit PNG——8 bit 会在天空、墙面这类渐变处出现看得见的色带。
需要先装一次 Rust:
cargo install --path . # 装好后任何目录都能直接敲 mukastack
cargo build --release # 或者只要一个 exe:target/release/mukastack.exe它不挑显卡,但比较吃内存:约为 张数 × 单张像素 × 3 字节——50 张 3300 万像素约 3.3 GB 缓存、
峰值 5–6 GB。内存吃紧或想快一点,加 --analysis-scale 2(按 1/2 分辨率做清晰度分析)。
# 最常用:给一个目录就行(自动按文件名排序、自动剔掉尺寸不一样的成品牌图)
mukastack "照片目录" -o merged.png
# 顺便出一张 1600px 的 JPEG 小图,方便快速看效果
mukastack "照片目录" -o merged.png --preview preview.jpg
# 素材特别大又赶时间:清晰度分析降到 1/2 分辨率
mukastack "照片目录" -o merged.png --analysis-scale 2
# 也可以直接列文件名,只合其中几张
mukastack a.jpg b.jpg c.jpg -o merged.png对应 --mode,默认的 bracket 就够用:
| 模式 | 大白话 | 什么时候换 |
|---|---|---|
bracket(默认) |
颜色和亮度按深度走,细节频段在附近几张里挑最清晰的那张 | 不用换 |
pyramid |
一切按深度平滑过渡,不做取舍 | 素材噪声很大,想要最干净、能接受偏软 |
max |
每个频段都在全组里挑最锐的那张 | 只想看细节极限;会带光晕和噪声 |
mukastack "照片目录" -o soft.png --mode pyramid
pixel、hard两个模式是给对照实验留的,日常不用。
同一台相机、同一个三脚架、同一组照片,1:1 放大,没有做任何锐化或修图。 左边两张是原片,右边是本工具的输出:
裁的是可莉的面部与右侧背景墙。第 15 张是面部最清楚的那张(这时背景墙完全虚掉), 第 28 张是背景墙最清楚的那张(这时面部已经软掉 56%)。合成结果两边都取上了。
下面这张是工具用来判断「每个像素该取哪一张」的距离分布图(加 --save-depth 就会导出,
可以用来检查有没有把背景误判成前景这类问题):
颜色代表张号:蓝=离镜头近,红=远。房间靠左的墙最近、右侧最远,手办本体是最近的一层。
说明里括号中的 ①–⑪ 是 bug 编号,完整记录见 开发记录.md。 下面按"你会想动的顺序"排。★ = 常规调参只需要看这几个。
| 参数 | 默认 | 说明 |
|---|---|---|
<INPUTS>... |
— | 文件、目录,或混合。目录会被扫描 |
-o, --output |
merged.png |
输出路径,扩展名决定格式 |
--preview / --preview-size |
关 / 1600 |
额外写一张小尺寸 JPEG |
--threads |
0=自动 |
线程数 |
--quiet |
关 | 不打印进度 |
--all-sizes |
关 | 不按多数派尺寸过滤(默认会剔除尺寸不同的成品图) |
--no-match-exposure |
关 | 跳过逐帧亮度匹配(默认会先对齐曝光) |
| 参数 | 默认 | 说明 |
|---|---|---|
-m, --mode ★ |
bracket |
见上表 |
--bracket-radius ★ |
16 |
细节频段的候选窗口半径(帧)。0=只用夹住深度的两帧 |
--coarse-levels |
4 |
最粗的 4 层改用窄窗口。治轮廓外的宽亮带(⑩ / ⑪) |
--bracket-radius-coarse |
0 |
上述窄层的半径 |
--bracket-strength |
1.0 |
仲裁权重;0 等价 --mode pyramid |
--base-winner |
0 |
低频道(颜色/亮度)是否也参与取优。保持 0:颜色亮度跟随深度才不会出光晕 |
-l, --levels |
0=自动 |
金字塔层数(自动取可用层数的 2/3) |
--band-power |
0 |
0=取最大;2–8=能量加权平均(更干净但更软) |
--band-energy-smooth |
2 |
选赢家前把该频段能量在半径内聚合,让判决空间连贯(⑧) |
--band-energy-gate |
0.7 |
只在判决不确定处用"邻域连贯"的赢家 |
--denoise |
0 |
频段软阈值(×噪声 sigma)。默认关:能压噪声的阈值同时会抹掉光滑面的真实微纹理 |
--no-clamp-range / --no-clamp-lo / --no-clamp-hi |
夹取全开 | 不要关:两侧夹回"帧实际取值范围"是"输出不含输入没有的值"的硬保证(⑥ / ⑨) |
--clamp-slack / --clamp-hi-dilate |
0 / 0 |
夹取容差(级)与上界膨胀半径 |
| 参数 | 默认 | 说明 |
|---|---|---|
--focus |
sml |
sml / tenengrad / variance |
--gray |
luma |
luma=固定 Rec.601 权重。不要用 pca:那是逐帧归一化,见 ④ bug 4 |
--analysis-scale ★ |
1 |
深度分析的降采样倍数。默认全分辨率:降采样会先把随对焦变化的最细结构平均掉。只在大批量赶时间时用 2 |
--focus-radius ★ |
4 |
焦点测度窗口半径(全分辨率像素)。4 而不是 16:窗口跨过强边缘时深度会落到远侧表面内部,见 ⑪ bug 11 |
--focus-prefilter |
1 |
算测度前的小半径模糊,压像素级噪声底 |
--edge-aware ★ |
16 |
焦点响应先按图像加权聚合再取 argmax,窗口不再跨过主体边缘——轮廓外亮带的根因修复(⑪)。0=关 |
--near-dilate ★ |
16 |
把深度场向近侧腐蚀这么多像素:轮廓内侧那条带改判给主体。修"剪影被背景纹理啃掉、变成台阶"的缺陷(⑭)。主体是光滑低对比面、背景有浮雕时特别有效。0=关 |
--spill ★ |
20 |
去掉轮廓外那圈"近景把颜色染到远处表面"的晕(⑫⑬)。判据只用深度,背景什么颜色都行。0=关(保留物理原样) |
--edge-eps |
0.0001 |
上述引导滤波的正则项 |
--coarse-scale |
4 |
粗先验的降采样倍数(1=关) |
--refine-window |
8 |
逐像素搜索相对粗先验的半宽(帧) |
--depth-median |
2 |
深度场去斑中值半径 |
--depth-bilateral / --depth-sigma |
6 / 1.5 |
按深度相似度加权的平滑(不会把台阶摊开) |
--guided-radius / --guided-eps |
6 / 0.001 |
引导滤波半径/正则项;把深度台阶吸附到图像边缘 |
--depth-smooth |
2 |
引导滤波迭代次数 |
--conf-smooth |
32 |
判定"有无焦点信息"时的聚合半径 |
--adaptive-window / --seam-window |
0 / 0 |
候选窗口按深度不确定度张开 / 按深度变化收缩。实测对已知缺陷近中性 |
--prior-window-gain / --prior-window-radius |
1 / 8 |
粗先验的稳健统计增益与半径 |
--depth-trust |
0 |
深度可信度门控 |
| 参数 | 默认 | 说明 |
|---|---|---|
--no-align |
关 | 跳过几何对齐(三脚架极稳、且主体顶到画幅边缘时可以试) |
--align-work-size |
900 |
对齐搜索的工作分辨率 |
--align-scale-range / --align-scale-steps |
0.02 / 16 |
缩放搜索半宽与步数 |
--align-focus-blur |
3 |
注册特征前的低通半径。消除"匹配的是模糊程度而不是几何",见 ① bug 1 |
--margin |
2 |
自动裁切时额外内缩的像素 |
--no-crop / --crop x,y,w,h |
关 / — | 关掉自动裁切 / 手动指定 |
| 参数 | 说明 |
|---|---|
--save-depth <prefix> |
导出深度场(16 bit,画布尺寸;并写 _final 版本) |
--save-conf <prefix> |
导出置信度图(8 bit) |
--save-range <prefix> |
导出夹取用的已 warp 逐像素 lo/hi。判断"输出里有没有输入没有的值"的唯一权威(⑨) |
--debug-point x,y |
打印该画布坐标处每帧的焦点响应(重采样前/搬移后各一份) |
--depth-trace Y,X0,X1 |
打印某一行在深度后处理各阶段的剖面 |
--depth-fix x,y,w,h,frame |
强制指定矩形(输出图坐标)用某个帧号,可重复。手工兜底,见 开发记录 |
不想看可以跳过这一节。 简单说它分三步走:
- 给每张照片的每一小块算一个「清晰度分数」;
- 据此判断画面上每个位置离镜头多远(上一节那张彩色图就是结果);
- 按这个距离,把每张照片里清楚的部分挑出来拼合。
麻烦全在第 2 步——边缘、光滑表面、噪声这些东西都会骗过「清晰度」的判断, 下面每一层讲的都是怎么不被骗(①–⑪ 是踩过的坑的编号)。下面提到的 ①–⑪ 各 bug 的完整记录(症状、判据、机制、被推翻的假设)在 开发记录.md,这里只保留结论。
- SML(Sum of Modified Laplacian,Nayar & Nakagawa 1994)默认。对光照渐变不敏感、计算便宜、对散焦单调递减。
Tenengrad(Sobel 梯度能量)、Variance(局部方差)可选,--focus切换。
关键约束:测度必须跨帧可比,所以不做任何逐帧归一化。
另外两条同样关键的约束,都是被 bug 教出来的:
- 必须在未重采样的原帧上计算。测度对重采样相位极其敏感(见 ⑤ bug 2), 所以先在各帧自己的坐标里算出响应场,再把响应场搬到画布坐标;图像本身照旧 warp。
- 窗口大小以全分辨率像素为准(
--focus-radius默认 16,内部再除以分析尺度), 这样换分析尺度时窗口的物理大小不变。
朴素做法是每像素硬取 argmax,结果是马赛克色块。本项目的处理:
- 沿帧序做 argmax,并在局部极大值处用抛物线插值出亚像素帧号(12.4 这种)
- 无信息像素从邻域填充:原始响应峰值恒为 0 的像素(平坦 JPEG 块)不允许投票, 否则它们会以"第 0 帧"这个虚构值污染整个邻域(见 ⑤ bug 3)
- 中值滤波去孤立尖峰(不扩散边界)
- 引导滤波(Guided Filter,以全栈平均灰度为导向图)把深度场吸附到物体边缘
- 向近侧腐蚀(
--near-dilate,默认 16 px):轮廓内侧那条带改判给主体。 焦点测量窗在轮廓处必然含进背景,而主体若是光滑低对比面,窗内唯一的纹理就是背景的浮雕, 于是深度台阶落进主体内部、那条带被按背景渲染——背景的锐利纹理就印在主体剪影上, 看起来像"边缘被啃掉"。而那条带里任何一帧都不存在背景的锐利细节(背景清晰的那些帧里, 主体的虚焦像恰好盖住它),所以改判给近侧不是糊弄,参照成品也是留一条平滑带(见 ⑭ bug 14)。
早期版本用的是"置信度百分位掩膜 + 多尺度 push-pull 补洞",那条路已经放弃: 百分位必然让固定比例的像素被替换,不管它是否真的不可靠(见第 8 节第 6 条)。
-
bracket(默认):两路累积器。一路是深度插值(在深度值两侧的两帧间按小数权重混合), 另一路是逐频段在"深度值 ±K 帧"窗口内取系数能量最大者。最后按--bracket-strength线性混合。 这样既拿到逐频段的锐度,又不会让选择跑到深度场之外(那才是光晕的来源)。K(--bracket-radius)默认给到 16,比直觉上需要的大得多,这是有意的: 深度场在低对比细节处仍是弱环——有些表面(光滑树脂、无浮雕的墙面)根本没有焦点信息 (实测:整条栈上的响应曲线是平的,没有任何峰),那里的深度是填补值, 而这个窗口正是它的补救:窗口够宽就能碰到真正清晰的那一帧,让该频段自己的能量把它选出来。 之所以不产生光晕,是因为只有细节频段可以游走,颜色和亮度始终跟随深度值。 实测 K 从 0 加到 16:墙面轮廓锐度 21.98 → 23.33、叶片 22.71 → 24.44、面部 55.36 → 58.23 (参照成品分别是 19.13、20.90、52.10),而最平坦区域的颗粒度没有变差(见第 4 节)。 以前"窗口越宽噪声越大"那个现象,是深度场算错造成的,不是窗口的固有代价。深度修好之后(见下方"灰度转换"一节),K 的效果在 4 以上就进入平台: K=4 / 8 / 16 的墙面分别是 23.18 / 23.35 / 23.33。 K 仍保留 16,因为无信息的表面确实依赖它。
顺带一提:正因为这个窗口在兜底,焦点测度的精度对最终画质影响很小。 实测把
--focus从 SML 换成 Tenengrad(后者在低对比区对真实锐度的排序相关性 0.895 对 0.549、动态范围 3.1× 对 1.5×,明显更好),端到端输出完全一样 (gradE 6.747 对 6.757,各区域轮廓锐度逐位相同)。要提升画质,应该去修深度场 或者放宽窗口,而不是换测度。 -
pyramid:只有深度插值那一路。最平滑,但系统性丢细节(实测只到理论上限的 60%)。 -
max:全栈逐频段取最大系数。最锐,但选择无约束,光晕和噪点风险最高。 -
pixel/hard:像素域过渡 / 硬选帧,用于快速预览和检查深度图。
逐像素"取最大"是一个极值统计量:即使所有帧在该处都没有真实信号,N 帧里的最大值也系统性地高于任何单帧。
这是本项目从第一轮起的核心担心,也是 --denoise 存在的理由。
但这个担心在本素材上没有被证实。 早期的"验证"用的是整块背景的纹理 std 或高频能量, 那些量同时包含真实细节,所以看上去总是在支持"我们更噪"。 直接去测全图最平坦的那块 200×200(在参照成品上按梯度能量找出来)得到的是相反的结论: 颗粒度 1.332 对参照成品的 1.400——最平坦的地方我们更干净。 全图上多出来的高频是真实细节(见第 4 节)。
所以频段软阈值默认关闭(--denoise 0),而且实测它在任何档位下都是净损失:
0.08 / 0.15 让各区域锐度全线下降,而背景 std 只降了 0.6。
如果哪天真要压噪,--band-power 2~8 是更温和的一条路(见 ⑤ 的取舍曲线)——
它在那里平均噪声、在真实结构处几乎等于取最大帧。
结论:要细节,不要阈值。 只有当素材本身噪点确实很大时才考虑这两条路。
[1/4] 解码 + 缓存 每帧解码一次 → **固定权重亮度**(Rec.601,逐帧相同)
→ 缓存为 16bit;50 帧 33MP 约 3.3 GB(若存 f32 需 6.5 GB)
[2/4] 对齐 工作分辨率上**先低通**(消掉离焦差异,只留几何)
→ 粗搜尺度 → 精搜 → 每个候选用 2D FFT 相位相关求残差平移
[3/4] 深度场 在**未 warp 的原帧**上算 SML(默认全分辨率)
→ argmax + 抛物线亚像素细化
→ 原始响应为 0 的像素从邻域填充 → 中值 → 引导滤波 → 向近侧腐蚀
响应场按变换搬到画布;图像本身仍 warp,供引导和融合使用
[4/4] 融合 再解码一次(一次一帧),拉普拉斯金字塔
逐频段深度插值 + 窗口内系数能量仲裁 → 塌缩 → 16bit PNG
内存策略:解码结果一次只驻留一帧,融合只保留累积器金字塔。
50 帧 33MP 的全分辨率灰度缓存约 3.3 GB,加融合缓冲峰值约 5–6 GB
(若把 50 帧 f32 RGB 全放内存需要 19 GB)。
把 --analysis-scale 设为 2 可把缓存降到约 0.8 GB,代价是深度精度下降。
对一组 50 张的 33MP 手办微距素材先做侦察(下面的数字都出自这一组):
- 7008×4672(33MP,索尼 A7R 系列),50 帧
- 平移 = 0:三脚架拍摄,帧间位移在 ±4px 内
- 对焦呼吸缩放真实存在,但幅度比早期估计小。用"先重模糊再搜缩放"的对焦无关 方法测得 scale 从第 0 帧的 0.9955 平滑单调升到第 49 帧的 1.0045(全程约 0.9%, 即两端相对中段各约 ±0.45%)。必须修正,但绝不足以解释任何大范围错位—— 早期估出的 1.0075 和 8 像素平移,都是估计器被对焦污染的产物(见 ⑤ bug 1)
- 曝光漂移:全图均值 163.55 → 166.28(约 1.7%)
- 整体梯度能量在第 12 帧达峰后单调下降,焦点扫掠的远端大幅超出景深
- 同目录还有
AF导出.png、堆叠.af、合并.psd、抠图.psd等成品/工程文件
所以:对齐模块不能省,但不必用 ECC;且目录扫描必须按像素尺寸过滤, 否则会把成品图当成一帧(本项目自动剔除少数派尺寸并打印提示)。
新手容易拿"单张最锐的帧"当上限 —— 那是错的。焦点堆叠的正确上限是 逐像素取全栈最大梯度(每个像素都从它最清晰的那帧取,融合不可能比这更锐)。
在参照成品的裁切区域内(4275×4002):
| 方案 | 梯度能量 | 占理论上限 | 最平坦区域颗粒度 |
|---|---|---|---|
| 理论上限(逐像素取全栈最大) | 7.77 | 100% | — |
max(不做深度约束) |
6.97 | 89.7% | — |
bracket 默认(全分辨率 + 窗口 16) |
6.90 | 88.8% | 1.332 |
| AF导出(Affinity 成品,含人工修图) | 5.32 | 68.4% | 1.400 |
(pyramid 纯深度插值 4.69 / 60.4%;单张最锐原帧 2.65 / 34.1%。)
max 的 gradE 略高,但它是不做深度约束换来的:在同一组区域里
背景墙 22.38 对默认的 23.33、颗粒度 4.228 对 4.125。
所以默认仍是 bracket。
两个结论:
- 任何单帧都远不是上限(34%),焦点堆叠的价值就在这里。
- "梯度能量高就是噪点"这个担心在本场景不成立:在最平坦的 200×200 区域, 我们的颗粒度是 1.332,反而低于参照成品的 1.400。全图上多出来的高频能量是真实细节。 (早期版本用整块背景的纹理 std 当噪声判据,那个量同时包含细节,因此误导了好几轮。)
梯度能量/纹理 std 这一对不够 —— 它们把细节和颗粒混在一个数里。
判断"看起来锐不锐"要看轮廓陡度:区域内最强 2% 像素的平均梯度幅值
(分位 0.98,即 tools/patchlib.py 的 acutance)。
判断"是否有噪声"则要看最平坦区域的颗粒度(见第 4 节上半),
两者分开之后结论就不再自相矛盾了。
(早期这里写着"实测 max 模式在叶片上细节能量最高、看起来却最糊"——
那是在三个 bug 之上测的。管线修好后 max 与默认在叶片上是 24.41 对 24.44,
没有差别;而 max 在没有深度约束的地方反而更差,见 ⑤。)
在参照成品裁切区内的 6 个典型区域,与 AF导出 的轮廓锐度之比。原点自动定位, 复现命令:
STACK_DIR="<你的素材目录>" python tools/patchlib.py "ours=out/merged.png"| 区域 | AF导出(绝对值) | mukastack | 比值 |
|---|---|---|---|
| 背景墙(浮雕) | 19.13 | 21.73 | 1.14 |
| 膝盖边缘 | 47.12 | 51.24 | 1.09 |
| 光滑身体 | 47.52 | 51.38 | 1.08 |
| 底座叶片(光滑树脂) | 20.90 | 22.16 | 1.06 |
| 面部(高对比,对照组) | 52.10 | 53.22 | 1.02 |
| 平坦背景 | 28.29 | 29.36 | 1.04 |
六个区域全部高于参照成品,而参照成品是带局部修图的(合并.psd/抠图.psd 也在同目录)。
AF导出 那一列与本表逐位相同,说明是同一批区域、同一个指标;我们这一列是
当前默认参数重测的(2026-09-24)。这张表早先印的 1.22/1.19/1.19/1.17/1.12/1.14
是更早一版默认输出的数字,可复现(out/实验记录/prev_default.png,gradE 6.90 / 颗粒 4.13)——
其后为压颗粒、去斑块、修剪影做的几次取舍让这项指标降了约 7~9%
(现在 gradE 5.96 / 颗粒 3.84),而表没有跟着更新。想换回偏锐那一档见 --band-energy-gate。
src/
main.rs CLI、流水线编排、目录扫描与尺寸过滤、输出
util.rs Plane / RgbImage 容器、盒滤波、高斯滤波、Catmull-Rom 插值、稳健统计
pyramid.rs 5 抽头二项式核的高斯/拉普拉斯金字塔、塌缩
focus.rs SML / Tenengrad / 局部方差 焦点测度
depth.rs 流式 argmax + 亚像素细化、无信息像素判定与补洞、深度域双边平滑、中值
align.rs 低通后的梯度特征、2D FFT 相位相关、两级尺度搜索、三次插值重采样
fuse.rs 裁切计算、双累积器金字塔融合、能量加权候选合并、频段软阈值去噪
tools/ Python 侦察/评估脚本(不参与构建,见下)
docs/ README 里用到的示例图(都是本工具在这套素材上的真实输出)
这些脚本不参与 cargo 构建,是我用来量化"这一版比上一版好还是差"的独立工具(Python + numpy + pillow)。 它们的价值在于判据:每个脚本都在注释里写清楚了它量的是什么、以及什么情况下它会骗人。
| 脚本 | 作用 |
|---|---|
patchlib.py |
逐区域对比的标准入口。自动定位每个输出图的裁切原点,再给轮廓锐度/颗粒度。被下面多个脚本当库用 |
eval_all.py |
批量评估多个输出图,输出分区域轮廓锐度与整体差异 |
truth.py |
独立真值:直接在解码原帧上算局部锐度,给出各位置的"真实最锐帧" |
depthcheck.py |
在指定位置采样深度场,与真值逐点对照 |
bandlib.py |
轮廓带测量的公共库(盒和、纹理、主体掩膜、局部对比) |
bandeval.py |
轮廓带的纹理比 + gradE + 颗粒;--frames 可取原帧作参照 |
bandtruth.py |
轮廓外亮度随距离的变化,参照=逐像素取深度对应帧 |
depthtruth.py |
深度真值:把主体排除出测度窗口后重新取最锐帧,给出逐距离的深度误差与亮度偏差 |
sidetruth.py |
上者的两侧版本(主体侧也各自排除对面),用来判断亮边在哪一侧 |
banddelta.py |
相对"墙面合焦帧"的亮度增量(单帧基准) |
bandexcess.py |
自参照的"超出远端拟合"量 |
sideprofile.py |
轮廓两侧的纹理/亮度剖面 |
deptherrmap.py / rimmap.py |
深度误差 / 亮度残差热力图(蓝=偏近或偏暗,红=偏远或偏亮) |
bandchart.py / bandchart_cells.py |
距离-亮度曲线与"减去真值"的曲线 |
bandview.py / mkzoom.py |
背景去除放大对比 / 通用高倍并排 |
overshoot.py |
输出是否越出"帧实际取值范围"(要看 --save-range 导出的已 warp 范围) |
mkreview_band.py / mkreview.py |
把一次改动的证据打成一个可浏览的检查包(out/检查_*/index.html) |
其余脚本是开发过程中针对具体疑点写的一次性探针(bloom.py、softscan.py、edgeband.py、
distdecay.py、stepoffset.py、measure_probe.py、pixel_probe.py、argmax_probe.py、
seam.py、confcal.py、recon.py、breath.py、scale_test.py、colour_audit.py、diag.py、
depthview.py、selmap.py、softmap.py、localize.py、ideal.py、ceiling.py、compare.py、
strip.py、threeway.py、grid.py、probe_*.py…)。
它们的坐标约定可能过时(尤其是硬编码裁切原点的那些),结论以 开发记录.md 里的记录为准。
eval_all.py/patchlib.py→ 找出与参照成品差异最大的区域,确认坐标属于哪张图 (输出图 / 参照裁切图 / 原始画布三者互差一个偏移)mkzoom.py、bandview.py→ 放大目视,先看清楚"不对"是什么形态(过冲?发虚?色偏?)- 需要数字时,按现象选判据:
- 越界/光晕 →
--save-range+overshoot.py - 轮廓外的亮带 →
bandtruth.py/depthtruth.py(真值必须独立于被测对象) - 光滑面颗粒 →
grain.py - 区域锐度 →
patchlib.py
- 越界/光晕 →
- 深度可疑 →
--save-depth+depthtruth.py+deptherrmap.py - 还不清楚 →
--debug-point x,y看工具自己看到的逐帧响应
一条反复踩出来的规矩:参照不能由被测对象自己生成(拿工具自己的深度图去挑参考帧, 深度错在哪参照就错在哪,差值恒等于零),以及比值判据必须先确认分母没被一起改动。
这些不是"以后再说"的待办,是量过之后的取舍,知情比修好更重要:
- 光滑表面上的噪声与真实微纹理不可分离。 树脂、漆面、皮肤上,每像素响应的信噪比接近 1,
任何能压住噪声的阈值都会同时抹掉真实纹理(视觉上就是"糊")。所以
--denoise默认关, 噪声按原样保留——保留噪声比抹掉细节更接近真实。素材本身噪声大时才建议开。 - 紧贴轮廓 1–3 像素有一道很细的亮边。 这是焦点堆叠在硬边缘上的过冲(拉普拉斯振铃), 两侧夹取已经砍掉大部分。人工修过的参照成品在同一位置更明显(+10.2 对 +3.9 级), 从轮廓外 5 像素起本工具反而更干净。要再压只能动"沿边缘的过冲"本身,见开发记录。
- 深度在低对比表面上仍可能局部读错(一个大平面上它的浮雕被读成别的帧)。
--bracket-radius的宽候选窗口就是为此存在的:它让细节频段有机会跳到正确的帧上去。所以这个参数故意开得偏大。 - 墙面浮雕靠近主体时本来就变淡(光照与反射的实际情况,连墙面合焦的原帧也一样), 加上主体的边缘过冲,容易被看成"近处物体在发光"。本工具是忠实复现,不是造出来的。
- 帧间位移超过 ~2 像素、或明显旋转/透视变化不在设计目标内(对齐是全局相似变换)。
- 时间与内存随
帧数 × 像素数线性增长;50 帧 33MP 一次约 3–4 分钟、峰值 5–6 GB。
开发记录.md 是这套代码为什么长成这样的原始记录,包含:
- 十四个决定性 bug:每个的症状、判据、机制、修法、以及事后被推翻的假设
- 判据本身的三次更正("背景墙是弱项"、"带内纹理比 <1 所以浮雕被抹掉"、 以及首页那张轮廓锐度表里我们自己那一列,都是我量错了)
- 一批负面结果:试过、无效、别再重复的参数与思路
- 复盘:这个项目里最容易骗人的坑(指标与目视冲突时先怀疑指标;参照不能用被测对象自己生成; 比值判据必须先确认分母没被一起改动……)
docs/ 与 README 里的示例照片是本项目作者 Attect 拍摄的(索尼 33MP × 50 帧包围对焦),
用于展示本工具的效果;转载时请注明拍摄者并保留本说明。
照片里的手办是游戏《原神》角色「可莉」的立体化商品,角色形象与造型设计的著作权归 米哈游(miHoYo)所有。本项目与米哈游没有任何关联,也未经其授权、赞助或认可; 这些照片仅用于技术演示,不作商业用途。完整说明见 docs/CREDITS.md。
代码:MIT,见 LICENSE。
tools/ 里的脚本需要指向素材目录与参照成品(我自己那套是私有的,所以没有硬编码):
export STACK_DIR="D:/photos/stack" # 素材目录
export REF_EXPORT="D:/photos/reference.png" # 可选:人工修过的成品,作为对比基准
python tools/patchlib.py "my output"=out/merged.png这些脚本是我开发时给自己造的测量台,tools/ 里所有中间产物都读写项目目录下的 out/
(ROOT 由脚本自身位置推出,不含任何机器相关的绝对路径)。脚本很多、参数各自不同,
要照抄的话先读代码开头的说明;tools/patchlib.py 与 tools/eval_all.py 是最常用的两个。


