depth_from_focus 详解

一句话先理解:

Depth From Focus(DFF)就是:让同一个物体在不同焦距/不同 Z 位置下成像,比较每个像素在不同位置的"清晰度",哪个位置最清晰,就认为物体表面就在那个深度。

它本质上是:

焦点搜索+清晰度评价+深度映射​

1. Depth From Focus 是什么?

Depth From Focus,简称 DFF,中文一般叫:

基于聚焦的深度估计 / 离焦深度恢复

它解决的问题是:

只有一台普通相机,通过改变相机/物镜与物体之间的焦距或 Z 位置,从一组不同焦平面的图像中恢复物体的 3D 深度。

例如,假设我们拍了 5 张图:

它和 Shape From Focus 是什么关系?

可以简单理解成:

Shape From Focus

强调:

从焦点变化恢复物体表面形状。

Depth From Focus

强调:

从焦点变化恢复每个像素对应的深度。

工程上两者高度相关。

你可以把它们统一成:

二、核心:"清晰度"得到深度

为什么"清晰度"可以得到深度?这才是 DFF 的核心。

假设显微镜沿 Z 轴扫描: z1​,z2​,⋯,zN​

物体表面某一点真实高度为: z0​

当: z=z0​ 的时候,这一点正好处于最佳焦平面。

于是 清晰度在 z=z0​ 附近达到最大​

所以: z0​≈argzmax​F(z)

a、光学解释

一个理想图像可以表示为: I(x,y)

经过光学系统以后: Iimage​=Iobject​∗PSF

其中: PSF

是点扩散函数。

b、完全聚焦

当: Δz=0

PSF 比较集中:

清晰:

│█│ │█│ │█│ │█│ │█│

c、离焦

当: Δz=0

PSF 扩散:

复制代码
              PSF
               ↑
          _____│_____
       __/           \__
______/                 \______

原来的高频结构被平均:

复制代码
原始:

│█│ │█│ │█│ │█│

离焦:

████████████████

所以:

离焦⇒PSF变宽⇒高频衰减⇒图像变模糊​

因此: 清晰度⟷高频信息​

而 DFF 就是利用这个关系寻找最佳焦点。

d、Laplacian Focus Measure

Focus Measure(聚焦评价函数)中的 Laplacian-CSDN博客

其中 W 是一个局部窗口。

为什么它可以判断清晰度?

因为: Laplacian≈二阶高频响应​

e、Tenengrad 做 DFF

R 复制代码
                DFF
                 │
                 ▼
          Focus Measure
                 │
       ┌─────────┼─────────┐
       ▼         ▼         ▼
   Laplacian   Sobel    Variance
       │         │         │
       ▼         ▼         ▼
     高频       梯度      局部对比度

f、DFF 的完整数学模型

加入我们采集了N个图像

R 复制代码
Focus
  ↑
  │
  │                 ●
  │              ●     ●
  │            ●         ●
  │         ●               ●
  │      ●
  │   ●
  └────────────────────────────→ Z
             ↑
           z*

那么: z∗(x,y)=argimax​Fi​(x,y)​

最终: D(x,y)=z∗(x,y)

得到深度图:

sql 复制代码
                    D(x,y)

       ┌─────────────────────┐
       │  0.52  0.53  0.54   │
       │  0.51  0.52  0.55   │
       │  0.50  0.51  0.54   │
       │  0.49  0.50  0.52   │
       └─────────────────────┘

如果直接:z∗=argmaxF

那么深度只能得到: z1​,z2​,z3​,⋯

这些离散位置。比如: z=0,10,20,30,40 μm

实际最佳焦点可能是: 17.3μm

但你的扫描只有: 10μm,20μm

所以: argmaxF

只能告诉你: z∗=20μm

精度只有扫描步长级别。

所以真正的 DFF 会做亚步长焦点估计这是非常关键的一步。

假设: Fi−1​,Fi​,Fi+1​

分别对应: zi−1​,zi​,zi+1​

而 Fi​ 最大,可以用二次函数拟合 Focus Curve:

三、数学逻辑

cpp 复制代码
depth_from_focus (
    Image,
    Depth,
    Confidence,
    'highpass',
    'next_maximum')

四、案例说明

halcon 案例说明

复制代码
* ============================================================
* Extract depth using multiple focus levels
* 基于多个焦平面图像进行 Depth From Focus(DFF)深度提取
*
* 核心思想:
*
*   10张不同Z位置/不同焦距下采集的图像
*                  ↓
*          depth_from_focus
*                  ↓
*        每个像素寻找最佳焦点
*                  ↓
*             Depth
*                  ↓
*          得到物体表面高度
*
* 同时输出:
*   Depth      :每个像素估计得到的深度/焦点位置
*   Confidence :该深度估计结果的可信程度
* ============================================================


* ------------------------------------------------------------
* Names:保存10张不同焦平面的图像文件名
* ------------------------------------------------------------

Names := []


* 关闭当前所有显示窗口
dev_close_window ()


* ------------------------------------------------------------
* 生成10张图像的文件名
*
* i = 1,2,...,10
*
* 'dff/focus_' + (i$'.2')
*
* 其中:
*
* i$'.2'
*
* 表示把整数格式化成两位数字:
*
* 1  -> 01
* 2  -> 02
* ...
* 9  -> 09
* 10 -> 10
*
* 最终:
*
* dff/focus_01
* dff/focus_02
* ...
* dff/focus_10
*
* 这些图像分别对应不同的焦平面。
* ------------------------------------------------------------

for i := 1 to 10 by 1

    Names := [Names,'dff/focus_' + (i$'.2')]

endfor


* ------------------------------------------------------------
* read_image
*
* 读取上面 Names 中指定的10张图像。
*
* HALCON 会将多张图像作为一个多通道图像对象处理。
*
* 可以理解成:
*
* Image =
*
*   Channel 1 → focus_01
*   Channel 2 → focus_02
*   ...
*   Channel 10 → focus_10
*
* 这里的10个channel并不是RGB意义上的颜色通道,
* 而是10个不同焦点位置采集的灰度图像。
* ------------------------------------------------------------

read_image (Image, Names)


* ------------------------------------------------------------
* channels_to_image
*
* 将当前的图像对象重新组织成一个多通道图像。
*
* 此时可以把 Image 理解为:
*
*                 Image
*                   │
*       ┌───────────┼───────────┐
*       ↓           ↓           ↓
*   Channel 1   Channel 2   ... Channel 10
*   focus_01    focus_02        focus_10
*
* 每一个channel对应一个不同的焦点位置。
* ------------------------------------------------------------

channels_to_image (Image, Image)


* ------------------------------------------------------------
* 获取图像宽度和高度
*
* Width  :图像宽度
* Height :图像高度
*
* 后面主要用于创建显示窗口。
* ------------------------------------------------------------

get_image_size (Image, Width, Height)


* ------------------------------------------------------------
* 创建一个显示窗口
*
* 参数:
*
* 0,0
*     窗口左上角位置
*
* Width, Height
*     窗口尺寸
*
* 'black'
*     窗口背景颜色
*
* WindowHandle
*     输出窗口句柄
* ------------------------------------------------------------

dev_open_window (0, 0, Width, Height, 'black', WindowHandle)


* ============================================================
* Access a channel of a multichannel image
* 访问多通道图像中的某一个通道
* ============================================================

for i := 1 to 10 by 1

    * --------------------------------------------------------
    * access_channel
    *
    * 从多通道图像 Image 中提取第 i 个 channel。
    *
    * Image1:
    *     当前第 i 个焦点位置对应的图像
    *
    * 例如:
    *
    * i = 1
    *     Image1 = focus_01
    *
    * i = 2
    *     Image1 = focus_02
    *
    * ...
    *
    * i = 10
    *     Image1 = focus_10
    *
    * --------------------------------------------------------

    access_channel (Image, Image1, i)

endfor


* ============================================================
* Depth From Focus
* ============================================================

* ------------------------------------------------------------
* depth_from_focus
*
* 这是整个程序最核心的算子。
*
* 输入:
*
* Image
*     包含10个不同焦点位置的图像。
*
* 输出:
*
* Depth
*     每一个像素对应的最佳聚焦深度。
*
* Confidence
*     深度估计的可信度。
*
* 'highpass'
*     使用高通滤波/高频信息评价图像清晰度。
*
* 'next_maximum'
*     使用相邻最大值附近的信息确定最佳焦点位置。
*
*
* ------------------------------------------------------------
*
* 数学上可以理解为:
*
* 对于一个像素位置 (x,y),存在10个不同Z位置:
*
*       Z1 Z2 Z3 ... Z10
*
* 对应10张图像:
*
*       I1 I2 I3 ... I10
*
* 对每张图像计算Focus Measure:
*
*       F1 F2 F3 ... F10
*
* 然后寻找:
*
*       Z* = argmax F(x,y,Z)
*
* 也就是说:
*
*       哪一个Z位置最清晰?
*
* 就认为:
*
*       这个像素的物体表面位于这个Z位置。
*
* ------------------------------------------------------------
*
* 例如某个像素:
*
* Z       Focus
* ----------------
* 1       12
* 2       20
* 3       35
* 4       52   ← 最大
* 5       41
* 6       25
*
* 那么:
*
*       Depth(x,y) ≈ Z4
*
* ------------------------------------------------------------

depth_from_focus (Image, Depth, Confidence, \
                  'highpass', 'next_maximum')


* ============================================================
* 对Depth进行局部平均
* ============================================================

* ------------------------------------------------------------
* mean_image
*
* 对 Depth 图像进行平均滤波。
*
* 11 × 11
*
* 表示使用11×11的邻域进行均值计算。
*
* 对于每个像素:
*
*       DepthHighConf(x,y)
*
* 大约等于周围11×11像素深度的平均值。
*
*
* 为什么要这样做?
*
* Depth From Focus 得到的深度图可能存在:
*
*   1. 噪声
*   2. 局部错误峰值
*   3. 单个像素的深度跳变
*   4. Focus Measure不稳定
*
* 均值滤波可以让深度图更加平滑。
*
* 注意:
*
* 这里实际上没有使用 Confidence 去筛选,
* 变量名 DepthHighConf 容易让人误以为已经进行了
* confidence filtering。
*
* 它实际上只是:
*
*       Depth
*          ↓
*      11×11 Mean
*          ↓
*      DepthHighConf
*
* ------------------------------------------------------------

mean_image (Depth, DepthHighConf, 11, 11)


* ============================================================
* 将深度图归一化到最大显示范围
* ============================================================

* ------------------------------------------------------------
* scale_image_max
*
* 将图像的灰度范围缩放,使最大值映射到255。
*
* 主要目的是方便后续:
*
*       threshold
*
* 使用固定灰度范围进行分割。
*
*
* 假设Depth原始范围:
*
*       20 ~ 80
*
* 经过scale_image_max以后可能变成:
*
*       63 ~ 255
*
* 这里需要特别注意:
*
* Depth 原始值的物理意义不能简单认为就是
* 0~255灰度值。
*
* 经过 scale_image_max 后:
*
*       ImageScaleMax
*
* 是一个用于后续图像处理/阈值分割的
* 归一化深度图。
*
* ------------------------------------------------------------

scale_image_max (DepthHighConf, ImageScaleMax)


* ============================================================
* 再次进行大尺度平均
* ============================================================

* ------------------------------------------------------------
* 51×51均值滤波。
*
* 前面:
*
*       11×11
*
* 主要用于降低局部噪声。
*
* 这里:
*
*       51×51
*
* 是更加大范围的平滑。
*
* 目的主要是:
*
*   1. 去除局部深度变化
*   2. 得到比较平滑的深度区域
*   3. 方便后续按照深度范围进行区域分割
*
* 代价是:
*
*       空间分辨率下降
*
* 小尺寸的深度结构可能被抹掉。
*
* ------------------------------------------------------------

mean_image (ImageScaleMax, DepthMean, 51, 51)


* ============================================================
* 根据深度范围提取一个区域
* ============================================================

* ------------------------------------------------------------
* threshold
*
* 将 DepthMean 中灰度值:
*
*       158 <= Gray <= 255
*
* 的像素提取出来。
*
* 输出:
*
*       Resistor
*
* 这里可以理解为:
*
*       Resistor = 深度较高区域
*
* 因为之前已经通过 scale_image_max
* 将深度图进行了归一化。
*
* ------------------------------------------------------------

threshold (DepthMean, Resistor, 158, 255)


* ------------------------------------------------------------
* connection
*
* 将二值区域 Resistor 中相互连接的像素
* 分成不同的连通区域。
*
* 例如:
*
*       ███       ████
*       ███       ████
*
* 会被分成:
*
*       Region1
*       Region2
*
* ConnectedRes 保存所有连通区域。
* ------------------------------------------------------------

connection (Resistor, ConnectedRes)


* ------------------------------------------------------------
* select_shape_std
*
* 从所有连通区域中选择面积最大的区域。
*
* 'max_area'
*
* 表示按照面积选择最大区域。
*
* 这里的:
*
*       FinalRes
*
* 就是最终认为属于 Resistor 的最大区域。
*
* ------------------------------------------------------------

select_shape_std (ConnectedRes, FinalRes, 'max_area', 0)


* ============================================================
* 提取另外一个深度范围
* ============================================================

* ------------------------------------------------------------
* 提取:
*
*       125 <= Gray <= 158
*
* 的区域。
*
* 也就是说:
*
* 这里选择的是另外一个深度范围。
*
* 可以理解为:
*
*       Resistor
*       ↓
*       深度较高区域
*
*       Solder
*       ↓
*       中等深度区域
*
* 具体哪个区域对应哪个物理结构,
* 要根据这个示例中的实际物体来确定。
* ------------------------------------------------------------

threshold (DepthMean, Solder, 125, 158)


* ------------------------------------------------------------
* 将Solder二值区域进行连通域分割。
* ------------------------------------------------------------

connection (Solder, ConnectedSol)


* ------------------------------------------------------------
* 从所有Solder连通区域中选择面积最大的区域。
*
* SelectedSol:
*
*       最大的Solder区域
* ------------------------------------------------------------

select_shape_std (ConnectedSol, SelectedSol, 'max_area', 0)


* ============================================================
* 填充区域内部孔洞
* ============================================================

* ------------------------------------------------------------
* fill_up
*
* 将 SelectedSol 区域内部的孔洞填充。
*
* 例如:
*
*       ███████
*       █  ○  █
*       █     █
*       ███████
*
* 填充之后:
*
*       ███████
*       ███████
*       ███████
*       ███████
*
* 输出:
*
*       FinalSol
*
* ------------------------------------------------------------

fill_up (SelectedSol, FinalSol)


* ============================================================
* 设置显示参数
* ============================================================

* 设置区域边界线宽度为3
dev_set_line_width (3)


* ------------------------------------------------------------
* 设置绘制方式:
*
* 'margin'
*
* 表示只显示区域的边界,而不是填充整个区域。
* ------------------------------------------------------------

dev_set_draw ('margin')


* ============================================================
* 显示原始多焦点图像
* ============================================================

dev_display (Image)


* 设置后续显示区域的颜色为红色
dev_set_color ('red')


* ------------------------------------------------------------
* 显示FinalSol
*
* FinalSol:
*
*   经过:
*
*       Depth
*        ↓
*       平滑
*        ↓
*       深度阈值
*        ↓
*       连通域
*        ↓
*       最大区域
*        ↓
*       填洞
*
*   得到的最终Solder区域。
* ------------------------------------------------------------

dev_display (FinalSol)


* 设置后续显示区域颜色为蓝色
dev_set_color ('blue')


* ------------------------------------------------------------
* 显示FinalRes
*
* FinalRes:
*
*   经过:
*
*       Depth
*        ↓
*       平滑
*        ↓
*       深度阈值 158~255
*        ↓
*       连通域
*        ↓
*       最大面积区域
*
*   得到的最终Resistor区域。
* ------------------------------------------------------------

dev_display (FinalRes)
把这段程序真正理解成一张"算法流程图"
R 复制代码
10张不同焦平面图像
        │
        │
        ▼
┌───────────────────┐
│ depth_from_focus  │
│                   │
│ Focus Measure     │
│      ↓            │
│ 找最佳焦点        │
└─────────┬─────────┘
          │
          ├──────────────► Confidence
          │
          ▼
        Depth
          │
          ▼
    11×11 Mean
          │
          ▼
    scale_image_max
          │
          ▼
    51×51 Mean
          │
          ▼
      DepthMean
          │
          ├──────────────┐
          │              │
          ▼              ▼
      158~255         125~158
          │              │
          ▼              ▼
     Resistor          Solder
          │              │
          ▼              ▼
      连通域           连通域
          │              │
          ▼              ▼
      最大区域         最大区域
          │              │
          │              ▼
          │           fill_up
          │              │
          └──────┬───────┘
                 ▼
             最终显示
       蓝色 = Resistor
       红色 = Solder

举例说明

假设 Z 扫描: 0, 10, 20, 30, 40 μm

某一个像素附近得到:

Z Focus Measure
0 12
10 28
20 50
30 35
40 16

那么 argmax F =20

所以初始估计 z*=20μm

假设在 10,20,30μm 三点进行抛物线拟合:

相关推荐
疯狂打码的少年1 小时前
【数据结构】八大排序算法对比总结(时间/空间/稳定性)
数据结构·笔记·算法
树獭哥1 小时前
量化金融入门:从数据规则到随机游走与凯利公式
人工智能·算法·金融·量化金融
薛定e的猫咪2 小时前
(arXiv 2026)GLiBRL :可学习基函数的深度贝叶斯元强化学习 ----待补充
人工智能·深度学习·学习·算法·机器学习
hetao17338372 小时前
2026-08-21~23 hetao1733837 的刷题记录
c++·算法
CQU_JIAKE3 小时前
8.23【A】
算法
sprlightning3 小时前
LHDC V5 算法深度解析
算法·fft·lhdcv5·imdct·mdct·rice·fac
csdn_aspnet4 小时前
C# 第k个最小元素(K’th Smallest Element)
算法·c#·排序算法
BioRunYiXue4 小时前
RACE技术全攻略:从全长克隆到靶基因验证
java·javascript·网络·人工智能·科技·算法·eclipse