You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

二维高斯拟合不收敛排查:密集区域光斑强度拟合求助

问题分析与修正方案

我瞅了你的代码和问题,发现几个关键的坑导致拟合失败,咱们一个个捋:


1. 局部区域拟合时的坐标系统错误

这是最致命的问题!你传入fit_gaussian_spots的x_guess和y_guess是原图的全局坐标,但你拟合的是裁剪后的局部子图。比如你从原图中裁剪了pre_y - local_area到pre_y + local_area的区域,那子图里的x/y坐标范围是0到2*local_area,光斑在子图里的位置应该是相对于子图左上角的偏移,而不是原图的全局坐标。用全局坐标当初始猜测,相当于让算法在子图里找一个不存在的位置,肯定收敛不了!

修正方法:计算局部区域内的相对坐标:

# 在主循环调用拟合函数前,转换坐标
y_start = max(pre_y - local_area, 0)
x_start = max(pre_x - local_area, 0)
local_x = pre_x - x_start
local_y = pre_y - y_start

2. 初始猜测参数太粗糙,和真实值差距过大

你当前的初始猜测initial_guess里,amp=1、sigma_x=1、sigma_y=1,但模拟数据里amp是100,sigma是3/9,差距非常大。curve_fit的Levenberg-Marquardt算法对初始值很敏感,初始值离最优解太远的话,很容易陷入局部最小值或者直接不收敛。

修正方法:基于局部区域的统计值生成合理初始值:

local_max = np.max(array)
local_min = np.min(array)
initial_guess = Params(
    amp=local_max - local_min,  # 信号幅度=峰值-背景
    x=x_guess, 
    y=y_guess, 
    sigma_x=array.shape[1]/4,  # 基于局部宽度的初始sigma
    sigma_y=array.shape[0]/4,
    rotation=0, 
    offset=local_min  # 背景初始值设为局部最小值
)

3. 参数边界设置不合理

你当前的边界有几个明显问题:

  • x/y的边界用x_guess*0.5和x_guess*1.5,如果x_guess是全局坐标,完全超出局部区域范围;
  • rotation设为-inf到2π,旋转角是周期的,[-π, π]足够,没必要设无穷大;
  • offset上限设为全局最大值,背景应该低于局部区域的平均值,而非最大值。

修正后的边界:

min_bounds = Params(
    amp=1e-8, 
    x=0,  # x不能小于局部区域左边界
    y=0,  # y不能小于局部区域上边界
    sigma_x=1e-8, 
    sigma_y=1e-8, 
    rotation=-np.pi,
    offset=local_min * 0.5
)
max_bounds = Params(
    amp=(local_max - local_min)*1.2,
    x=array.shape[1]-1,  # x不能超过局部区域宽度-1
    y=array.shape[0]-1,  # y不能超过局部区域高度-1
    sigma_x=array.shape[1]/2,
    sigma_y=array.shape[0]/2,
    rotation=np.pi,
    offset=local_min + (local_max - local_min)*0.2
)

4. 拟合后直接round参数丢失精度

你在拟合后做了popt = Params(*np.round(popt)),这会把所有参数取整,比如sigma本来是3.2,直接变成3,导致后续评估的高斯和真实光斑偏差极大,看起来像拟合失败。必须去掉这个round操作,保留浮点精度。


修改后的完整代码

把所有修正整合后的代码如下:

import scipy.optimize as opt
import numpy as np
import matplotlib.pyplot as plt
import skimage.feature
from collections import namedtuple

def gaussian_2d(xy_array, amplitude, pos_x, pos_y, sigma_x, sigma_y, rotation, offset):
    """ Expression for a 2D gaussian function with variance in both x and y """
    x, y = xy_array
    cos_rot = np.cos(rotation)
    sin_rot = np.sin(rotation)
    a = (cos_rot ** 2) / (2 * sigma_x ** 2) + (sin_rot ** 2) / (2 * sigma_y ** 2)
    b = -(np.sin(2 * rotation)) / (4 * sigma_x ** 2) + (np.sin(2 * rotation)) / (4 * sigma_y ** 2)
    c = (sin_rot ** 2) / (2 * sigma_x ** 2) + (cos_rot ** 2) / (2 * sigma_y ** 2)
    g = amplitude * np.exp(-(a * ((x - pos_x) ** 2) + 2 * b * (x - pos_x) * (y - pos_y) + c * ((y - pos_y) ** 2)))
    g += offset
    return g.ravel()

def fit_gaussian_spots(x_guess, y_guess, array):
    Params = namedtuple("Parameters", "amp, x, y, sigma_x, sigma_y, rotation, offset")
    eps = 1e-8
    local_max = np.max(array)
    local_min = np.min(array)
    
    # 更合理的初始猜测
    initial_guess = Params(
        amp=local_max - local_min,
        x=x_guess,
        y=y_guess,
        sigma_x=array.shape[1]/4,
        sigma_y=array.shape[0]/4,
        rotation=0,
        offset=local_min
    )
    
    # 修正后的边界
    min_bounds = Params(
        amp=eps,
        x=0,
        y=0,
        sigma_x=eps,
        sigma_y=eps,
        rotation=-np.pi,
        offset=local_min * 0.5
    )
    max_bounds = Params(
        amp=(local_max - local_min) * 1.2,
        x=array.shape[1]-1,
        y=array.shape[0]-1,
        sigma_x=array.shape[1]/2,
        sigma_y=array.shape[0]/2,
        rotation=np.pi,
        offset=local_min + (local_max - local_min)*0.2
    )
    
    try:
        X, Y = create_grid(*array.shape)
        popt, pcov = opt.curve_fit(
            f=gaussian_2d,
            xdata=(X, Y),
            ydata=array.ravel(),
            p0=initial_guess,
            bounds=(min_bounds, max_bounds),
            maxfev=10000  # 增加最大迭代次数
        )
        popt = Params(*popt)
    except (ValueError, RuntimeError) as e:
        print(f"Fit failed for spot at ({x_guess}, {y_guess}): {str(e)}")
        popt, pcov = None, None
    return popt, pcov

def create_grid(h, w):
    """ Creates a grid of x and y points to fit and evaluate over """
    x = np.arange(0, w, 1)
    y = np.arange(0, h, 1)
    x, y = np.meshgrid(x, y)
    return x, y

def evaluate_gaussian(x, y, popt, local_x_start=0, local_y_start=0):
    """ 把局部坐标还原为全局坐标后评估高斯 """
    global_popt = (popt.amp, popt.x + local_x_start, popt.y + local_y_start,
                   popt.sigma_x, popt.sigma_y, popt.rotation, popt.offset)
    z = gaussian_2d((x, y), *global_popt)
    return z

if __name__ == "__main__":
    np.random.seed(4)
    h, w = 200, 200
    x, y = create_grid(h=h, w=w)
    # 创建模拟数据
    img = []
    for _ in range(10):
        randx = np.random.randint(10, w - 10)
        randy = np.random.randint(10, h - 10)
        amp = 100
        d = gaussian_2d(
            xy_array=(x, y),
            amplitude=amp,
            pos_x=randx,
            pos_y=randy,
            sigma_x=9,
            sigma_y=3,
            rotation=3,
            offset=0
        )
        img.append(d)
    img = np.sum(img, axis=0).reshape(h, w)
    print("max intensity: {:.2f}".format(img.max()))
    
    # 检测光斑
    spots = skimage.feature.peak_local_max(img, num_peaks=20, min_distance=10)
    fig, ax = plt.subplots(ncols=2, figsize=(12,6))
    local_area = 20
    fit = []
    
    for idx, (pre_y, pre_x) in enumerate(spots):
        # 计算局部区域边界
        y_start = max(pre_y - local_area, 0)
        y_end = pre_y + local_area
        x_start = max(pre_x - local_area, 0)
        x_end = pre_x + local_area
        local_img = img[y_start:y_end, x_start:x_end]
        
        # 转换为局部坐标
        local_x = pre_x - x_start
        local_y = pre_y - y_start
        
        # 拟合高斯
        popt, pcov = fit_gaussian_spots(x_guess=local_x, y_guess=local_y, array=local_img)
        if popt is None:
            continue
        
        # 打印全局坐标参数
        global_popt = (popt.amp, popt.x + x_start, popt.y + y_start,
                       popt.sigma_x, popt.sigma_y, popt.rotation, popt.offset)
        print(f"Spot {idx}: amp={global_popt[0]:.2f}, x={global_popt[1]:.2f}, y={global_popt[2]:.2f}, sigma_x={global_popt[3]:.2f}, sigma_y={global_popt[4]:.2f}")
        
        # 绘图
        ax[0].add_patch(plt.Circle((pre_x, pre_y), 5, linewidth=0.5, fill=False, color="red"))
        ax[1].add_patch(plt.Rectangle((x_start, y_start), width=x_end-x_start, height=y_end-y_start, fill=False, color="yellow"))
        
        # 评估全局高斯并保存
        fit.append(evaluate_gaussian(x, y, popt, local_x_start=x_start, local_y_start=y_start))
    
    fit = np.sum(fit, axis=0)
    ax[0].set_title("Original Image")
    ax[0].imshow(img, origin="bottom", extent=(x.min(), x.max(), y.min(), y.max()))
    ax[1].set_title("Fitted Spots")
    ax[1].imshow(fit.reshape(img.shape), origin="bottom", extent=(x.min(), x.max(), y.min(), y.max()))
    plt.tight_layout()
    plt.show()

额外建议

  • 如果光斑重叠严重,单光斑拟合效果有限,可以考虑多高斯联合拟合(同时拟合多个光斑参数),但复杂度会高很多;
  • 先对图像做简单背景扣除(比如局部均值拟合),减少offset参数的拟合压力;
  • 保留maxfev参数,给算法足够的迭代次数收敛。

内容的提问来源于stack exchange,提问作者komodovaran_

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:36:05