Skip to content

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Muka-Focus-Stacking

把一组「每张只有一部分清楚」的照片,合成一张从头到尾都清楚的照片。

拍微距、手办、静物这类近景时,光圈收得再小也只能让薄薄一层清楚——前面的花瓣清楚了,后面的就糊。 常规做法是把焦点从近到远扫一遍、连拍一串(这叫包围对焦 / 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

可莉手办:50 张包围对焦照片的合成结果

合成结果(缩略图,1600px)。原始素材 7008×4672、共 50 张。 照片是我自己拍的,拍摄对象的版权说明见 照片与版权。

这些数字怎么量出来的、每个参数为什么是这个值,写在下面的「工作原理」和 开发记录.md 里。只想出图的话,看下一节就够了。


安装与第一次使用

方式一:下载现成的 exe(推荐,不用装任何东西)

  1. 到 Releases 下载 mukastack.exe,比如放到 D:\tools\。
  2. 把那组包围对焦照片放到一个目录,比如 D:\photos\stack\。
  3. 按 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 放大,没有做任何锐化或修图。 左边两张是原片,右边是本工具的输出:

单帧 vs 合成

裁的是可莉的面部与右侧背景墙。第 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 强制指定矩形(输出图坐标)用某个帧号,可重复。手工兜底,见 开发记录

工作原理

不想看可以跳过这一节。 简单说它分三步走:

  1. 给每张照片的每一小块算一个「清晰度分数」;
  2. 据此判断画面上每个位置离镜头多远(上一节那张彩色图就是结果);
  3. 按这个距离,把每张照片里清楚的部分挑出来拼合。

麻烦全在第 2 步——边缘、光滑表面、噪声这些东西都会骗过「清晰度」的判断, 下面每一层讲的都是怎么不被骗(①–⑪ 是踩过的坑的编号)。下面提到的 ①–⑪ 各 bug 的完整记录(症状、判据、机制、被推翻的假设)在 开发记录.md,这里只保留结论。

① 焦点测度 —— 每个像素"在哪一帧最清晰"

  • SML(Sum of Modified Laplacian,Nayar & Nakagawa 1994)默认。对光照渐变不敏感、计算便宜、对散焦单调递减。
  • Tenengrad(Sobel 梯度能量)、Variance(局部方差)可选,--focus 切换。

关键约束:测度必须跨帧可比,所以不做任何逐帧归一化。

另外两条同样关键的约束,都是被 bug 教出来的:

  • 必须在未重采样的原帧上计算。测度对重采样相位极其敏感(见 ⑤ bug 2), 所以先在各帧自己的坐标里算出响应场,再把响应场搬到画布坐标;图像本身照旧 warp。
  • 窗口大小以全分辨率像素为准(--focus-radius 默认 16,内部再除以分析尺度), 这样换分析尺度时窗口的物理大小不变。

② 深度场 —— 把"哪一帧最清晰"变成连续场

朴素做法是每像素硬取 argmax,结果是马赛克色块。本项目的处理:

  1. 沿帧序做 argmax,并在局部极大值处用抛物线插值出亚像素帧号(12.4 这种)
  2. 无信息像素从邻域填充:原始响应峰值恒为 0 的像素(平坦 JPEG 块)不允许投票, 否则它们会以"第 0 帧"这个虚构值污染整个邻域(见 ⑤ bug 3)
  3. 中值滤波去孤立尖峰(不扩散边界)
  4. 引导滤波(Guided Filter,以全栈平均灰度为导向图)把深度场吸附到物体边缘
  5. 向近侧腐蚀(--near-dilate,默认 16 px):轮廓内侧那条带改判给主体。 焦点测量窗在轮廓处必然含进背景,而主体若是光滑低对比面,窗内唯一的纹理就是背景的浮雕, 于是深度台阶落进主体内部、那条带被按背景渲染——背景的锐利纹理就印在主体剪影上, 看起来像"边缘被啃掉"。而那条带里任何一帧都不存在背景的锐利细节(背景清晰的那些帧里, 主体的虚焦像恰好盖住它),所以改判给近侧不是糊弄,参照成品也是留一条平滑带(见 ⑭ bug 14)。

早期版本用的是"置信度百分位掩膜 + 多尺度 push-pull 补洞",那条路已经放弃: 百分位必然让固定比例的像素被替换,不管它是否真的不可靠(见第 8 节第 6 条)。

③ 融合 —— 三种策略,bracket 为默认

  • 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。

两个结论:

  1. 任何单帧都远不是上限(34%),焦点堆叠的价值就在这里。
  2. "梯度能量高就是噪点"这个担心在本场景不成立:在最平坦的 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 里用到的示例图(都是本工具在这套素材上的真实输出)

分析工具(tools/)

这些脚本不参与 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 里的记录为准。

诊断顺序(遇到"某处不对"时)

  1. eval_all.py / patchlib.py → 找出与参照成品差异最大的区域,确认坐标属于哪张图 (输出图 / 参照裁切图 / 原始画布三者互差一个偏移)
  2. mkzoom.py、bandview.py → 放大目视,先看清楚"不对"是什么形态(过冲?发虚?色偏?)
  3. 需要数字时,按现象选判据:
    • 越界/光晕 → --save-range + overshoot.py
    • 轮廓外的亮带 → bandtruth.py / depthtruth.py(真值必须独立于被测对象)
    • 光滑面颗粒 → grain.py
    • 区域锐度 → patchlib.py
  4. 深度可疑 → --save-depth + depthtruth.py + deptherrmap.py
  5. 还不清楚 → --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 是最常用的两个。

About

纯 Rust 的焦点堆叠(景深合成)工具:把一组包围对焦照片合成一张全清晰图片

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages