Muka Focus Stacking 是一个纯 Rust 写的焦点堆叠(景深合成)命令行工具。拍微距、手办、静物这类近景时,光圈收得再小也只能让薄薄一层清楚——前面的花瓣清楚了,后面的花蕊就糊。常规做法是把焦点从近到远扫一遍、连拍一串(这叫包围对焦 / focus bracketing),回来再用软件把每一张里清楚的那部分挑出来拼合。这个工具就干这件事:全程在你自己的电脑上跑,不上传、不联网、不限张数、不留水印。
你需要准备的:三脚架(至少机位不能动),以及一组同一构图、焦点从近到远逐张推进的照片——多数相机自带的「包围对焦」就能拍,手动一张张挪焦点也行。格式不限(JPEG / TIFF / PNG 都吃),RAW 请先转成 TIFF 或 JPEG;张数多一点更保险。
它和现成方案的区别只有一句:画质目标是「比人工精修过的成品更锐」,而不是「能合上就行」。
实测效果
文章顶部的图就是合成结果(缩略图 1600px,未做任何锐化或修图):原始素材 7008×4672(33MP)、共 50 张包围对焦照片,照片为作者本人拍摄,版权说明见文末。
同一组素材与在 Affinity Photo 里手工修过的成品逐区域比对轮廓锐度(最强 0.5% 边缘像素的梯度峰值)——数字大于 1 就是比它更清楚,六个区域全部更高:
| 区域 | 人工精修成品 | 本工具 | 比值 |
|---|---|---|---|
| 背景墙(浮雕) | 19.13 | 23.33 | 1.22 |
| 膝盖边缘 | 47.12 | 56.27 | 1.19 |
| 光滑身体 | 47.52 | 56.65 | 1.19 |
| 底座叶片(光滑树脂) | 20.90 | 24.44 | 1.17 |
| 面部(高对比,对照组) | 52.10 | 58.23 | 1.12 |
| 平坦背景 | 28.29 | 32.14 | 1.14 |
同时没有更噪:全图最平坦的那块 200×200 区域,本工具的颗粒度是 1.332,反而低于参照成品的 1.400——多出来的高频是真实细节,不是噪点。
安装与第一次使用
方式一:到 Releases 下载 mukastack.exe(约 3.0 MB,单文件、不需要额外运行库),比如放到 D:\tools\。它是命令行工具、没有图形界面,直接双击会一闪而过,要这样调用:
D:\tools\mukastack.exe "D:\photos\stack" -o "D:\photos\stack\merged.png"
跑完之后(50 张 3300 万像素约 3–4 分钟),输出文件就在 -o 指定的位置。默认参数就是最高画质,什么都不用加。 输出是 16 bit PNG(8 bit 会在天空、墙面这类渐变处出现看得见的色带)。首次运行 Windows 的 SmartScreen 可能拦一下,选「仍要运行」即可(exe 没有数字签名)。
方式二:自己编译(需要 Rust 1.85+):
cargo install --path . # 装好后任何目录都能直接敲 mukastack
cargo build --release # 或者只要一个 exe:target/release/mukastack.exe
常用命令:
# 最常用:给一个目录就行(自动按文件名排序、自动剔掉尺寸不一样的成品图)
mukastack "照片目录" -o merged.png
# 顺便出一张 1600px 的 JPEG 小图,方便快速看效果
mukastack "照片目录" -o merged.png --preview preview.jpg
# 素材特别大又赶时间:清晰度分析降到 1/2 分辨率(缓存约 0.8 GB)
mukastack "照片目录" -o merged.png --analysis-scale 2
# 也可以直接列文件名,只合其中几张
mukastack a.jpg b.jpg c.jpg -o merged.png
它不挑显卡,但比较吃内存:约为「张数 × 单张像素 × 3 字节」——50 张 3300 万像素约 3.3 GB 缓存、峰值 5–6 GB。
三种「拼合风格」怎么选
对应 --mode,默认的 bracket 就够用:
| 模式 | 大白话 | 什么时候换 |
|---|---|---|
bracket(默认) | 颜色和亮度按深度走,细节频段在附近几张里挑最清晰的那张 | 不用换 |
pyramid | 一切按深度平滑过渡,不做取舍 | 素材噪声很大,想要最干净、能接受偏软 |
max | 每个频段都在全组里挑最锐的那张 | 只想看细节极限;会带光晕和噪声 |
pixel、hard 两个模式是给对照实验留的,日常不用。
工作原理
简单说它分三步走:给每张照片的每一小块算一个「清晰度分数」;据此判断画面上每个位置离镜头多远(深度场);按这个距离,把每张照片里清楚的部分挑出来拼合。麻烦全在第 2 步——边缘、光滑表面、噪声这些东西都会骗过「清晰度」的判断,这个工具的处理是:
- 焦点测度:默认 SML(Sum of Modified Laplacian),对光照渐变不敏感、计算便宜、对散焦单调递减;测度必须跨帧可比,所以不做任何逐帧归一化;必须在未重采样的原帧上计算(测度对重采样相位极其敏感),窗口大小以全分辨率像素为准,换分析尺度时窗口的物理大小不变;
- 深度场:沿帧序 argmax,在局部极大值处用抛物线插值出亚像素帧号(12.4 这种);原始响应恒为 0 的「无信息像素」(平坦 JPEG 块)从邻域填充,不允许用虚构值污染邻域;中值滤波去孤立尖峰(不扩散边界);引导滤波把深度台阶吸附到物体边缘;再向近侧腐蚀(默认 16 px),把轮廓内侧那条带改判给主体,修掉「剪影被背景纹理啃掉、变成台阶」的缺陷;
- 融合:拉普拉斯金字塔双累积器——一路深度插值(在深度值两侧的两帧间按小数权重混合)保住颜色和亮度,一路逐频段在「深度值 ± 16 帧」窗口内取系数能量最大者保住锐度,最后线性混合。窗口故意开得比直觉大:光滑树脂、无浮雕墙面这类表面根本没有焦点信息,那里的深度是填补值,宽窗口正是它的补救——让细节频段自己的能量把它选出来;之所以不产生光晕,是因为只有细节频段可以游走,颜色和亮度始终跟随深度值。
质量上限的参照系:逐像素取全栈最大梯度的「理论上限」梯度能量为 7.77,默认 bracket 6.90(88.8%),不做深度约束的 max 6.97(89.7%,但背景墙更差、颗粒度更高),人工精修成品 5.32(68.4%),单张最锐的原帧只有 2.65(34.1%)——任何单帧都远不是上限,焦点堆叠的价值就在这里。
诊断与检查
--save-depth导出深度场(16 bit):颜色代表张号,蓝=离镜头近、红=远,可用来检查有没有把背景误判成前景;--save-range导出「已 warp 的逐像素取值范围」:判断「输出里有没有输入没有的值」的唯一权威;--save-conf导出置信度图(8 bit);--debug-point x,y打印该坐标处每帧的焦点响应;--depth-fix x,y,w,h,frame强制指定矩形用某个帧号,可重复,手工兜底。
代码结构
| 文件 | 职责 |
|---|---|
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/ 下还有一套不参与 cargo 构建的 Python 侦察/评估脚本(patchlib.py、eval_all.py、truth.py 等),是开发时用来量化「这一版比上一版好还是差」的独立测量台;十四个决定性 bug 的完整记录(症状、判据、机制、被推翻的假设)写在仓库的 开发记录.md 里。
已知限制与取舍
这些不是「以后再说」的待办,是量过之后的取舍:
- 光滑表面上的噪声与真实微纹理不可分离:树脂、漆面、皮肤上每像素响应的信噪比接近 1,任何能压住噪声的阈值都会同时抹掉真实纹理,所以
--denoise默认关闭——保留噪声比抹掉细节更接近真实; - 紧贴轮廓 1–3 像素有一道很细的亮边(拉普拉斯振铃),人工修过的参照成品在同一位置反而更明显(+10.2 对 +3.9 级),从轮廓外 5 像素起本工具更干净;
- 深度在低对比表面上仍可能局部读错,宽候选窗口就是为此存在的,所以这个参数故意开得偏大;
- 帧间位移超过约 2 像素、或明显旋转/透视变化不在设计目标内(对齐是全局相似变换);
- 时间与内存随「帧数 × 像素数」线性增长。
开源协议
代码以 MIT License 开源,见仓库 LICENSE。
版权说明:README 与
docs/中的示例照片为作者本人拍摄(索尼 33MP 相机 × 50 帧包围对焦),照片中的手办是游戏《原神》角色「可莉」的立体化商品,角色形象与造型设计的著作权归米哈游(miHoYo)所有;这些照片仅用于技术演示,不作商业用途。本项目与米哈游没有任何关联,也未经其授权、赞助或认可。完整说明见仓库docs/CREDITS.md。
