def centroid(psf_img, sub_bg=False, camera_offset=0.0, camera_gain=1.0): """ Localize with center-of-mass. args ---- psf_img : 2D ndarray sub_bg : bool, subtract BG before taking centroid camera_offset : float, offset of camera camera_gain : float, gain of camera returns ------- dict : { 'y0': y centroid in pixels, 'x0': x centroid in pixels 'I': integrated intensity above background; 'amp': the intensity of the brightest pixel; 'bg': the estimated background level; 'snr': the estimated signal-to-noise ratio based on the amplitude } """ # Subtract BG and divide out gain psf_img = utils.rescale_img(psf_img, camera_offset=camera_offset, camera_gain=camera_gain) # Estimate background bg = utils.ring_mean(psf_img) # Subtract background psf_sub_bg = utils.set_neg_to_zero(psf_img - bg) # Integrate intensity above background I = psf_sub_bg.sum() # Take the brightest pixel above background as # the amplitude amp = utils.amp_from_I(I, sigma=1.0) # Find spot centers if sub_bg: y0, x0 = ndi.center_of_mass(psf_sub_bg) else: y0, x0 = ndi.center_of_mass(psf_img) # Estimate signal-to-noise snr = utils.estimate_snr(psf_img, amp) return dict((('bg', bg), ('I', I), ('amp', amp), ('y0', y0), ('x0', x0), ('snr', snr)))
def radial_symmetry(psf_img, sigma=1.0, camera_offset=0.0, camera_gain=1.0): """ Find spot centers by radial symmetry, and infer the intensity of the spots using a Gaussian model. args ---- psf_img : 2D ndarray sigma : float, Gaussian sigma camera_offset : float, offset of camera camera_gain : float, gain of camera returns ------- dict: { 'y0': y center, 'x0': x center, 'I': estimated integrated intensity of Gaussian, 'amp': estimated Gaussian peak amplitude; 'bg': estimated background level; 'snr': estimated signal to noise ratio } """ # Subtract BG and divide out gain psf_img = utils.rescale_img(psf_img, camera_offset=camera_offset, camera_gain=camera_gain) # Estimate spot centers y0, x0 = utils.rs(psf_img) # Estimate background level bg = utils.ring_mean(psf_img) # Estimate the intensity of the Gaussian I = utils.estimate_intensity(psf_img, y0, x0, bg, sigma=sigma) # Estimate peak amplitude of Gaussian amp = utils.amp_from_I(I, sigma=sigma) # Estimate SNR snr = utils.estimate_snr(psf_img, amp) # Return dict with values as output return dict((('y0', y0), ('x0', x0), ('bg', bg), ('I', I), ('amp', amp), ('snr', snr)))
def ls_log_gaussian(psf_img, sigma=1.0, camera_bg=0.0, camera_gain=1.0): """ Find the least-squares estimate for the parameters of a pointwise 2D Gaussian PSF model, assuming noise is log-normally distributed. While this noise model is fairly unrealistic, it admits a linear LS estimator for the parabolic log intensities of a pointwise Gaussian. As a result, it is very fast. However, it has the issue that it is biased- the log normality assumption tends to make it biased toward the center of the fitting window. Use at your own risk. args ---- psf_img : 2D ndarray sigma : float, the width of the Gaussian model returns ------- dict : { 'y0': estimated y center, 'x0': estimated x center, 'I': estimated PSF intensity, 'bg': estimated background intensity, 'amp': estimated peak PSF amplitude, 'snr': estimated SNR } """ # Subtract BG and divide out gain psf_img = utils.rescale_img(psf_img, camera_offset=camera_offset, camera_gain=camera_gain) # Common factors V = sigma**2 V2 = V * 2 # Estimate background by taking the mean # of the outer ring of pixels bg = utils.ring_mean(psf_img) # Subtract background psf_img_sub = utils.set_neg_to_zero(psf_img - bg) # Avoid taking log of zero pixels nonzero = psf_img_sub > 0.0 # Pixel indices Y, X = np.indices(psf_img.shape) Y = Y[nonzero] X = X[nonzero] # Log PSF above background log_psf = np.log(psf_img_sub[nonzero]) # Response vector in LS problem R = log_psf + np.log(V2 * np.pi) + (Y**2 + X**2) / V2 # Independent matrix in LS problem M = np.asarray([np.ones(nonzero.sum()), V * Y, V * X]).T # Compute the LS parameter estimate ls_pars = utils.pinv(M) @ R # Use the LS parameters to the find the # spot center y0 = ls_pars[1] x0 = ls_pars[2] I = np.exp(ls_pars[0] + (y0**2 + x0**2) / V2) # Estimate peak Gaussian amplitude amp = utils.amp_from_I(I, sigma=sigma) # Estimate SNR snr = utils.estimate_snr(psf_img, amp) return dict((('y0', y0), ('x0', x0), ('I', I), ('bg', bg), ('amp', amp), ('snr', snr)))
def ls_point_gaussian(psf_img, sigma=1.0, max_iter=20, camera_gain=1.0, camera_offset=0.0, damp=0.3, convergence_crit=1.0e-5, divergence_crit=0.5, debug=False): """ Find the least-squares estimate for the parameters of a pointwise-evaluated 2D Gaussian PSF model, given a sample PSF image. This is equivalent to the maximum likelihood estimate for this model in the presence of Gaussian noise. While the pointwise Gaussian is less accurate than the integrated Gaussian model, it is somewhat faster to evaluate. args ---- psf_img : 2D ndarray sigma : float, width of 2D Gaussian max_iter : int camera_gain : float camera_offset : float damp : float, damping factor for parameter updates convergence_crit : float, the criterion for fit convergence (y0, x0) divergence_crit : float, the criterion for fit divergence (y0, x0) debug : bool, show intermediate steps and plot PSF model at each iteration returns ------- dict : { 'y0': estimated y center, 'x0': estimated x center, 'I': estimated PSF intensity, 'bg': estimated background intensity, 'amp': estimated peak PSF amplitude, 'snr': estimated SNR } """ # Subtract BG and divide out gain psf_img = utils.rescale_img(psf_img, camera_offset=camera_offset, camera_gain=camera_gain) # Make initial parameter guesses with the # radial symmetry method guess = radial_symmetry(psf_img, sigma=sigma) # Current parameter estimate pars = np.array([guess['y0'], guess['x0'], guess['I'], guess['bg']]) # Update to the current parameters, used to # call convergence update = np.ones(4, dtype='float64') iter_idx = 0 while iter_idx < max_iter: # If converging, terminate if (np.abs(update[:2]) < convergence_crit).all(): break # Calculate PSF and Jacobian under the present # model model, J = utils.J_point_gaussian(psf_img, pars, sigma=sigma) # Calculate the residuals r = (model - psf_img).ravel() # Get the new update vector update = np.linalg.inv(J.T @ J) @ (J.T @ r) # Apply the new update vector if debug: print(pars) utils.wireframe_overlay(psf_img, model) pars = pars - damp * update iter_idx += 1 # If not converged, fall back to initial guess (radial # symmetry method) if (update[:2] >= divergence_crit).any(): pars = np.array([guess['y0'], guess['x0'], guess['I'], guess['bg']]) converged = False else: converged = True # Check whether the fit makes sense - is # within window, doesn't have crazy intensity # values, etc. if not utils.fit_is_sane(pars, psf_img.shape[0]): pars = np.array([guess['y0'], guess['x0'], guess['I'], guess['bg']]) converged = False # Estimate peak amplitude of Gaussian amp = utils.amp_from_I(pars[2], sigma=sigma) # Estimate SNR snr = utils.estimate_snr(psf_img, amp) if debug: print(pars) utils.wireframe_overlay(psf_img, model) return dict((('y0', pars[0]), ('x0', pars[1]), \ ('I', pars[2]), ('bg', pars[3]), ('amp', amp), ('snr', snr), ('converged', converged)))
def mle_poisson(psf_img, sigma=1.0, max_iter=20, damp=0.3, camera_offset=0.0, camera_gain=1.0, convergence_crit=3.0e-5, ridge=1.0e-4, debug=False, divergence_crit=1.0): """ Estimate the maximum likelihood model parameters for an integrated 2D Gaussian PSF under a Poisson noise model, using a Levenberg-Marquardt procedure. Due to the nature of Poisson noise, it is recommended to measure the camera gain and offset to convert from grayvalues to photons. args ---- psf_img : 2D ndarray sigma : float max_iter : int damp : float camera_offset, camera_bg : floats convergence_crit : float ridge : float, initial regularization term debug : bool, show each stage of fitting returns ------- dict : { 'y0': y center, 'x0': x center, 'I': intensity, 'bg': background intensity, 'amp': peak amplitude, 'snr': estimate signal to noise ratio, 'y0_err': error in y0, 'x0_err': error in x0, 'I_err': error in I, 'bg_err': error in bg, 'converged': bool, whether the iteration converged } """ # Subtract BG and divide out gain psf_img = utils.rescale_img(psf_img, camera_offset=camera_offset, camera_gain=camera_gain) # Make initial guess using radial symmetry guess = radial_symmetry(psf_img, sigma=sigma) # Current parameter estimate pars = np.array([guess['y0'], guess['x0'], guess['I'], guess['bg']]) # Update to the parameter estimate, used # to call convergence / divergence update = np.ones(4, dtype='float64') # Hessian of log-likelihood function H = np.zeros((4, 4), dtype='float64') # Continue iterating until max_iter reached # or convergence reached iter_idx = 0 while iter_idx < max_iter: # Check for convergence if any(np.abs(update[:2]) < convergence_crit): break # Calculate PSF model, Jacobian, and # Hessian under Poisson noise model model, J, H = utils.L_poisson(psf_img, pars, sigma=sigma) # Calculate gradient of log-likelihood # with respect to each model parameter grad = J.sum(axis=0) # Invert the Hessian H_inv = utils.invert_hessian(H, ridge=ridge) # Determine the update vector, the change # in parameters update = -damp * (H_inv @ grad) # Apply the update if debug: print(pars) utils.wireframe_overlay(psf_img, model) pars += update iter_idx += 1 # If estimate is diverging, fall back to original # guess converged = (np.abs(update[:2]) < divergence_crit).all() if not converged: pars = [guess['y0'], guess['x0'], \ guess['I'], guess['bg']] # Check whether the fit makes sense - is # within window, doesn't have crazy intensity # values, etc. if not utils.fit_is_sane(pars, psf_img.shape[0]): pars = np.array([guess['y0'], guess['x0'], guess['I'], guess['bg']]) converged = False # Estimate peak amplitude of Gaussian amp = utils.amp_from_I(pars[2], sigma=sigma) # Estimate SNR snr = utils.estimate_snr(psf_img, amp) if debug: print(pars) utils.wireframe_overlay(psf_img, model) return dict((('y0', pars[0]), ('x0', pars[1]), \ ('I', pars[2]), ('bg', pars[3]), ('amp', amp), ('snr', snr), ('converged', converged)))