def test_otf2psf(self): """ Test otf2psf() by verifying that the ideal circular aperture otf obtained with circ_aperture_otf() produces the correct psf, obtained by airy_fn() :return: """ na = 1.3 wavelength = 0.465 dx = 0.061 nx = 101 dy = dx ny = nx fxs = tools.get_fft_frqs(nx, dx) fys = tools.get_fft_frqs(ny, dy) dfx = fxs[1] - fxs[0] dfy = fys[1] - fys[0] otf = fit_psf.circ_aperture_otf(fxs[None, :], fys[:, None], na, wavelength) psf, (ys, xs) = fit_psf.otf2psf(otf, (dfy, dfx)) psf = psf / psf.max() psf_true = fit_psf.airy_fn(xs[None, :], ys[:, None], [1, 0, 0, na, 0], wavelength) self.assertAlmostEqual(np.max(np.abs(psf - psf_true)), 0, 4)
na = 1.3 pixel_size = 0.065 emission_wavelengths = [0.519, 0.580] excitation_wavelengths = [0.465, 0.532] # ############################################ # load OTF data # ############################################ otf_data_path = "data/2020_05_19_otf_fit_blue.pkl" with open(otf_data_path, 'rb') as f: otf_data = pickle.load(f) otf_p = otf_data['fit_params'] otf_fn = lambda f, fmax: 1 / (1 + (f / fmax * otf_p[ 0])**2) * psf.circ_aperture_otf(f, 0, na, 2 * na / fmax) # ############################################ # load affine transformations from DMD to camera # ############################################ affine_fnames = [ "data/2021-02-03_09;43;06_affine_xform_blue_z=0.pkl", "data/2021-02-03_09;43;06_affine_xform_green_z=0.pkl" ] affine_xforms = [] for p in affine_fnames: with open(p, 'rb') as f: affine_xforms.append(pickle.load(f)['affine_xform']) # ############################################
def simulated_img(ground_truth, max_photons, cam_gains, cam_offsets, cam_readout_noise_sds, pix_size=None, otf=None, na=1.3, wavelength=0.5, photon_shot_noise=True, bin_size=1, use_otf=False): """ Convert ground truth image (with values between 0-1) to simulated camera image, including the effects of photon shot noise and camera readout noise. :param use_otf: :param ground_truth: Relative intensity values of image :param max_photons: Mean photons emitted by ber of photons will be different than expected. Furthermore, due to the "blurring" of the point spread function and possible binning of the image, no point in the image may realize "max_photons" :param cam_gains: gains at each camera pixel :param cam_offsets: offsets of each camera pixel :param cam_readout_noise_sds: standard deviation characterizing readout noise at each camera pixel :param pix_size: pixel size of ground truth image in ums. Note that the pixel size of the output image will be pix_size * bin_size :param otf: optical transfer function. If None, use na and wavelength to set values :param na: numerical aperture. Only used if otf=None :param wavelength: wavelength in microns. Only used if otf=None :param photon_shot_noise: turn on/off photon shot-noise :param bin_size: bin pixels before applying Poisson/camera noise. This is to allow defining a pattern on a finer pixel grid. :return img: :return snr: :return max_photons_real: """ if np.any(ground_truth > 1) or np.any(ground_truth < 0): warnings.warn( 'ground_truth image values should be in the range [0, 1] for max_photons to be correct' ) img_size = ground_truth.shape if use_otf: # get OTF if otf is None: fx = tools.get_fft_frqs(img_size[1], pix_size) fy = tools.get_fft_frqs(img_size[0], pix_size) otf = psf.circ_aperture_otf(fx[None, :], fy[:, None], na, wavelength) # blur image with otf/psf # todo: maybe should add an "imaging forward model" function to fit_psf.py and call it here. gt_ft = fft.fftshift(fft.fft2(fft.ifftshift(ground_truth))) img_blurred = max_photons * fft.fftshift( fft.ifft2(fft.ifftshift(gt_ft * otf))).real img_blurred[img_blurred < 0] = 0 else: img_blurred = max_photons * ground_truth # resample image by binning img_blurred = tools.bin(img_blurred, (bin_size, bin_size), mode='sum') max_photons_real = img_blurred.max() # add shot noise if photon_shot_noise: img_shot_noise = np.random.poisson(img_blurred) else: img_shot_noise = img_blurred # add camera noise and convert from photons to ADU readout_noise = np.random.standard_normal( img_shot_noise.shape) * cam_readout_noise_sds img = cam_gains * img_shot_noise + readout_noise + cam_offsets # signal to noise ratio sig = cam_gains * img_blurred # assuming photon number large enough ~gaussian noise = np.sqrt(cam_readout_noise_sds**2 + cam_gains**2 * img_blurred) snr = sig / noise return img, snr, max_photons_real
def plot_otf(frq_vects, fmax_img, otf, otf_unc=None, to_use=None, wf_corrected=None, figsize=(20, 10)): """ Plot complete OTF :param frq_vects: :param fmax_img: :param otf: :param figsize: :return: """ if otf_unc is None: otf_unc = np.zeros(otf.shape) if to_use is None: to_use = np.ones(otf.shape, dtype=np.int) nmax1 = int(np.round(0.5 * (otf.shape[1] - 1))) nmax2 = int(np.round(0.5 * (otf.shape[2] - 1))) fmag = np.linalg.norm(frq_vects, axis=-1) fmag_interp = np.linspace(0, fmax_img, 1000) # only care about fmax value, so create na/wavelength that give us this na = 1 wavelength = 2 * na / fmax_img otf_ideal = fit_psf.circ_aperture_otf(fmag_interp, 0, na, wavelength) figh = plt.figure(figsize=figsize) grid = plt.GridSpec(2, 6) # 1D otf ax = plt.subplot(grid[0, :2]) ylim = [-0.05, 1.2] plt.title("otf mag") plt.xlabel("Frequency (1/um)") plt.ylabel("otf") ph_ideal, = plt.plot(fmag_interp, otf_ideal, 'k') plt.errorbar(fmag[to_use], np.abs(otf[to_use]), yerr=otf_unc[to_use], color="b", fmt='.') colors = ["g", "m", "r", "y", "c"] phs = [ph_ideal] labels = ["OTF ideal, fmax=%0.2f (1/um)" % fmax_img] + list(range(1, 6)) # plot main series peaks for jj in range(1, 6): ph = plt.errorbar(fmag[:, nmax1, nmax2 + jj][to_use[:, nmax1, nmax2 + jj]], np.abs(otf[:, nmax1, nmax2 + jj][to_use[:, nmax1, nmax2 + jj]]), yerr=otf_unc[:, nmax1, nmax2 + jj][to_use[:, nmax1, nmax2 + jj]], color=colors[jj - 1], fmt=".") phs.append(ph) plt.errorbar(fmag[:, nmax1, nmax2 - jj][to_use[:, nmax1, nmax2 - jj]], np.abs(otf[:, nmax1, nmax2 - jj][to_use[:, nmax1, nmax2 - jj]]), yerr=otf_unc[:, nmax1, nmax2 - jj][to_use[:, nmax1, nmax2 - jj]], color=colors[jj - 1], fmt=".") plt.legend(phs, labels) xlim = ax.get_xlim() plt.plot(xlim, [0, 0], 'k') plt.plot([fmax_img, fmax_img], ylim, 'k') ax.set_xlim(xlim) ax.set_ylim(ylim) # 1D log scale ax = plt.subplot(grid[1, :2]) plt.title("otf mag (log scale)") plt.xlabel("Frequency (1/um)") plt.ylabel("otf") plt.plot(fmag_interp, otf_ideal, 'k') plt.errorbar(fmag[to_use], np.abs(otf[to_use]), yerr=otf_unc[to_use], fmt='.') # plot main series peaks for jj in range(1, 6): plt.errorbar(fmag[:, nmax1, nmax2 + jj][to_use[:, nmax1, nmax2 + jj]], np.abs(otf[:, nmax1, nmax2 + jj][to_use[:, nmax1, nmax2 + jj]]), yerr=otf_unc[:, nmax1, nmax2 + jj][to_use[:, nmax1, nmax2 + jj]], color=colors[jj - 1], fmt=".") plt.errorbar(fmag[:, nmax1, nmax2 - jj][to_use[:, nmax1, nmax2 - jj]], np.abs(otf[:, nmax1, nmax2 - jj][to_use[:, nmax1, nmax2 - jj]]), yerr=otf_unc[:, nmax1, nmax2 - jj][to_use[:, nmax1, nmax2 - jj]], color=colors[jj - 1], fmt=".") xlim = ax.get_xlim() ax.plot([fmax_img, fmax_img], ylim, 'k') ax.set_xlim(xlim) ax.set_ylim([1e-4, 1.2]) ax.set_yscale('log') # show widefield corrected/not peaks ax = plt.subplot(grid[1, 4:]) ylim = [-0.05, 1.2] plt.title("otf mag, widefield corrected/not") plt.xlabel("Frequency (1/um)") plt.ylabel("otf") plt.plot(fmag_interp, otf_ideal, 'k') phu = plt.errorbar(fmag[to_use], np.abs(otf[to_use]), yerr=otf_unc[to_use], color="b", fmt='.') corrected = np.logical_and(wf_corrected, to_use) phc = plt.errorbar(fmag[corrected], np.abs(otf[corrected]), yerr=otf_unc[corrected], color="r", fmt=".") xlim = ax.get_xlim() plt.plot(xlim, [0, 0], 'k') plt.plot([fmax_img, fmax_img], ylim, 'k') ax.set_xlim(xlim) ax.set_ylim(ylim) plt.legend([phu, phc], ["uncorrected peaks", "corrected"]) # 2D otf ax = plt.subplot(grid[0, 2:4]) plt.title("2D otf (log scale)") plt.xlabel("fx (1/um)") plt.ylabel("fy (1/um)") clims = [1e-3, 1] frqs_pos = np.array(frq_vects, copy=True) y_is_neg = frq_vects[..., 1] < 0 frqs_pos[y_is_neg] = -frqs_pos[y_is_neg] plt.plot([-fmax_img, fmax_img], [0, 0], 'k') plt.scatter(frqs_pos[to_use, 0].ravel(), frqs_pos[to_use, 1].ravel(), c=np.log10(np.abs(otf[to_use]).ravel()), norm=matplotlib.colors.Normalize(vmin=np.log10(clims[0]), vmax=np.log10(clims[1]))) cb = plt.colorbar() plt.clim(np.log10(clims)) circ = matplotlib.patches.Circle((0, 0), radius=fmax_img, color='k', fill=0, ls='-') ax.add_artist(circ) ax.set_xlim([-fmax_img, fmax_img]) ax.set_ylim([-0.05 * fmax_img, fmax_img]) # plot phase ax = plt.subplot(grid[1, 2:4]) plt.title("phase") plt.xlabel("Frequency (1/um)") plt.ylabel("phase") ax.plot(fmag[to_use], np.angle(otf[to_use]).ravel(), '.') ylims = [-np.pi - 0.1, np.pi + 0.1] ax.set_ylim(ylims) # plot 2D phase ax = plt.subplot(grid[0, 4:]) plt.title("2D otf phase") plt.xlabel("fx (1/um)") plt.ylabel("fy (1/um)") clims_phase = [-np.pi - 0.1, np.pi + 0.1] plt.plot([-fmax_img, fmax_img], [0, 0], 'k') plt.scatter(frqs_pos[to_use, 0].ravel(), frqs_pos[to_use, 1].ravel(), c=np.angle(otf[to_use]), norm=matplotlib.colors.Normalize(vmin=clims_phase[0], vmax=clims_phase[1])) cb = plt.colorbar() plt.clim(clims_phase) circ = matplotlib.patches.Circle((0, 0), radius=fmax_img, color='k', fill=0, ls='-') ax.add_artist(circ) ax.set_xlim([-fmax_img, fmax_img]) ax.set_ylim([-0.05 * fmax_img, fmax_img]) return figh