def guide_star_seeing(subframe): # subframe = subframe - np.median(subframe) subframe = subframe - np.percentile(subframe,5) sub_frame_l = int(np.shape(subframe)[0]) y, x = np.mgrid[:sub_frame_l, :sub_frame_l] # Fit with constant, bounds, tied x and y sigmas and outlier rejection: gaussian_init = models.Const2D(0.0) + models.Gaussian2D(subframe[int(sub_frame_l/2),int(sub_frame_l/2)],int(sub_frame_l/2),int(sub_frame_l/2),8/2.355,8/2.355,0) gaussian_init.x_stddev_1.min = 1.0/2.355 gaussian_init.x_stddev_1.max = 20.0/2.355 gaussian_init.y_stddev_1.min = 1.0/2.355 gaussian_init.y_stddev_1.max = 20.0/2.355 gaussian_init.y_stddev_1.tied = tie_sigma gaussian_init.theta_1.fixed = True fit_gauss = fitting.FittingWithOutlierRemoval(fitting.LevMarLSQFitter(),sigma_clip,niter=3,sigma=3.0) # gaussian, mask = fit_gauss(gaussian_init, x, y, subframe) gain = 8.21 #e per ADU read_noise = 2.43 #ADU weights = gain / np.sqrt(np.absolute(subframe)*gain + (read_noise*gain)**2) #1/sigma for each pixel gaussian, mask = fit_gauss(gaussian_init, x, y, subframe, weights) fwhm_x = 2.355*gaussian.x_stddev_1.value fwhm_y = 2.355*gaussian.y_stddev_1.value x_seeing = fwhm_x * 0.579 y_seeing = fwhm_y * 0.579 return(x_seeing,y_seeing)
def create_test_image(gap=3, N=25): ''' Create one random stitched camera, with a Gaussian-ish background to it. ''' # generate a random model p_generate = (models.Const2D(amplitude=10**np.random.uniform(0, 1)) + models.Gaussian2D(amplitude=10**np.random.uniform(.5, 2), x_mean=np.random.uniform(-N, N), y_mean=np.random.uniform(-N, N), x_stddev=np.random.uniform(2, N), y_stddev=np.random.uniform(2, N), theta=np.random.uniform(0, 2*np.pi))) # create fake x and y arrays x, y = np.meshgrid(np.arange(-N-gap, N+gap+1), np.arange(-N-gap, N+gap+1)) # create the model perfect = p_generate(x, y) z = np.random.normal(perfect, 1) # find the gaps, replace them with zeros, and mark them as bad bad = (np.abs(x) < gap) | (np.abs(y) < gap) z[bad] = 0 ok = bad == False return x, y, z, ok, p_generate
def test_get_bounding_box(): model = models.Const2D(2) # No with_bbox assert model.get_bounding_box(False) is None # No bounding_box with pytest.raises(NotImplementedError): model.bounding_box assert model.get_bounding_box(True) is None # Normal bounding_box model.bounding_box = ((0, 1), (0, 1)) assert not isinstance(model.bounding_box, CompoundBoundingBox) assert model.get_bounding_box(True) == ((0, 1), (0, 1)) # CompoundBoundingBox with no removal bbox = CompoundBoundingBox.validate(model, {(1,): ((-1, 0), (-1, 0)), (2,): ((0, 1), (0, 1))}, selector_args=[('y', False)]) model.bounding_box = bbox assert isinstance(model.bounding_box, CompoundBoundingBox) # Get using argument not with_bbox assert model.get_bounding_box(True) == bbox # Get using with_bbox not argument assert model.get_bounding_box((1,)) == ((-1, 0), (-1, 0)) assert model.get_bounding_box((2,)) == ((0, 1), (0, 1))
def test_legacy_const(tmpdir): model = astropy_models.Const1D(amplitude=5.) assert_model_roundtrip(model, tmpdir, version="1.3.0") model = astropy_models.Const2D(amplitude=5.) with pytest.raises(TypeError, match="does not support models with > 1 dimension"): assert_model_roundtrip(model, tmpdir, version="1.3.0")
def from_tree_transform(self, node): if self.version < AsdfVersion('1.4.0'): # The 'dimensions' property was added in 1.4.0, # previously all values were 1D. return models.Const1D(node['value']) elif node['dimensions'] == 1: return models.Const1D(node['value']) elif node['dimensions'] == 2: return models.Const2D(node['value'])
def FitGaussian2D(B, reconstruct_model=False): ''' Fits a 2D gaussian model to a vignette using Levenberg-Marquardt algorithm and least squares statistic. input: B: Vignette with the object reconstruct_model: Boolean flag. Indicates if the reconstructed model and its residuals should be computed output: params: Dictionary with the 6 fitted parameters. Keys = ['amp', 'ab', 'ellip', 'theta', 'x0', 'y0'] *rec_model: Vignette with the fitted model. Only if reconstruct_model=True *rec_resi: Vignette with the residuals, ie: rec_resi=B-rec_model. Only if reconstruct_model=True Notes: Given the polar nature of the ellipticity definition, the fit can be confusing. For ex., given a gaussian with ellip = 0.1 and theta = 0 (where theta is measured from the y axis), it is posible that the fitter returns ellip = -0.1 and theta = pi/2. This time the minus sign in ellip indicates that it is meassuring it on the oposite axis, thus theta is being measured from the x axis. We fix the position x0,y0 of the model at the center of the stamp. There is no need to fit them Notes 2: Add the posibility of fitting a sum of gaussians instead of just one. ''' p = model_Gaussian2D(amp=B.max()-B.min(), ab=5, e1=0., e2=0.2,x0=B.shape[0]/2,y0=B.shape[1]/2) + \ models.Const2D(amplitude=B.min()) # Constraints on some parameters p.x0_0.fixed = True p.y0_0.fixed = True x, y = np.meshgrid(range(B.shape[0]), range(B.shape[1]), indexing='ij') out_gauss, out_const = fitter(p, x, y, B, maxiter=10000, acc=1e-16) # fitter() is defined globally # retrieves the 6 gaussian parameters params = { 'amp': out_gauss.amp.value, 'ab': out_gauss.ab.value, 'x0': out_gauss.x0.value, 'y0': out_gauss.y0.value, 'e1': out_gauss.e1.value, 'e2': out_gauss.e2.value } # generates a vignette with the model and computes residuals if reconstruct_model: rec_model = Gauss2D_stamp(amp=params['amp'], ab=params['ab'], e1=params['e1'], e2=params['e2'], x0=params['x0'], y0=params['y0'], noise=0, zerolevel=out_const.amplitude, shape=B.shape) resi = B - rec_model return params, rec_model, resi else: return params
def models_with_input_eq(): # 1D model m1 = astmodels.Shift(1*u.kg) m1.input_units_equivalencies = {'x': u.mass_energy()} # 2D model m2 = astmodels.Const2D(10*u.Hz) m2.input_units_equivalencies = {'x': u.dimensionless_angles(), 'y': u.dimensionless_angles()} # 2D model with only one input equivalencies m3 = astmodels.Const2D(10*u.Hz) m3.input_units_equivalencies = {'x': u.dimensionless_angles()} # model using equivalency that has args using units m4 = astmodels.PowerLaw1D(amplitude=1*u.m, x_0=10*u.pix, alpha=7) m4.input_units_equivalencies = {'x': u.equivalencies.pixel_scale(0.5*u.arcsec/u.pix)} return[m1, m2, m3, m4]
def FitSersic2D(B, reconstruct_model=False): ''' Fits a 2D sersic model to a vignette using Levenberg-Marquardt algorithm and least squares statistic. input: B: Vignette with the object reconstruct_model: Boolean flag. Indicates if the reconstructed model and its residuals should be computed output: params: Dictionary with the 7 fitted parameters. Keys = ['amp', 'radius', 'sersic_n', 'e1', 'e2', 'x0', 'y0'] *rec_model: Vignette with the fitted model. Only if reconstruct_model=True *rec_resi: Vignette with the residuals, ie: rec_resi=B-rec_model. Only if reconstruct_model=True Notes: We fix the position x0,y0 of the model at the center of the stamp. There is no need to fit them ''' p = model_Sersic2D(amp=B.max()-B.min(), radius=10, sersic_n=4, x0=B.shape[0]/2,y0=B.shape[1]/2,e1=0.1, e2=0.1) + \ models.Const2D(amplitude=B.min()) # Constraints on some parameters p.x0_0.fixed = True p.y0_0.fixed = True x, y = np.meshgrid(range(B.shape[0]), range(B.shape[1]), indexing='ij') out_sersic, out_const = fitter(p, x, y, B, maxiter=10000, acc=1e-16) # fitter() is defined globally # retrieves sersic parameters params = { 'amp': out_sersic.amp.value, 'sersic_n': out_sersic.sersic_n.value, 'radius': out_sersic.radius.value, 'x0': out_sersic.x0.value, 'y0': out_sersic.y0.value, 'e1': out_sersic.e1.value, 'e2': out_sersic.e2.value } # generates a vignette with the model and computes residuals if reconstruct_model: rec_model = Sersic2D_stamp(amp=out_sersic.amp, radius=out_sersic.radius, sersic_n=out_sersic.sersic_n, x0=out_sersic.x0, y0=out_sersic.y0, e1=out_sersic.e1, e2=out_sersic.e2, noise=0, zerolevel=out_const.amplitude, shape=B.shape) resi = B - rec_model return params, rec_model, resi else: return params
def FitMoffat2D(B, reconstruct_model=False): ''' Fits a 2D moffat model to a vignette using Levenberg-Marquardt algorithm and least squares statistic. input: B: Vignette with the object reconstruct_model: Boolean flag. Indicates if the reconstructed model and its residuals should be computed output: params: Dictionary with the 6 fitted parameters. Keys = ['beta', 'fwhm', 'e1', 'e2', 'x0', 'y0'] *rec_model: Vignette with the fitted model. Only if reconstruct_model=True *rec_resi: Vignette with the residuals, ie: rec_resi=B-rec_model. Only if reconstruct_model=True Notes: We fix the position x0,y0 of the model at the center of the stamp. There is no need to fit them ''' p = model_Moffat2D(fwhm=5, beta=1, x0=B.shape[0]/2,y0=B.shape[1]/2,e1=0.1, e2=0.1) + \ models.Const2D(amplitude=B.min()) # Constraints on some parameters p.x0_0.fixed = True p.y0_0.fixed = True x, y = np.meshgrid(range(B.shape[0]), range(B.shape[1]), indexing='ij') out_moffat, out_const = fitter(p, x, y, B, maxiter=10000, acc=1e-16) # fitter() is defined globally # retrieves the 6 moffat parameters params = { 'beta': out_moffat.beta.value, 'fwhm': out_moffat.fwhm.value, 'x0': out_moffat.x0.value, 'y0': out_moffat.y0.value, 'e1': out_moffat.e1.value, 'e2': out_moffat.e2.value } # generates a vignette with the model and computes residuals if reconstruct_model: rec_model = Moffat2D_stamp(beta=out_moffat.beta, fwhm=out_moffat.fwhm, x0=out_moffat.x0, y0=out_moffat.y0, e1=out_moffat.e1, e2=out_moffat.e2, noise=0, zerolevel=out_const.amplitude, shape=B.shape) resi = B - rec_model return params, rec_model, resi else: return params
def fit_gaussian2d(b, fitter=None): if fitter is None: fitter = fitting.LevMarLSQFitter() y2, x2 = np.mgrid[:b.shape[0], :b.shape[1]] ampl = b.max() - b.min() p = models.Gaussian2D( x_mean=b.shape[1] / 2.0, y_mean=b.shape[0] / 2.0, x_stddev=1.0, y_stddev=1.0, theta=np.pi / 4.0, amplitude=ampl, ) p += models.Const2D(amplitude=b.min()) out = fitter(p, x2, y2, b, maxiter=1000) return out
def fitpsf(psf, *center): fixed = {} bounds = {} if not center: amplitude = psf.max() y_center, x_center = numpy.unravel_index(psf.argmax(), psf.shape) fixed['x_mean'] = False fixed['y_mean'] = False bounds['x_stddev'] = [0., psf.shape[1]] bounds['y_stddev'] = [0., psf.shape[0]] else: amplitude = psf[center[1], center[0]] x_center = center[0] y_center = center[1] fixed['x_mean'] = True fixed['y_mean'] = True bounds['x_stddev'] = [0., psf.shape[1]] bounds['y_stddev'] = [0., psf.shape[0]] x_stddev = 3. / 2.35 y_stddev = 3. / 2.35 theta = 0 p_init = models.Gaussian2D(amplitude = amplitude, x_mean = x_center, \ y_mean = y_center, x_stddev = x_stddev, y_stddev = y_stddev, \ theta = theta, fixed = fixed, bounds = bounds) p_const_init = models.Const2D(amplitude=0.) fit_p = fitting.LevMarLSQFitter() gridy, gridx = numpy.mgrid[0:psf.shape[0]:1, 0:psf.shape[1]:1] p = fit_p(p_init + p_const_init, gridx, gridy, psf, maxiter=200) if not numpy.any(fit_p.fit_info['param_cov']): p = None return p
def fit_plane(image, maxiter=5000, epsilon=1e-10): model = models.Planar2D(slope_x=0., slope_y=0, intercept=0) + models.Const2D(0) # Make x and y grid to fit to y_arange, x_arange = np.where(~(np.isnan(image))) z = image[(y_arange, x_arange)] # Fit model to grid fit = fitting.LevMarLSQFitter() # fitting.LinearLSQFitter() fitted_line = fit(model, x_arange, y_arange, z, maxiter=5000, epsilon=1e-10) return fitted_line, fit
def guess_2d_gaussian(x, y, z, ok=None): ''' Make a guess to initialize the baseline + 2D Gaussian model, based on medians and weighted moments of the image. ''' if ok == None: ok = np.ones_like(z).astype(np.bool) # estimate a baseline flux, from the median of the good pixels baseline_guess = np.median(z[ok]) # do a *veyr* coarse subtraction, to focus on just the blob crudelysubtracted = np.maximum((z-baseline_guess), 0) amplitude_guess = np.sum(crudelysubtracted) def moment(q): ''' Take the weighted moment of a quantity. ''' return np.sum(q[ok]*crudelysubtracted[ok])/np.sum(crudelysubtracted[ok]) # calculate flux-weighted centroids of the blob x_guess = moment(x) y_guess = moment(y) # calculate flux-weighted widths of the blob x_width_guess = np.sqrt(moment((x - x_guess)**2)) y_width_guess = np.sqrt(moment((y - y_guess)**2)) # create an initial model, with the initial guesses initial = ( models.Const2D(amplitude=baseline_guess) + models.Gaussian2D(amplitude=amplitude_guess, x_mean=x_guess, y_mean=y_guess, x_stddev=x_width_guess, y_stddev=y_width_guess, theta=0.0)) return initial
def fit_2D_gaussian(xmat, ymat, z): ''' Return fitted model parameters ''' g = astropy_models.Gaussian2D(x_mean=[0], y_mean=[0], x_stddev=[1], y_stddev=[1], amplitude=z.max(), theta=[0], fixed={'amplitude': True, 'x_mean': True, 'y_mean': True}) + \ astropy_models.Const2D(amplitude=[np.percentile(z, 10)]) fit_g = fitting.LevMarLSQFitter() output = fit_g(g, xmat, ymat, z) cov = fit_g.fit_info['param_cov'] if cov is None: warn("Fitting failed.") cov = np.zeros((4, 4)) * np.NaN return output, cov
def _initialize_model(self): """Initialize a model with first guesses for the parameters. The user can select between several astropy models, e.g., 'Gaussian2D', 'Moffat2D'. We will use the data to get the first estimates of the parameters of each model. Finally, a Constant2D model is added to account for the background or sky level around the star. """ max_value = self.data.max() if self.model_type == self._GAUSSIAN2D: model = models.Gaussian2D(x_mean=self.x, y_mean=self.y, x_stddev=1, y_stddev=1) model.amplitude = max_value # Establish reasonable bounds for the fitted parameters model.x_stddev.bounds = (0, self._box / 4) model.y_stddev.bounds = (0, self._box / 4) model.x_mean.bounds = (self.x - 5, self.x + 5) model.y_mean.bounds = (self.y - 5, self.y + 5) elif self.model_type == self._MOFFAT2D: model = models.Moffat2D() model.x_0 = self.x model.y_0 = self.y model.gamma = 2 model.alpha = 2 model.amplitude = max_value # Establish reasonable bounds for the fitted parameters model.alpha.bounds = (1, 6) model.gamma.bounds = (0, self._box / 4) model.x_0.bounds = (self.x - 5, self.x + 5) model.y_0.bounds = (self.y - 5, self.y + 5) model += models.Const2D(self.fit_sky()) model.amplitude_1.fixed = True return model
def gfit(data, x0, y0, size=5, fwhm=3, sub=True, plot=None, fig=1, scale=1, pafixed=False): """ Does gaussian fit to input data given initial xcen,ycen """ fit = fitting.LevMarLSQFitter() #fit=fitting.SLSQPLSQFitter() z = data[int(y0) - size:int(y0) + size, int(x0) - size:int(x0) + size] xcen, ycen = np.unravel_index(np.argmax(z), z.shape) xcen += (int(x0) - size) ycen += (int(y0) - size) y, x = np.mgrid[ycen - size:ycen + size, xcen - size:xcen + size] z = data[ycen - size:ycen + size, xcen - size:xcen + size] g_init = models.Gaussian2D(x_mean=xcen, y_mean=ycen, x_stddev=fwhm / 2.354, y_stddev=fwhm / 2.354, amplitude=data[ycen, xcen], theta=0., fixed={'theta': pafixed}) + models.Const2D(0.) g = fit(g_init, x, y, z) xfwhm = g[0].x_stddev * 2.354 * scale yfwhm = g[0].y_stddev * 2.354 * scale fwhm = np.sqrt(xfwhm * yfwhm) xcen = g[0].x_mean.value ycen = g[0].y_mean.value theta = (g[0].theta.value % (2 * np.pi)) * 180. / np.pi print( 'xFWHM:{:8.2f} yFWHM:{:8.2f} FWHM:{:8.2f} SCALE:{:8.2f} PA:{:8.2f}' .format(xfwhm, yfwhm, fwhm, scale, theta)) if plot is not None: r = np.sqrt((y - ycen)**2 + (x - xcen)**2) plots.plotp(plot, r, z, xt='R(pixels)', yt='Intensity') r = np.arange(0., 5 * fwhm / 2.354 / scale, 0.1) peak = g[0].amplitude plot.plot( r, peak * np.exp(-np.power(r, 2.) / (2 * np.power(g[0].x_stddev, 2.))) + g[1].amplitude) plot.plot( r, peak * np.exp(-np.power(r, 2.) / (2 * np.power(g[0].y_stddev, 2.))) + g[1].amplitude) plot.text(0.9, 0.9, 'x: {:7.1f} y: {:7.1f} fw: {:8.2f}'.format(xcen, ycen, fwhm), transform=plot.transAxes, ha='right') plt.draw() if sub: out = data out[ycen - size:ycen + size, xcen - size:xcen + size] -= g[0](x, y) return out return g
def model_plot(model, image, amp, x_wid=5, y_wid=5, angle=0, fignumber=1, title=None): cutout_size = np.shape(image)[0] # Open Fit y_o, x_o = np.mgrid[:cutout_size, :cutout_size] z_o = image if model == "Gaussian": g2d_model = models.Gaussian2D(np.amax(z_o), cutout_size / 2, cutout_size / 2, x_stddev=x_wid, y_stddev=y_wid, theta=angle) c2d_model = models.Const2D(0.0) p_init_o = g2d_model + c2d_model elif model == "Moffat": g2d_model = Elliptical_Moffat2D(amplitude=np.amax(z_o), x_0=cutout_size / 2, y_0=cutout_size / 2, width_x=x_wid, width_y=y_wid) c2d_model = models.Const2D(0.0) p_init_o = g2d_model + c2d_model fit_p_o = fitting.LevMarLSQFitter() # weights = 1.0 / z_o**0.5 # weights = z_o**0.5 # weights = np.ones(z_o.shape, dtype=float) p_o = fit_p_o(p_init_o, x_o, y_o, z_o, epsilon=1e-12, acc=1e-12, maxiter=300, weights=None) # FWHM values? if model == "Gaussian": x_fwhm = p_o.x_stddev_0.value / gaussian_fwhm_to_sigma y_fwhm = p_o.y_stddev_0.value / gaussian_fwhm_to_sigma elif model == "Moffat": # BUG this isn't quite right for a moffat fit... x_fwhm = p_o.width_x_0.value y_fwhm = p_o.width_y_0.value #Images model_img = p_o(x_o, y_o) diff_img = z_o - model_img #Images cut: half_size = image.shape[0] / 2 x_lo = int(round(half_size - x_fwhm)) x_hi = x_lo + int(round(x_fwhm * 2)) y_lo = int(round(half_size - y_fwhm)) y_hi = y_lo + int(round(y_fwhm * 2)) #cut size for metrics z_o_cut = z_o[y_lo:y_hi, x_lo:x_hi].astype(float) diff_img_cut = diff_img[y_lo:y_hi, x_lo:x_hi].astype(float) # Fit metrics residual_o = np.sum(diff_img_cut**2) PSF_mean = np.mean(z_o_cut) fresid = np.median(np.abs(diff_img_cut / z_o_cut)) # mean fractional residual FVU = np.sum(diff_img**2) / np.sum( (z_o_cut - PSF_mean)**2) # fraction of variance unexplained # Plotting plt.figure(fignumber, figsize=(12, 3)) plt.suptitle(title) vmin = z_o.min() vmax = z_o.max() # left panel plt.subplot(1, 4, 1) plt.imshow(z_o, origin='lower', vmin=vmin, vmax=vmax) plt.gca().add_patch( Rectangle((x_lo, y_lo), x_hi - x_lo, y_hi - y_lo, edgecolor='red', facecolor='none', lw=2)) plt.colorbar(fraction=0.046, pad=0.05) plt.title("Data") # left middle panel plt.subplot(1, 4, 2) plt.imshow(model_img, origin='lower', vmin=vmin, vmax=vmax) plt.gca().add_patch( Rectangle((x_lo, y_lo), x_hi - x_lo, y_hi - y_lo, edgecolor='red', facecolor='none', lw=2)) plt.colorbar(fraction=0.046, pad=0.04) plt.title("Model") # right middle panel plt.subplot(1, 4, 3) plt.imshow(z_o - model_img, origin='lower', vmin=-vmax / 6, vmax=vmax / 6) plt.gca().add_patch( Rectangle((x_lo, y_lo), x_hi - x_lo, y_hi - y_lo, edgecolor='red', facecolor='none', lw=1)) plt.colorbar(fraction=0.046, pad=0.04) plt.title("Data-Model") # right panel plt.subplot(1, 4, 4) plt.imshow((z_o - model_img) / z_o, origin='lower', vmax=1, vmin=-1) plt.gca().add_patch( Rectangle((x_lo, y_lo), x_hi - x_lo, y_hi - y_lo, edgecolor='red', facecolor='none', lw=1)) plt.colorbar(fraction=0.046, pad=0.04) plt.title("Fract. Resid.") plt.tight_layout() print("Residuals Squared Summed - ", title, ": ", '{:.2e}'.format(residual_o)) print("Fraction of Variance Unexplained (FVU) - ", title, ": ", '{:.2e}'.format(FVU)) print("Median Fractional Residual Per Pix - ", title, ": ", '{:.2e}'.format(fresid)) if model == "Moffat": print("beta: ", p_o.power_0.value) print('\nFit Info:') print('nfev = ', fit_p_o.fit_info['nfev']) print('ierr = ', fit_p_o.fit_info['ierr']) print('mesg = ', fit_p_o.fit_info['message']) print('\nParams:') for k, v in p_o._parameters_.items(): val = v.value unit = 'pix' if 'phi' in k: val = val % 360.0 unit = 'deg' if 'theta' in k: val = np.rad2deg(val) % 360.0 unit = 'deg' if 'amp' in k: unit = 'flux' print(f'{k:15s} = {val:10.6f} {unit:5s}') print('\n') return p_o
def find_source(im, guesspos=None, searchbox=None, fitbox=None, guessmeth='max', smooth=0, searchsmooth=3, guessFWHM=None, guessamp=None, guessbg=None, method='fast', sign=1, verbose=False, fixFWHM=False, fixpos=False, minamp=0.01, maxamp=None, plot=False, maxFWHM=None, minFWHM=None, posradius=None, silent=False): """ find the point source in an image and provide its best fit parameters from a Gaussian fit Parameters: - im: image to be searched - guesspos: 2D array with the (y, x) guessed position for the point source position. If None, use the middle of the whole image - searchbox: int or 2D array with (y, x) size, size of box to be searched centered on the guesspos. If None, search the whole image - fitbox: int or 2D array with (y, x) size, size of box used for the fit. If None, use searchbox - guessmeth: method for guesstimating the point source position (currently only 'max' and other). For 'max' use the maximum brightness pixel in the searchbox. Other: use centre of the searchbox - smooth: optional smoothing with a Gaussian. The value specifies the width of the Gaussian used for the smoothing - searchsmooth: optional smoothing only for guesstimating source position - guessFWHM: guess for the FWHM of the source. If None, use middle between minFWHM and maxFWHM or 5 if the former are not provided - guessamp: guess for the amplitude of the source. If None, use the pixel brightness at the guess position - guessbg: guess for the background level in the image. If None, use the median of the image. - method: method used for the fitting: 'fast': use LevMarLSQFitter from astropy, 'mpfit': use the MPFIT package - sign: sign of the point source to be searched - fixFWHM: fix the FWHM of the source to the guess value - fixpos: fix the position of the source to the guess value - minamp, maxamp: minimum and maximum allowed values for the amplitude of the source - minFWHM, maxFWHM: minimum and maximum allowed values for the FWHM of the source - posradius: maximum allowed radius in pix around the guess position for the source location. If None, the whole searchbox is allowed """ funname = "FIND_SOURCE" s2f = (2.0 * np.sqrt(2.0*np.log(2.0))) f2s = 1.0/s2f s = np.shape(im) if sign < 0: im = -im if smooth > 0: im = gaussian_filter(im, sigma=smooth) if guesspos is None : guesspos = 0.5*np.array(s) if verbose: print(funname + ": method: ", method) print(funname + ": s: ", s) print(funname + ": sign: ", sign) print(funname + ": searchsmooth: ", searchsmooth) print(funname + ": guessmeth: ", guessmeth) print(funname + ": initial guessamp: ", guessamp) print(funname + ": initial guessbg: ", guessbg) print(funname + ": intial guesspos: ", guesspos) print(funname + ": intial searchbox: ", searchbox) # --- define the search box if searchbox is not None : # test if the box provided is an integer, in which case blow up to array if not hasattr(searchbox, "__len__"): searchbox = np.array([searchbox, searchbox]) searchbox = np.array(searchbox, dtype=int) sx0 = np.max([0, int(np.round(guesspos[1] - 0.5 * searchbox[1]))]) sx1 = np.min([s[1], int(np.round(guesspos[1] + 0.5 * searchbox[1]))]) sy0 = np.max([0, int(np.round(guesspos[0] - 0.5 * searchbox[0]))]) sy1 = np.min([s[0], int(np.round(guesspos[0] + 0.5 * searchbox[0]))]) searchim = im[sy0:sy1, sx0:sx1] if verbose: print(funname + ": sy0, sy1, sx0, sx1 ", sy0, sy1, sx0, sx1) searchbox = np.array(np.shape(searchim)) # print(funname + ": ss: ", np.shape(searchim)) else: searchbox = np.array(s, dtype=int) sx0 = 0 sy0 = 0 searchim = im if verbose: print(funname + ": final searchbox: ", searchbox) # --- should the first guess be based on the max or on the position? if guessmeth == 'max': if searchsmooth > 0: # smoothing is quick and thus on by default ssim = gaussian_filter(searchim, sigma=searchsmooth, mode='nearest') guesspos = np.array(np.unravel_index(np.nanargmax(ssim), searchbox)) if plot is True: plt.figure(1, figsize=(3,3)) plt.imshow(ssim, origin='bottom', interpolation='nearest') plt.title('Smoothed Search image') plt.show() # print(funname + ": guesspos: ", guesspos) else: guesspos = np.array(np.unravel_index(np.nanargmax(searchim), searchbox)) else: guesspos = 0.5*searchbox if verbose: print(funname + ": guesspos in searchbox: ", guesspos) print(funname + ": guesspos in total image: ", guesspos[0] + sy0, guesspos[1] + sx0) # print(funname + ": guesspos: ", guesspos) guesspos = np.array([guesspos[0] + sy0, guesspos[1] + sx0]) if plot is True: plt.clf() plt.close(1) plt.figure(1, figsize=(3,3)) plt.imshow(searchim, origin='bottom', interpolation='nearest') plt.title('Search image') plt.show() if verbose: print(funname + ": intial fitbox: ", fitbox) # --- define the fit box if fitbox is not None: # test if the box provided is an integer, in which case blow up to array if not hasattr(fitbox, "__len__"): fitbox = np.array([fitbox, fitbox]) fitbox = np.array(fitbox, dtype=int) fx0 = int(np.round(guesspos[1] - 0.5 * fitbox[1])) fx1 = int(np.round(guesspos[1] + 0.5 * fitbox[1])) fy0 = int(np.round(guesspos[0] - 0.5 * fitbox[0])) fy1 = int(np.round(guesspos[0] + 0.5 * fitbox[0])) guesspos = 0.5*fitbox # print('guesspos, fx0, fx1, fy0, fy1 ', guesspos, fx0, fx1, fy0, fy1) # for the new guess position, we have to take into account if the # fitbox is smaller than expected because being close to the edge if fx0 < 0: guesspos[1] = guesspos[1] + fx0 fx0 = 0 # if fx1 > s[1]: # guesspos[1] = guesspos[1] - (fx1 - s[1]) # fx1 = s[1] if fy0 < 0: guesspos[0] = guesspos[0] + fy0 fy0 = 0 # if fy1 > s[0]: # guesspos[0] = guesspos[0] - (fy1 - s[0]) # fy1 = s[0] fitim = im[fy0:fy1, fx0:fx1] fs = np.array(np.shape(fitim)) else: fitim = im fx0 = 0 fy0 = 0 fs = np.shape(fitim) fitbox = fs if verbose: print(funname + ": final fitbox: ", fitbox) print(funname + ": final guesspos in fitbox: ", guesspos) if plot is True: plt.figure(1, figsize=(3,3)) plt.imshow(fitim, origin='bottom', interpolation='nearest') plt.title('(Sub)image to be fitted') plt.show() if guessFWHM is None: if maxFWHM is not None and minFWHM is not None: guessFWHM = 0.5 * (maxFWHM + minFWHM) elif maxFWHM is not None: guessFWHM = 0.5 * maxFWHM elif minFWHM is not None: guessFWHM = 2 * minFWHM else: guessFWHM = 5 # --- estimate the BG with ignoring central source (use either 3*FWHM or # 80% of image whatever is smaller). First generate a background image # of sufficient size bgbox = int(np.round(6*guessFWHM)) if verbose: print('bgbox:', bgbox) print("bgcenpos: ", [fy0 + 0.5*fitbox[0], fx0 + 0.5*fitbox[1]]) bgim = _crop_image(im, box=bgbox, cenpos=[fy0 + 0.5*fitbox[0], fx0 + 0.5*fitbox[1]], exact=False) ignore_aper = np.min([3*guessFWHM, 0.8*np.max(s)]) bgval, bgstd = _measure_bkg(bgim, ignore_aper=ignore_aper) if guessbg is None: guessbg = bgval if guessamp is None: guessamp = fitim[int(guesspos[0]), int(guesspos[1])] - guessbg if maxFWHM is None: maxFWHM = np.max(s) if minFWHM is None: minFWHM = 1 maxsigma = maxFWHM * f2s minsigma = minFWHM * f2s if posradius is not None: minx = guesspos[1] - posradius maxx = guesspos[1] + posradius miny = guesspos[0] - posradius maxy = guesspos[0] + posradius else: minx = 0 maxx = fs[1] miny = 0 maxy = fs[0] sigma = guessFWHM * f2s guess = [guessbg, guessamp , guesspos[1], guesspos[0], sigma, sigma, 0] if verbose: print(' - Guess: ', guess) print(' - minFWHM: ', minFWHM) print(' - maxFWHM: ', maxFWHM) print(' - minsigma: ', minsigma) print(' - maxsigma: ', maxsigma) print(' - minamp: ', minamp) print(' - maxamp: ', maxamp) print(' - minx,maxx, miny,maxy: ', minx, maxx, miny, maxy) y, x = np.mgrid[:fs[0], :fs[1]] g_init = models.Gaussian2D(amplitude=guessamp, x_mean=guesspos[1], y_mean=guesspos[0], x_stddev=sigma, y_stddev=sigma) c_init = models.Const2D(amplitude=guessbg) init = g_init + c_init gim = init(x, y) if plot is True: plt.figure(1, figsize=(3,3)) plt.imshow(gim, origin='bottom', interpolation='nearest') plt.title('Guess') plt.show() if np.isnan(fitim).any(): if not silent: print(funname + ": WARNING: image to be cropped contains NaNs!") fitim[np.isnan(fitim)] = guessbg # set any NaNs to 0 for crop to work if ('mpfit' in method) : # params=[] - initial input parameters for Gaussian function. # (height, amplitude, x, y, width_x, width_y, rota) # parameter limits minpars = [0, minamp, minx, miny, minsigma, minsigma, 0] maxpars = [0, maxamp, maxx, maxy, maxsigma, maxsigma, 0] limitedmin = [False, True, True, True, True, True, False] limitedmax = [False, False, True, True, True, True, False] # ensure that the fit is positive if the sign is 1 # (or negative if the sign is -1) if minamp is None: limitedmin[1] = False if maxamp: limitedmax[1] = True if fixFWHM: limitedmin[4] = True limitedmax[4] = True minpars[4] = sigma-0.001 maxpars[4] = sigma+0.001 limitedmin[5] = True limitedmax[5] = True minpars[5] = sigma-0.001 maxpars[5] = sigma+0.001 if fixpos: limitedmin[2] = True limitedmax[2] = True minpars[2] = guesspos[1]-0.001 maxpars[2] = guesspos[1]+0.001 limitedmin[3] = True limitedmax[3] = True minpars[3] = guesspos[0]-0.001 maxpars[3] = guesspos[0]+0.001 res = _gaussfit(fitim, err=None, params=guess, returnfitimage=True, return_all=1, minpars=minpars, maxpars=maxpars, limitedmin=limitedmin, limitedmax=limitedmax) params = res[0][0] perrs = res[0][1] if perrs is None: perrs = np.full(6,-1, dtype=float) fit = res[1] elif 'fast' in method: # ensure that the fit is positive init.amplitude_0.bounds = (minamp, maxamp) init.x_mean_0.bounds = (minx, maxx) init.y_mean_0.bounds = (miny, maxy) init.x_stddev_0.bounds = (minsigma, maxsigma) # --- ensure that angle stays in useful pounds #init.theta_0.bounds = (-2*np.pi, 2*np.pi) # somehow fixing the angle does not work if fixFWHM: init.x_stddev_0.fixed = True init.y_stddev_0.fixed = True if fixpos: init.x_mean_0.fixed = True init.y_mean_0.fixed = True fit_meth = fitting.LevMarLSQFitter() # fit_meth = fitting.SimplexLSQFitter() # very slow # fit_meth = fitting.SLSQPLSQFitter() # not faster than LevMar g_fit = fit_meth(init, x, y, fitim, acc=1e-8) fit = g_fit(x, y) params = np.array([g_fit.amplitude_1.value, g_fit.amplitude_0.value, g_fit.x_mean_0.value, g_fit.y_mean_0.value, g_fit.x_stddev_0.value, g_fit.y_stddev_0.value, g_fit.theta_0.value]) perrs = params * 0 # this method does not provide uncertainty estimates # --- convert theta to deg: params[6] = params[6]/np.pi*180.0 # --- theta measures the angle from the x-axis, so we need to add 90 params[6] = params[6] + 90.0 # print(init.x_stddev_0.fixed, init.x_stddev_0.bounds) # print(g_fit.x_stddev_0.fixed, g_fit.x_stddev_0.bounds) else: print(funname + ": ERROR: non-valid method requested: " + method +"\n returning None") return(None, None, None) # --- use the STD of the BG for the BG level uncertainty if larger than # error estimate if bgstd > perrs[0]: perrs[0] = bgstd if verbose: print(funname + ": uncorrected fit params: ", params) print(funname + ": uncorrected fit errs: ", perrs) # --- compute the position in the total image and switch x and y to agree # with the numpy convention temp = np.copy(params) params[2] = temp[3] + fy0 params[3] = temp[2] + fx0 temp = np.copy(perrs) perrs[2] = temp[3] perrs[3] = temp[2] # --- if the y FWHM is larger than the one in x direction, switch them so # that the first FWHM is the major axis one. if params[5] > params[4]: temp = params[4] params[4] = params[5] params[5] = temp temp = perrs[4] perrs[4] = perrs[5] perrs[5] = temp params[6] = params[6] + 90.0 # --- normalise the angle params[6] = params[6] % 180 if params[6] < 0: params[6] = params[6] + 180 # if sign < 0: params[0] = - params[0] params[1] = - params[1] fit = - fit fitim = - fitim if verbose: print(" - GET_POINTSOURCE: fitted params: ", params) # convert sigma to FWHM for the output: params[4:6] = params[4:6] * s2f if plot is True: plt.figure(1, figsize=(3,3)) plt.imshow(fit, origin='bottom', interpolation='nearest') plt.title('Fit with sign') plt.show() plt.close(1) plt.figure(1, figsize=(3,3)) plt.imshow(fitim-fit, origin='bottom', interpolation='nearest') plt.title('Residual') plt.show() plt.close(1) ims = [fitim, fit, fitim-fit] return(params, perrs, ims)
astmodels.Multiply(3), astmodels.Multiply(10 * u.m), astmodels.RotateCelestial2Native(5.63, -72.5, 180), astmodels.EulerAngleRotation(23, 14, 2.3, axes_order='xzx'), astmodels.Mapping((0, 1), n_inputs=3), astmodels.Shift(2. * u.deg), astmodels.Scale(3.4 * u.deg), astmodels.RotateNative2Celestial(5.63 * u.deg, -72.5 * u.deg, 180 * u.deg), astmodels.RotateCelestial2Native(5.63 * u.deg, -72.5 * u.deg, 180 * u.deg), astmodels.RotationSequence3D([1.2, 2.3, 3.4, .3], 'xyzx'), astmodels.SphericalRotationSequence([1.2, 2.3, 3.4, .3], 'xyzy'), astmodels.AiryDisk2D(amplitude=10., x_0=0.5, y_0=1.5), astmodels.Box1D(amplitude=10., x_0=0.5, width=5.), astmodels.Box2D(amplitude=10., x_0=0.5, x_width=5., y_0=1.5, y_width=7.), astmodels.Const1D(amplitude=5.), astmodels.Const2D(amplitude=5.), astmodels.Disk2D(amplitude=10., x_0=0.5, y_0=1.5, R_0=5.), astmodels.Ellipse2D(amplitude=10., x_0=0.5, y_0=1.5, a=2., b=4., theta=0.1), astmodels.Exponential1D(amplitude=10., tau=3.5), astmodels.Gaussian1D(amplitude=10., mean=5., stddev=3.), astmodels.Gaussian2D(amplitude=10., x_mean=5., y_mean=5., x_stddev=3., y_stddev=3.), astmodels.KingProjectedAnalytic1D(amplitude=10., r_core=5., r_tide=2.), astmodels.Logarithmic1D(amplitude=10., tau=3.5), astmodels.Lorentz1D(amplitude=10., x_0=0.5, fwhm=2.5), astmodels.Moffat1D(amplitude=10., x_0=0.5, gamma=1.2, alpha=2.5), astmodels.Moffat2D(amplitude=10., x_0=0.5, y_0=1.5, gamma=1.2, alpha=2.5),
def star_centers_from_waffle_cube(cube, wave, instrument, waffle_orientation, high_pass=False, center_offset=(0, 0), smooth=0, coro=True, display=False, save_path=None): ''' Compute star center from waffle images Parameters ---------- cube : array_like Waffle IRDIS cube wave : array_like Wavelength values, in nanometers instrument : str Instrument, IFS or IRDIS waffle_orientation : str String giving the waffle orientation '+' or 'x' high_pass : bool Apply high-pass filter to the image before searching for the satelitte spots smooth : int Apply a gaussian smoothing to the images to reduce noise. The value is the sigma of the gaussian in pixel. Default is no smoothing center_offset : tuple Apply an (x,y) offset to the default center position. Default is no offset coro : bool Observation was performed with a coronagraph. Default is True display : bool Display the fit of the satelitte spots save_path : str Path where to save the fit images Returns ------- spot_center : array_like Centers of each individual spot in each frame of the cube spot_dist : array_like The 6 possible distances between the different spots img_center : array_like The star center in each frame of the cube ''' # instrument if instrument == 'IFS': pixel = 7.46 offset = 102 elif instrument == 'IRDIS': pixel = 12.25 offset = 0 else: raise ValueError('Unknown instrument {0}'.format(instrument)) # standard parameters dim = cube.shape[-1] nwave = wave.size loD = wave * 1e-6 / 8 * 180 / np.pi * 3600 * 1000 / pixel # waffle parameters freq = 10 * np.sqrt(2) * 0.97 box = 8 if waffle_orientation == '+': orient = offset * np.pi / 180 elif waffle_orientation == 'x': orient = offset * np.pi / 180 + np.pi / 4 # spot fitting xx, yy = np.meshgrid(np.arange(2 * box), np.arange(2 * box)) # multi-page PDF to save result if save_path is not None: pdf = PdfPages(save_path) # center guess if instrument == 'IFS': center_guess = np.full((nwave, 2), ((dim // 2) + 3, (dim // 2) - 1)) elif instrument == 'IRDIS': center_guess = np.array(((485, 520), (486, 508))) # loop over images spot_center = np.zeros((nwave, 4, 2)) spot_dist = np.zeros((nwave, 6)) img_center = np.zeros((nwave, 2)) for idx, (wave, img) in enumerate(zip(wave, cube)): print(' wave {0:2d}/{1:2d} ({2:.3f} micron)'.format( idx + 1, nwave, wave)) # remove any NaN img = np.nan_to_num(img) # center guess (+offset) cx_int = int(center_guess[idx, 0]) + center_offset[0] cy_int = int(center_guess[idx, 1]) + center_offset[1] # optional high-pass filter if high_pass: img = img - ndimage.median_filter(img, 15, mode='mirror') # optional smoothing if smooth > 0: img = ndimage.gaussian_filter(img, smooth) # mask for non-coronagraphic observations if not coro: mask = aperture.disc(cube[0].shape[-1], 5 * loD[idx], diameter=False, center=(cx_int, cy_int), invert=True) img *= mask # create plot if needed if save_path or display: fig = plt.figure(0, figsize=(8, 8)) plt.clf() col = ['red', 'blue', 'magenta', 'purple'] ax = fig.add_subplot(111) ax.imshow(img / img.max(), aspect='equal', vmin=1e-2, vmax=1, norm=colors.LogNorm(), interpolation='nearest') ax.set_title(r'Image #{0} - {1:.3f} $\mu$m'.format(idx + 1, wave)) # satelitte spots for s in range(4): cx = int(cx_int + freq * loD[idx] * np.cos(orient + np.pi / 2 * s)) cy = int(cy_int + freq * loD[idx] * np.sin(orient + np.pi / 2 * s)) sub = img[cy - box:cy + box, cx - box:cx + box] # fit: Gaussian + constant imax = np.unravel_index(np.argmax(sub), sub.shape) g_init = models.Gaussian2D(amplitude=sub.max(), x_mean=imax[1], y_mean=imax[0], x_stddev=loD[idx], y_stddev=loD[idx]) + \ models.Const2D(amplitude=sub.min()) fitter = fitting.LevMarLSQFitter() par = fitter(g_init, xx, yy, sub) fit = par(xx, yy) cx_final = cx - box + par[0].x_mean cy_final = cy - box + par[0].y_mean spot_center[idx, s, 0] = cx_final spot_center[idx, s, 1] = cy_final # plot sattelite spots and fit if save_path or display: ax.plot([cx_final], [cy_final], marker='D', color=col[s]) ax.add_patch( patches.Rectangle((cx - box, cy - box), 2 * box, 2 * box, ec='white', fc='none')) axs = fig.add_axes((0.17 + s * 0.2, 0.17, 0.1, 0.1)) axs.imshow(sub, aspect='equal', vmin=0, vmax=sub.max(), interpolation='nearest') axs.plot([par[0].x_mean], [par[0].y_mean], marker='D', color=col[s]) axs.set_xticks([]) axs.set_yticks([]) axs = fig.add_axes((0.17 + s * 0.2, 0.06, 0.1, 0.1)) axs.imshow(fit, aspect='equal', vmin=0, vmax=sub.max(), interpolation='nearest') axs.set_xticks([]) axs.set_yticks([]) # lines intersection intersect = lines_intersect(spot_center[idx, 0, :], spot_center[idx, 2, :], spot_center[idx, 1, :], spot_center[idx, 3, :]) img_center[idx] = intersect # scaling spot_dist[idx, 0] = np.sqrt( np.sum((spot_center[idx, 0, :] - spot_center[idx, 2, :])**2)) spot_dist[idx, 1] = np.sqrt( np.sum((spot_center[idx, 1, :] - spot_center[idx, 3, :])**2)) spot_dist[idx, 2] = np.sqrt( np.sum((spot_center[idx, 0, :] - spot_center[idx, 1, :])**2)) spot_dist[idx, 3] = np.sqrt( np.sum((spot_center[idx, 0, :] - spot_center[idx, 3, :])**2)) spot_dist[idx, 4] = np.sqrt( np.sum((spot_center[idx, 1, :] - spot_center[idx, 2, :])**2)) spot_dist[idx, 5] = np.sqrt( np.sum((spot_center[idx, 2, :] - spot_center[idx, 3, :])**2)) # finalize plot if save_path or display: ax.plot([spot_center[idx, 0, 0], spot_center[idx, 2, 0]], [spot_center[idx, 0, 1], spot_center[idx, 2, 1]], color='w', linestyle='dashed') ax.plot([spot_center[idx, 1, 0], spot_center[idx, 3, 0]], [spot_center[idx, 1, 1], spot_center[idx, 3, 1]], color='w', linestyle='dashed') ax.plot([intersect[0]], [intersect[1]], marker='+', color='w', ms=15) ext = 1000 / pixel ax.set_xlim(intersect[0] - ext, intersect[0] + ext) ax.set_ylim(intersect[1] - ext, intersect[1] + ext) plt.tight_layout() if save_path: pdf.savefig() if display: plt.pause(1e-3) if save_path: pdf.close() return spot_center, spot_dist, img_center
def iraf_style_photometry(phot_apertures, bg_apertures, data, dark_std_data, header, seeing, bg_method='mean', epadu=1.0, gain=8.21, non_linear_threshold=4000): """Computes photometry with PhotUtils apertures, with IRAF formulae Parameters ---------- phot_apertures : photutils PixelAperture object (or subclass) The PhotUtils apertures object to compute the photometry. i.e. the object returned via CircularAperture. bg_apertures : photutils PixelAperture object (or subclass) The phoutils aperture object to measure the background in. i.e. the object returned via CircularAnnulus. data : array The data for the image to be measured. bg_method: {'mean', 'median', 'mode'}, optional The statistic used to calculate the background. All measurements are sigma clipped. NOTE: From DAOPHOT, mode = 3 * median - 2 * mean. epadu: float, optional Gain in electrons per adu (only use if image units aren't e-). Returns ------- final_tbl : astropy.table.Table An astropy Table with the colums X, Y, flux, flux_error, mag, and mag_err measurements for each of the sources. """ exptime = header['EXPTIME'] if bg_method not in ['mean', 'median', 'mode']: raise ValueError( 'Invalid background method, choose either mean, median, or mode') #Create a list to hold the flux for each source. aperture_sum = [] interpolation_flags = np.zeros(len(phot_apertures.positions), dtype='bool') for i in range(len(phot_apertures.positions)): pos = phot_apertures.positions[i] #Cutout around the source position cutout_w = 15 x_pos = pos[0] y_pos = pos[1] cutout = data[int((y_pos - cutout_w)):int(y_pos + cutout_w) + 1, int(x_pos - cutout_w):int(x_pos + cutout_w) + 1] x_cutout = x_pos - np.floor(x_pos - cutout_w) y_cutout = y_pos - np.floor(y_pos - cutout_w) ap = CircularAperture((x_cutout, y_cutout), r=phot_apertures.r) #Cut out the pixels JUST inside the aperture, and check if there are NaNs there. If so, interpolate over NaNs in the cutout. ap_mask = ap.to_mask(method='exact') ap_cut = ap_mask.cutout(cutout) non_linear_sum = np.sum(ap_cut / gain > non_linear_threshold) # if non_linear_sum > 0: # print('Pixels in the non-linear range!') # breakpoint() bad_sum = np.sum(np.isnan(ap_cut)) if bad_sum > 0: bads = np.where(np.isnan(cutout)) bad_dists = np.sqrt((bads[0] - y_cutout)**2 + (bads[1] - x_cutout)**2) #Check if any bad pixels fall within the aperture. If so, set the interpolation flag to True for this source. if np.sum(bad_dists < phot_apertures.r + 1): # if np.sum(bad_dists < 1) == 0: # #ONLY interpolate if bad pixels lay away from centroid position by at least a pixel. # #2D gaussian fitting approach # #Set up a 2D Gaussian model to interpolate the bad pixel values in the cutout. # model_init = models.Const2D(amplitude=np.nanmedian(cutout))+models.Gaussian2D(amplitude=np.nanmax(cutout), x_mean=x_cutout, y_mean=y_cutout, x_stddev=seeing, y_stddev=seeing) # xx, yy = np.indices(cutout.shape) #2D grids of x and y coordinates # mask = ~np.isnan(cutout) #Find locations where the cutout has *good* values (i.e. not NaNs). # x = xx[mask] #Only use coordinates at these good values. # y = yy[mask] # cutout_1d = cutout[mask] #Make a 1D cutout using only the good values. # fitter = fitting.LevMarLSQFitter() # model_fit = fitter(model_init, x, y, cutout_1d) #Fit the model to the 1d cutout. # cutout[~mask] = model_fit(xx,yy)[~mask] #Interpolate the pixels in the cutout using the 2D Gaussian fit. # pdb.set_trace() # #TODO: interpolate_replace_nans with 2DGaussianKernel probably gives better estimation of *background* pixels. # else: # interpolation_flags[i] = True #2D gaussian fitting approach #Set up a 2D Gaussian model to interpolate the bad pixel values in the cutout. model_init = models.Const2D( amplitude=np.nanmedian(cutout)) + models.Gaussian2D( amplitude=np.nanmax(cutout), x_mean=x_cutout, y_mean=y_cutout, x_stddev=seeing, y_stddev=seeing) xx, yy = np.indices( cutout.shape) #2D grids of x and y coordinates mask = ~np.isnan( cutout ) #Find locations where the cutout has *good* values (i.e. not NaNs). x = xx[mask] #Only use coordinates at these good values. y = yy[mask] cutout_1d = cutout[ mask] #Make a 1D cutout using only the good values. fitter = fitting.LevMarLSQFitter() model_fit = fitter(model_init, x, y, cutout_1d) #Fit the model to the 1d cutout. # #Uncomment this block to show inerpolation plots. # norm = ImageNormalize(cutout, interval=ZScaleInterval()) # plt.ion() # fig, ax = plt.subplots(1, 4, figsize=(10,4), sharex=True, sharey=True) # ax[0].imshow(cutout, origin='lower', norm=norm) # ax[0].set_title('Data') # ax[1].imshow(model_fit(xx,yy), origin='lower', norm=norm) # ax[1].set_title('2D Gaussian Model') cutout[~mask] = model_fit( xx, yy )[~mask] #Interpolate the pixels in the cutout using the 2D Gaussian fit. # ax[2].imshow(cutout, origin='lower', norm=norm) # ax[2].set_title('Data w/ Bad\nPixels Replaced') # ax[3].imshow(cutout-model_fit(xx,yy), origin='lower') # ax[3].set_title('Residuals') # pdb.set_trace() interpolation_flags[i] = True # #Gaussian convolution approach. # cutout = interpolate_replace_nans(cutout, kernel=Gaussian2DKernel(x_stddev=0.5)) phot_source = aperture_photometry(cutout, ap) # if np.isnan(phot_source['aperture_sum'][0]): # pdb.set_trace() aperture_sum.append(phot_source['aperture_sum'][0]) #Add positions/fluxes to a table xcenter = phot_apertures.positions[:, 0] * u.pix ycenter = phot_apertures.positions[:, 1] * u.pix phot = QTable([xcenter, ycenter, aperture_sum], names=('xcenter', 'ycenter', 'aperture_sum')) #Now measure the background around each source. mask = make_source_mask( data, nsigma=3, npixels=5, dilate_size=7 ) #Make a mask to block out any sources that might show up in the annuli and bias them. bg_phot = aperture_stats_tbl( ~mask * data, bg_apertures, sigma_clip=True ) #Pass the data with sources masked out to the bg calculator. ap_area = phot_apertures.area bg_method_name = 'aperture_{}'.format(bg_method) background = bg_phot[bg_method_name] flux = phot['aperture_sum'] - background * ap_area # Need to use variance of the sources for Poisson noise term in error computation. flux_error = compute_phot_error(flux, bg_phot, bg_method, ap_area, exptime, dark_std_data, phot_apertures, epadu) mag = -2.5 * np.log10(flux) mag_err = 1.0857 * flux_error / flux # Make the final table X, Y = phot_apertures.positions.T stacked = np.stack([ X, Y, flux, flux_error, mag, mag_err, background, interpolation_flags ], axis=1) names = [ 'X', 'Y', 'flux', 'flux_error', 'mag', 'mag_error', 'background', 'interpolation_flag' ] final_tbl = Table(data=stacked, names=names) #Check for nans if sum(np.isnan(final_tbl['flux'])) > 0: bad_locs = np.where(np.isnan(final_tbl['flux']))[0] #pdb.set_trace() return final_tbl
def simulate_image(psffits=None, theofits=None, obsfits=None, distance=None, extfits=None, wlen=None, pfov=None, filt=None, psfext=0, obsext=0, bgstd=0, bgmed=0, silent=False, posang=0, foi_as=4, outname=None, manscale=None, fnucdiam_as=0.45, suffix=None, fitsize=None, outfolder='.', writesimfits=False, writetheofits=False, writeallfits=False, writefitplot=False, debug=False, returncmod=True, fitpsf=False, saveplot=False, meastime=False): """ Simulate a (VISIR) imaging observaion given a real image, a PSF reference image and a model image for a provided object distance and position angle Current constraints: - The routine assumes that the images are squares (x = y) """ if meastime: tstart = time.time() psfim = fits.getdata(psffits, ext=psfext) psfhead = fits.getheader(psffits) # print(obsfits, obsext) # ==== 1. Load Observational and PSF data ==== if obsfits is not None: obsim = fits.getdata(obsfits, ext=obsext) obshead = fits.getheader(obsfits) if wlen is None: wlen = float(obshead['WAVELEN']) if pfov is None: pfov = obshead['PFOV'] if filt is None: filt = obshead['Filter'] else: if wlen is None: wlen = float(psfhead['WAVELEN']) if pfov is None: pfov = psfhead['PFOV'] if filt is None: filt = psfhead['Filter'] wlenstr = "{:.1f}".format(wlen) diststr = "{:.1f}".format(distance) pastr = "{:.0f}".format(posang) # --- optinal cropping of the PSF image if fitsize is not None: psfim = _crop_image(psfim, box=fitsize, cenpos=_get_pointsource(psfim)[0][2:4]) psfsize = np.array(np.shape(psfim)) psfsize_as = psfsize[0] * pfov if not silent: print("Obs. wavelength [um]: " + wlenstr) # ==== 2. Prepare theoretical image for convolution ==== theohdu = fits.open(theofits) theohead = theohdu[0].header # --- check if provided file is the full cube if 'NAXIS3' in theohead: # find the right frame to be extracted mind, mwlen = find_wlen_match(theofits, wlen) theoim = theohdu[0].data[mind] if not silent: print(" - Model wavelength [um] | frame no: " + str(mwlen) + " | " + str(mind)) else: theoim = theohdu[0].data theohdu.close() if meastime: print(" - All files read. Elapsed time: ", time.time() - tstart) if debug is True: print("THEOIM: ") plt.imshow(theoim, origin='bottom', norm=LogNorm(), interpolation='nearest') plt.show() # --- if a manual scaling (fudge) factor was provided, apply to the model if manscale: theoim = theoim * manscale # --- rotate the model image (this changes its extent and thus has to be # done first) # unrot = np.copy(theoim) if np.abs(posang - 0) > 0.01: theoim = ndimage.interpolation.rotate(theoim, 180 - posang, order=0) if meastime: print(" - Model rotated. Elapsed time: ", time.time() - tstart) if debug is True: print("THEOIM_ROT: ") plt.imshow(theoim, origin='bottom', norm=LogNorm(), interpolation='nearest') plt.show() # --- size units of the theoretical image theopixsize_pc = theohead['CDELT1'] theosize = np.array(np.shape(theoim)) theosize_pc = theopixsize_pc * theosize[0] # pc theosize_as = 2 * np.arctan( theosize_pc / distance / 2.0e6) * 180 / np.pi * 3600 theopixsize_as = (2 * np.arctan(theopixsize_pc / distance / 2.e6) * 180 / np.pi * 3600) # plt.imshow(unrot, origin='bottom', norm=LogNorm()) # plt.imshow(theoim, origin='bottom', norm=LogNorm()) # plt.imshow(theoim, origin='bottom') # theopfov = angsize/theosize[0] # normalize image to flux = 1 # theoim = theoim/np.sum(theoim) # theoim = theoim/np.max(theoim) # --- convert to the right flux units # surface brightness unit in cube is W/m^2/arcsec2 # -> convert to flux density in mJy # print(np.sum(theoim)) freq = 2.99793e8 / (1e-6 * wlen) # print(freq) theoim = theoim * 1.0e29 / freq * theopixsize_as**2 theototflux = np.sum(theoim) # --- resample to the same pixel size as the observation # print( "- Resampling the model image to instrument pixel size...") # --- the ndimage.zoom sometimes creates weird artifacts and thus might not # be a good choice. Instead, use the self-made routine # theoim_resres = ndimage.zoom(theoim_res, theopixsize_as/pfov, order=0) # --- do the rasmpling before scaling it to the size of the observation to # save computing time theoim_res, sizerat = _increase_pixelsize(theoim, oldpfov=theopixsize_as, newpfov=pfov, meastime=False) if meastime: print(" - Pixelsize increased. Elapsed time: ", time.time() - tstart) if debug is True: print("sizerat: ", sizerat) print("THEOIM_RESRES: ") plt.imshow(theoim_res, origin='bottom', norm=LogNorm(), interpolation='nearest') plt.show() # --- if the PSF frame size is larger than the model extend the model frame framerat = psfsize_as / theosize_as if debug is True: print("psfsize_as: ", psfsize_as) print("theosize_as: ", theosize_as) print("theopixsize_as: ", theopixsize_as) print("theosize_pc: ", theosize_pc) print("theosize: ", theosize) print("framerat: ", framerat) #print("newsize: ", newsize) if framerat > 1.0: theoim_resres = _extend_image(theoim_res, fac=framerat) if meastime: print(" - Frame extended. Elapsed time: ", time.time() - tstart) if debug is True: print("thesize_ext: ", np.shape(theoim_resres)) print("THEOIM_RESRES: ") plt.imshow(theoim_resres, origin='bottom', norm=LogNorm(), interpolation='nearest') plt.show() # plt.imshow(theoim_res, origin='bottom') else: theoim_resres = np.copy(theoim_res) theosize_new = np.array(np.shape(theoim_resres)) if debug is True: print("theosize_new: ", theosize_new) print("new theosize_as: ", theosize_new * pfov) # print(np.sum(theoim_resres)) # --- after resampling we need to re-establish the right flux levels theoim_resres = theoim_resres / np.sum(theoim_resres) * theototflux # plt.imshow(theoim_resres, origin='bottom', norm=LogNorm()) # plt.imshow(theoim_resres, origin='bottom') # ==== 4. Optionally, apply a foreground extinction map if provided ==== if extfits: exthdu = fits.open(extfits) exthead = exthdu[0].header extpfov = exthead["PFOV"] # check if provided file is the full cube if 'NAXIS3' in exthead: # find the right frame to be extracted if not mind: mind, mwlen = find_wlen_match(theofits, wlen) extim = exthdu[0].data[mind] else: extim = exthdu[0].data # plt.imshow(extim,origin='bottom') # plt.show() exthdu.close() # --- Do we have to resample the extinction map if np.abs(extpfov - pfov) > 0.001: # print("Resampling extinction map... ") extim = ndimage.zoom(extim, 1.0 * pfov / extpfov, order=0) # --- match the size of the extinction map to the one of the model # image extsize = np.array(np.shape(extim)) if theosize_new[0] > extsize[0]: print(" - ERROR: provided extinction map is too small to cover \ the imaged region. Abort...") return () if extsize[0] > theosize_new[0]: # print("Cuttting exctinction map...") extim = _crop_image(extim, box=theosize_new) # --- finally apply the extinction map theoim_resres = theoim_resres * extim # print('Maximum extinction: ',np.min(extim)) # ==== 5. Obtain the Airy function from the calibrator ==== if debug is True: print("PSF_IM: ") plt.imshow(psfim, origin='bottom', norm=LogNorm(), interpolation='nearest') plt.show() # --- set up the fitting of the STD star image # --- assume that the maximum of the (cropped) image is the STD star if fitpsf is True: psfmax = np.max(psfim) psfmaxpos = np.unravel_index(np.argmax(psfim), psfsize) # print(im_bg, im_max, im_size, im_maxpos) # --- Use an Airy plus a Moffat + constant as PSF model c_init = models.Const2D(amplitude=np.median(psfim)) oa_init = obsc_AiryDisk2D(amplitude=psfmax, x_0=psfmaxpos[1], y_0=psfmaxpos[0], radius=5, obsrat=0.15) m_init = models.Moffat2D(amplitude=psfmax, x_0=psfmaxpos[1], y_0=psfmaxpos[0], gamma=5, alpha=1) a_plus_m = oa_init + m_init + c_init # print(psfmax, psfmaxpos) # --- Selection of fitting method fit_meth = fitting.LevMarLSQFitter() # --- Do the Fitting: y, x = np.mgrid[:psfsize[0], :psfsize[1]] am_fit = fit_meth(a_plus_m, x, y, psfim) # print(am_fit.radius_0.value, am_fit.obsrat_0.value) # then refine the fit by subtracting another Moffat # a_plus_m_minus_m = am_fit - m_init # amm_fit = fit_meth(a_plus_m_minus_m, x, y, psfim) # z_amm = amm_fit(x, y) # res = psfim - z_amm #print(amm_fit.radius_0.value, amm_fit.obsrat_0.value) if meastime: print(" - PSf fit. Elapsed time: ", time.time() - tstart) if debug is True: print("PSF_FIT: ") plt.imshow(am_fit(x, y), origin='bottom', norm=LogNorm(), interpolation='nearest') plt.show() # --- Genarate the PSF image with the fitted model # --- check whether the model image is on sky larger than the PSF image # and if this is the case increase the grid and adjust the positions # of the fits size_diff = theosize_new[0] - psfsize[0] if size_diff > 0: y, x = np.mgrid[:theosize_new[0], :theosize_new[1]] am_fit.x_0_0 = am_fit.x_0_0 + 0.5 * size_diff am_fit.y_0_0 = am_fit.y_0_0 + 0.5 * size_diff am_fit.x_0_1 = am_fit.x_0_1 + 0.5 * size_diff am_fit.y_0_1 = am_fit.y_0_1 + 0.5 * size_diff # --- create the final PSF image by subtracting the constant terms # and normalise by its area psf = am_fit(x, y) - am_fit.amplitude_2.value psf = psf / np.sum(psf) if debug is True: print("PSF_FINAL: ") plt.imshow(psf, origin='bottom', norm=LogNorm(), interpolation='nearest') plt.show() # plt.imshow(psf, origin='bottom', norm=LogNorm()) # plt.show() if writeallfits is True: fout = psffits.replace('.fits', '_fit.fits') if 'OBS NAME' in psfhead: psfhead.remove('OBS NAME') fits.writeto(fout, psf, psfhead, overwrite=True) # --- optinally, write the a plot documenting the quality of the PSF fit if writefitplot: fitplotfname = psffits.split('/')[-1] fitplotfname = fitplotfname.replace('.fits', '_fitplot.pdf') fitplotfname = outfolder + '/' + fitplotfname maxrad = int(0.5 * np.sqrt(0.5) * np.min(psfsize)) _make_fit_plots(psfim, am_fit(x, y), fitplotfname, am_fit.x_0_0, am_fit.y_0_0, maxrad, inv=True, cmap='gist_heat', labelcolor='black') # --- alternatively the PSF can also be direclty provided so that no fit is # necessary else: # normalise the provided psf psf = psfim - np.nanmin(psfim) psf = psf / np.nansum(psf) psf[np.argwhere(np.isnan(psf))] = 0 # ==== 6. Convolution of the model image with the PSF ==== simim = scipy.signal.convolve2d(theoim_resres, psf, mode='same') # print(np.sum(simim)) if meastime: print(" - Model convolved. Elapsed time: ", time.time() - tstart) if debug is True: print("SIMIM:") plt.imshow(simim, origin='bottom', norm=LogNorm(), interpolation='nearest') plt.show() # ==== 7. Flux measurements and application of noise ==== # --- Flux measurement on the model image before applying noise ftotmod = int(np.nansum(simim)) apos = 0.5 * np.array(theosize_new) nucrad_px = 0.5 * fnucdiam_as / pfov from aper import aper (mag, magerr, flux, fluxerr, sky, skyerr, badflag, outstr) = aper(simim, apos[1], apos[0], apr=nucrad_px, exact=True, setskyval=0) fnucmod = int(flux) if not silent: print(' - Model total | nuclear flux [mJy]: ' + str(ftotmod) + ' | ' + str(fnucmod)) plotsize_px = int(1.0 * foi_as / pfov) # --- Measure the background noise from the real observation if obsfits is not None: bg = np.copy(obsim) # bgsize = np.shape(bg) params, _, _ = _get_pointsource(obsim) xpos = params[2] ypos = params[3] bg[int(ypos - 0.5 * plotsize_px):int(ypos + 0.5 * plotsize_px), int(xpos - 0.5 * plotsize_px):int(xpos + 0.5 * plotsize_px)] = 0 if bgstd == 0: bgstd = np.std(bg[bg != 0]) if bgmed == 0: bgmed = np.median(bg[bg != 0]) # plt.imshow(bg, origin='bottom', norm=LogNorm(), cmap='gist_heat') # --- crop the observational image to the requested plot size cim = _crop_image(obsim, box=plotsize_px, cenpos=_get_pointsource(obsim)[0][2:4]) newsize = np.shape(cim) # --- make flux measurements on the nucleus and total ftotobs = int(np.sum(cim - bgmed)) apos = 0.5 * np.array(newsize) (mag, magerr, flux, fluxerr, sky, skyerr, badflag, outstr) = aper(cim, apos[1], apos[0], apr=nucrad_px, exact=True, setskyval=0) fnucobs = int(flux) if not silent: print(' - Observed total | nuclear flux [mJy]: ' + str(ftotobs) + ' | ' + str(fnucobs)) else: cim = None if meastime: print(" - Fluxes measured. Elapsed time: ", time.time() - tstart) # pdb.set_trace() # --- crop the simulated image to the requested plot size params, _, _ = _get_pointsource(simim) csimim = _crop_image(simim, box=plotsize_px, cenpos=_get_pointsource(simim)[0][2:4]) if debug is True: plt.imshow(simim, origin='bottom', cmap='gist_heat', norm=LogNorm()) plt.title('simim') plt.show() print(plotsize_px, np.shape(csimim)) plt.imshow(csimim, origin='bottom', cmap='gist_heat', norm=LogNorm()) plt.title('csimim') plt.show() if meastime: print(" - Obs cropped. Elapsed time: ", time.time() - tstart) # --- generate an artifical noise frame with same properties as in real # image if bgstd > 0: artbg = np.random.normal(scale=bgstd, size=(plotsize_px, plotsize_px)) + bgmed # artbg = 0 # --- apply the artificial background # csimim = csimim/np.max(csimim)*(np.max(cim)-bgmed)+bgmed+artbg csimim = csimim + artbg # --- crop the PSF image to the requested plot size cpsf = _crop_image(psf, box=plotsize_px, cenpos=_get_pointsource(psf)[0][2:4]) # print(bgmed, bgstd, np.std(artbg), np.median(cim), np.median(csimim), # np.std(cim), np.std(csimim), np.max(cim), np.min(cim), np.max(csimim), # np.min(csimim)) if meastime: print(" - PSF cropped. Elapsed time: ", time.time() - tstart) theopfov = theopixsize_as theobox = int(np.round(plotsize_px * pfov / theopfov)) if returncmod: # --- crop the model imae to the requested plot size if framerat > 1.0: theoim_res_0 = _extend_image(theoim, fac=framerat) else: theoim_res_0 = theoim cmod = _crop_image(theoim_res_0, box=theobox, exact=False) # plt.imshow(cmod, origin='bottom', norm=LogNorm(), cmap='gist_heat') if meastime: print(" - Theo cropped. Elapsed time: ", time.time() - tstart) else: cmod = None # --- write out the simulated image as a fits file if not outname: theofitsfile = theofits.split("/")[-1] out_str = theofitsfile.replace("_total.fits", "") out_str = (out_str + "_pa" + pastr + "_dist" + diststr + "_wlen" + wlenstr) if suffix: out_str = out_str + "_" + suffix else: out_str = outname out_str = outfolder + '/' + out_str if writeallfits or writetheofits: fout = out_str + '_mod.fits' theohead["Filter"] = filt theohead["WAVELEN"] = wlen theohead["PFOV"] = theopixsize_as fits.writeto(fout, cmod, theohead, overwrite=True) if saveplot: _simple_image_plot(cmod, fout.replace(".fits", ".png"), log=True) if writeallfits or writesimfits: fout = out_str + '_sim.fits' theohead["PFOV"] = pfov theohead["BUNIT"] = 'mJy' ts = np.shape(simim) theohead["CRPIX1"] = 0.5 * ts[1] theohead["CDELT1"] = pfov theohead["CTYPE1"] = 'arcsec' theohead["CRPIX2"] = 0.5 * ts[0] theohead["CDELT2"] = pfov theohead["CTYPE2"] = 'arcsec' fits.writeto(fout, csimim, theohead, overwrite=True) if saveplot: _simple_image_plot(csimim, fout.replace(".fits", ".png"), log=True) if meastime: print(" - All finished: ", time.time() - tstart) return (cpsf, cmod, cim, csimim, wlen, pfov, theopfov)
def star_centers_from_PSF_cube(cube, wave, pixel, display=False, save_path=None): ''' Compute star center from PSF images Parameters ---------- cube : array_like PSF IFS cube wave : array_like Wavelength values, in nanometers pixel : float Pixel scale, in mas/pixel display : bool Display the fit of the satelitte spots save_path : str Path where to save the fit images Returns ------- img_center : array_like The star center in each frame of the cube ''' # standard parameters nwave = wave.size loD = wave * 1e-6 / 8 * 180 / np.pi * 3600 * 1000 / pixel box = 30 # spot fitting xx, yy = np.meshgrid(np.arange(2 * box), np.arange(2 * box)) # multi-page PDF to save result if save_path is not None: pdf = PdfPages(save_path) # loop over images img_center = np.zeros((nwave, 2)) for idx, (wave, img) in enumerate(zip(wave, cube)): print(' wave {0:2d}/{1:2d} ({2:.3f} micron)'.format( idx + 1, nwave, wave)) # remove any NaN img = np.nan_to_num(img) # center guess cy, cx = np.unravel_index(np.argmax(img), img.shape) # sub-image sub = img[cy - box:cy + box, cx - box:cx + box] # fit peak with Gaussian + constant imax = np.unravel_index(np.argmax(sub), sub.shape) g_init = models.Gaussian2D(amplitude=sub.max(), x_mean=imax[1], y_mean=imax[0], x_stddev=loD[idx], y_stddev=loD[idx]) + \ models.Const2D(amplitude=sub.min()) fitter = fitting.LevMarLSQFitter() par = fitter(g_init, xx, yy, sub) cx_final = cx - box + par[0].x_mean cy_final = cy - box + par[0].y_mean img_center[idx, 0] = cx_final img_center[idx, 1] = cy_final if save_path or display: fig = plt.figure(0, figsize=(8, 8)) plt.clf() ax = fig.add_subplot(111) ax.imshow(img / img.max(), aspect='equal', vmin=1e-6, vmax=1, norm=colors.LogNorm(), interpolation='nearest') ax.plot([cx_final], [cy_final], marker='D', color='red') ax.add_patch( patches.Rectangle((cx - box, cy - box), 2 * box, 2 * box, ec='white', fc='none')) ax.set_title(r'Image #{0} - {1:.3f} $\mu$m'.format(idx + 1, wave)) ext = 1000 / pixel ax.set_xlim(cx_final - ext, cx_final + ext) ax.set_ylim(cy_final - ext, cy_final + ext) plt.tight_layout() if save_path: pdf.savefig() if display: plt.pause(1e-3) if save_path: pdf.close() return img_center
def autocorrelation_fit(im2, rough_sep = 44.93, rough_pa=0., cutout_sz=5, background_subtracted=False, plot_cutout=False, plot_resids=False): """ Fits to the distance of the second peak in the autocorrelation (i.e. the binary separation). This will perform a Levenberg-Marquardt fit over a small, fixed region around the peak. The model used for the fit will depend on the options set. rough_sep, rough_pa: Defaults 44.93 pixels and 0 degrees These define the position of the region used for the fit. cutout_sz: Default 5 pixels The radius of the box used for the fit. background_subtracted: Default False If True, the model used for the fit will be an Airy Disk + constant background. If False, the model will be a Gaussian + planar background. These seemed to work best. """ rough_pos = np.round([rough_sep*np.sin(rough_pa),rough_sep*np.cos(rough_pa)]).astype(int) calc_pos = [rough_sep*np.sin(rough_pa),rough_sep*np.cos(rough_pa)] cutout = np.copy(im2[im2.shape[0]//2+rough_pos[0]-cutout_sz:im2.shape[0]//2+rough_pos[0]+cutout_sz, im2.shape[1]//2+rough_pos[1]-cutout_sz:im2.shape[1]//2+rough_pos[1]+cutout_sz]) cutout /= np.max(cutout) if plot_cutout: plt.imshow(cutout) x,y = np.indices(cutout.shape) x = x + rough_pos[0] - cutout_sz y = y + rough_pos[1] - cutout_sz # Fit a Gaussian fit = fitting.LevMarLSQFitter() if background_subtracted: gauss = models.AiryDisk2D(amplitude=cutout.max(),x_0=calc_pos[0],y_0=calc_pos[1],radius=3.54) bckgd = models.Const2D(amplitude=0.6) else: gauss = models.Gaussian2D(amplitude = cutout.max(),x_stddev=1.36,y_stddev=1.36, x_mean=calc_pos[0],y_mean=calc_pos[1]) bckgd = models.Planar2D(slope_x = -0.00564816,slope_y=-0.02378304,intercept=1.01) gauss.fixed['theta']=True cutout_model = gauss + bckgd # fit the data with the fitter fitted_model = fit(cutout_model,x,y,cutout,maxiter=100000,acc=1e-7); # Rename the parameters so the output looks the same if background_subtracted: fitted_model.x_mean_0 = fitted_model.x_0_0 fitted_model.y_mean_0 = fitted_model.y_0_0 if plot_resids: test = fitted_model(x,y) plt.figure(figsize=(12,4)) plt.clf() plt.subplot(131) plt.imshow(cutout,origin='lowerleft',vmin=0.7,vmax=1);plt.colorbar() plt.subplot(132) plt.imshow(test,origin='lowerleft',vmin=0.7,vmax=1);plt.colorbar() plt.subplot(133) plt.imshow(cutout-test,origin='lowerleft',vmin=-0.05,vmax=0.05);plt.colorbar() return fitted_model
def _do_fitting(self): """Performs 1-D and 2-G gaussian fits to the stars in the internal fit_table. A 2-dimensional Gaussian plus flat background model is fit to each cut-out image. The model is initialized with the global background level, and source centroid and peak value from the input photometry list. The Levenberg-Marquardt non-linear least squares fitting algorithm is used to fit the data. The data is assumed to have Gaussian errors that are the square root of the pixel counts. There are some limitations to this at present: - The reduced chi-squared values obtained are unrealistic. It is not clear whether the errors are underestimated and/or the fitting has trouble converging to the true solution. """ # Setup num_stars = len(self._fit_table) sigma_to_fwhm = 2.35482 fwhm_to_sigma = 1.0 / sigma_to_fwhm max_iterations = 500 # Fit for x and y after initially just fitting for amplitude, # fwhm, and theta? is_pos_fitted_for = True # Extract data from main image, stores in _pixel_array. self._extract_cutouts() # Add extra columns to the output table for the fit results self._prepare_fit_columns() self._logger.info( f'Starting to fit Const2D+XX models to {num_stars} star cutouts.') perf_time_start = time.perf_counter( ) # highest res timer, counts sleeps # X and Y pixel index grids needed by the fitter x_grid, y_grid = np.mgrid[0:self._box_width_pix, 0:self._box_width_pix] # We initialize the 2-D Gaussians based on the initial FWHM # but with a slight asymmetry sig_x/sig_y = 1.1, theta approx 30 degrees. def_axrat = 1.05 sig_y = self._init_fwhm * fwhm_to_sigma sig_x = def_axrat * sig_y rotang = 1.1 # radians, approx 60 degrees. xpos = self._box_width_pix / 2 ypos = self._box_width_pix / 2 ampl = 1 num_fit_pars = 0 # The background model starts off fixed. bg_mod = models.Const2D(amplitude=self._init_bglvl) bg_mod.amplitude.fixed = True # 2-D gaussian model for the star. star_mod = models.Gaussian2D(amplitude=ampl, x_mean=xpos, y_mean=ypos, x_stddev=sig_x, y_stddev=sig_y, theta=rotang) # Constraint theta to -pi/2 to pi/2 # NOTE Disabled as it results in the fitter failing very often ##half_pi = 0.5 * math.pi ##star_mod.theta.bounds = (-half_pi, half_pi) # Start fits at the initial centroid positions star_mod.x_mean.fixed = True star_mod.y_mean.fixed = True num_fit_pars += 4 # 4 free fit parameters comb_mod = star_mod + bg_mod fitter = fitting.LevMarLSQFitter() # Iterate over stars, first updating initial values, then fitting, # then storing the fit results. for idx in range(num_stars): # Get better estimate for initial parameters, in particular # the amplitude. # Note: you can set a Parameter object by value directly, # (e.g. "bob=3") but to access it you must access its value # attribute (e.g. "x = bob.value") starid = self._fit_table['id'][idx] xpos = self._fit_table['xcenter'][idx] - self._fit_table['xmin'][ idx] ypos = self._fit_table['ycenter'][idx] - self._fit_table['ymin'][ idx] ampl = self._fit_table['peak_adu'][idx] bglvl = self._fit_table['bgmed_per_pix'][idx] comb_mod[0].x_mean = xpos comb_mod[0].y_mean = ypos comb_mod[0].amplitude = ampl comb_mod[1].amplitude = bglvl current_num_fit_pars = num_fit_pars # Estimated standard devation of the data. var_arr = np.where(self._pixel_array[idx] > 0, self._pixel_array[idx], 1) mean_variance = np.mean(var_arr[var_arr != 1]) rms_stddev = math.sqrt(mean_variance) std_arr = np.where(var_arr != 1, np.sqrt(var_arr), rms_stddev) # If used, the weight array should have values of 1/sigma if self._use_weights: weights_arr = 1.0 / std_arr else: weights_arr = None # Fit self._logger.debug( f'Fitting star #{idx} (id={starid}) at fixed xpos={xpos:.2f}, ypos={ypos:.2f}' ) fitted_mod, fit_status, fit_message, fit_ok, uncert_vals = self._do_single_fit( fitter, comb_mod, self._pixel_array[idx], weights_arr, x_grid, y_grid, max_iterations, current_num_fit_pars, idx) # If the ift is OK, and fit_for_bg is true, then fit for the BG level. if (fit_ok) and (self._fit_for_bg is True): fitted_mod[1].amplitude.fixed = False current_num_fit_pars += 1 fitted_mod, fit_status, fit_message, fit_ok, uncert_vals = self._do_single_fit( fitter, fitted_mod, self._pixel_array[idx], weights_arr, x_grid, y_grid, max_iterations, current_num_fit_pars, idx) # If the fit did NOT fail we can relax the constraints on the # position of the gaussian. if fit_ok and is_pos_fitted_for: fitted_mod[0].x_mean.fixed = False fitted_mod[0].y_mean.fixed = False current_num_fit_pars += 2 fitted_mod, fit_status, fit_message, fit_ok, uncert_vals = self._do_single_fit( fitter, fitted_mod, self._pixel_array[idx], weights_arr, x_grid, y_grid, max_iterations, current_num_fit_pars, idx) # Calculate reduced chi squared. rchisq = self._calculate_rchisq(self._pixel_array[idx], x_grid, y_grid, std_arr, fitted_mod, current_num_fit_pars) # Extract fit parameters # Unfortunately we have to hardcode the parameter order. :( # And which columns they should go into. fit_par_dict = { 'xc_fit': 1, # Not used. 'yc_fit': 2, 'ampl': 0, 'fwhm_x': 3, 'fwhm_y': 4, 'theta': 5 } if is_pos_fitted_for: # Error par dict if xc and yc are fitted for err_par_dict = { 'xc_err': 1, 'yc_err': 2, 'ampl_err': 0, 'fwhm_x_err': 3, 'fwhm_y_err': 4, 'theta_err': 5 } else: # Error par dict if xc and yc are fixed err_par_dict = { 'ampl_err': 0, 'fwhm_x_err': 1, 'fwhm_y_err': 2, 'theta_err': 3 } self._fit_table['ampl'][idx] = fitted_mod[0].amplitude.value self._fit_table['xc_fit'][idx] = fitted_mod[0].x_mean.value self._fit_table['yc_fit'][idx] = fitted_mod[0].y_mean.value self._fit_table['fwhm_x'][ idx] = sigma_to_fwhm * fitted_mod[0].x_stddev.value self._fit_table['fwhm_y'][ idx] = sigma_to_fwhm * fitted_mod[0].y_stddev.value self._fit_table['theta'][idx] = fitted_mod[0].theta.value self._fit_table['fit_ok'][idx] = fit_ok self._fit_table['rchisq'][idx] = rchisq # Get the errors. if fit_ok: for col in err_par_dict: paridx = err_par_dict[col] if 'fwhm' in col: self._fit_table[col][ idx] = sigma_to_fwhm * uncert_vals[paridx] else: self._fit_table[col][idx] = uncert_vals[paridx] # Calculate axis ratio and circularity. # Note this calculation is not quite right because the two # parameters are NOT independent. fwhm_x = self._fit_table['fwhm_x'][idx] fwhm_y = self._fit_table['fwhm_y'][idx] axrat = max(fwhm_x, fwhm_y) / min(fwhm_x, fwhm_y) axrat_err = 0 if fit_ok: fwhm_xerr = self._fit_table['fwhm_x_err'][idx] fwhm_yerr = self._fit_table['fwhm_y_err'][idx] axrat_err = axrat * math.sqrt((fwhm_xerr / fwhm_x)**2 + (fwhm_yerr / fwhm_y)**2) circular = self.is_circular(fwhm_x, fwhm_y, fwhm_xerr, fwhm_yerr) self._fit_table['circular'][idx] = circular self._fit_table['axrat'][idx] = axrat self._fit_table['axrat_err'][idx] = axrat_err perf_time_end = time.perf_counter() # highest res timer, counts sleeps perf_time = perf_time_end - perf_time_start # seconds avg_time_ms = 1000.0 * perf_time / num_stars # milliseconds self._logger.debug( f'Fitting completed in {perf_time:.3f} s (average {avg_time_ms:.1f} ms/star)' ) if not self._quiet: print('Fit results:') print(self._fit_table) print('') return
def peak_center(img, window=0, edges=0, mask=None): '''Determine the center position of a peak Parameters ---------- img : array Image where the peak must be found window : int Half-size of sub-window in which to do the fit. If 0, then the fit is performed over the full image. Default is 0 edges : int Number of pixels to hide on the edges. Default is 0 mask : array Mask to apply to the data. Default is None Returns ------- cx, cy : tuple Center of the peak ''' img = img.copy() # hide edges if edges > 0: img[:edges, :] = 0 img[:, :edges] = 0 img[edges:, :] = 0 img[:, edges:] = 0 # apply mask if mask is not None: img *= mask # estimate peak position imax = np.unravel_index(np.argmax(img), img.shape) cx_int = imax[1] cy_int = imax[0] # sub-window win = window // 2 if window > 0: if (window % 2): raise ValueError('window parameter must be even') sub = img[cy_int - win:cy_int + win, cx_int - win:cx_int + win] cx_init = win cy_init = win else: sub = img cx_init = cx_int cy_init = cy_int # fit x, y = np.meshgrid(np.arange(sub.shape[1]), np.arange(sub.shape[0])) g_init = models.Gaussian2D(amplitude=sub.max(), x_mean=cx_init, y_mean=cy_init) + \ models.Const2D(amplitude=0) fit_g = fitting.LevMarLSQFitter() g = fit_g(g_init, x, y, sub) cx = g[0].x_mean.value - cx_init + cx_int cy = g[0].y_mean.value - cy_init + cy_int return cx, cy
def star_centers_from_waffle_img_cube(cube_cen, wave, waffle_orientation, center_guess, pixel, orientation_offset, high_pass=False, center_offset=(0, 0), box_size=16, smooth=0, coro=True, save_path=None, logger=_log): ''' Compute star center from waffle images (IRDIS CI, IRDIS DBI, IFS) Parameters ---------- cube_cen : array_like IRDIFS waffle cube wave : array_like Wavelength values, in nanometers waffle_orientation : str String giving the waffle orientation '+' or 'x' center_guess : array Estimation of the image center as a function of wavelength. This should be an array of shape nwave*2. pixel : float Pixel scale, in mas/pixel orientation_offset : float Field orientation offset, in degrees high_pass : bool Apply high-pass filter to the image before searching for the satelitte spots. Default is False smooth : int Apply a gaussian smoothing to the images to reduce noise. The value is the sigma of the gaussian in pixel. Default is no smoothing center_offset : tuple Apply an (x,y) offset to the default center position. The offset will move the search box of the waffle spots by the amount of specified pixels in each direction. Default is no offset box_size : int Size of the box in which the fit is performed. Default is 16 pixels coro : bool Observation was performed with a coronagraph. Default is True save_path : str Path where to save the fit images. Default is None, which means that the plot is not produced logger : logHandler object Log handler for the reduction. Default is root logger Returns ------- spot_centers : array_like Centers of each individual spot in each frame of the cube spot_dist : array_like The 6 possible distances between the different spots img_centers : array_like The star center in each frame of the cube ''' # standard parameters nwave = wave.size loD = wave * 1e-9 / 8 * 180 / np.pi * 3600 * 1000 / pixel # waffle parameters freq = 10 * np.sqrt(2) * 0.97 box = box_size // 2 if waffle_orientation == '+': orient = orientation_offset * np.pi / 180 elif waffle_orientation == 'x': orient = orientation_offset * np.pi / 180 + np.pi / 4 # spot fitting xx, yy = np.meshgrid(np.arange(2 * box), np.arange(2 * box)) # multi-page PDF to save result if save_path is not None: pdf = PdfPages(save_path) # loop over images spot_centers = np.zeros((nwave, 4, 2)) spot_dist = np.zeros((nwave, 6)) img_centers = np.zeros((nwave, 2)) for idx, (wave, img) in enumerate(zip(wave, cube_cen)): logger.info(' ==> wave {0:2d}/{1:2d} ({2:4.0f} nm)'.format( idx + 1, nwave, wave)) # remove any NaN img = np.nan_to_num(img) # center guess (+offset) cx_int = int(center_guess[idx, 0]) + center_offset[0] cy_int = int(center_guess[idx, 1]) + center_offset[1] # optional high-pass filter if high_pass: img = img - ndimage.median_filter(img, 15, mode='mirror') # optional smoothing if smooth > 0: img = ndimage.gaussian_filter(img, smooth) # mask for non-coronagraphic observations if not coro: mask = aperture.disc(cube_cen[0].shape[-1], 5 * loD[idx], diameter=False, center=(cx_int, cy_int), invert=True) img *= mask # create plot if needed if save_path: fig = plt.figure('Waffle center - imaging', figsize=(8.3, 8)) plt.clf() if high_pass: norm = colors.PowerNorm(gamma=1, vmin=-1e-1, vmax=1e-1) else: norm = colors.LogNorm(vmin=1e-2, vmax=1) col = ['green', 'blue', 'deepskyblue', 'purple'] ax = fig.add_subplot(111) ax.imshow(img / img.max(), aspect='equal', norm=norm, interpolation='nearest', cmap=global_cmap) ax.set_title(r'Image #{0} - {1:.0f} nm'.format(idx + 1, wave)) ax.set_xlabel('x position [pix]') ax.set_ylabel('y position [pix]') # satelitte spots for s in range(4): cx = int(cx_int + freq * loD[idx] * np.cos(orient + np.pi / 2 * s)) cy = int(cy_int + freq * loD[idx] * np.sin(orient + np.pi / 2 * s)) sub = img[cy - box:cy + box, cx - box:cx + box] # bounds for fitting: spots slightly outside of the box are allowed gbounds = { 'amplitude': (0.0, None), 'x_mean': (-2.0, box * 2 + 2), 'y_mean': (-2.0, box * 2 + 2), 'x_stddev': (1.0, 20.0), 'y_stddev': (1.0, 20.0) } # fit: Gaussian + constant imax = np.unravel_index(np.argmax(sub), sub.shape) g_init = models.Gaussian2D(amplitude=sub.max(), x_mean=imax[1], y_mean=imax[0], x_stddev=loD[idx], y_stddev=loD[idx], bounds=gbounds) + \ models.Const2D(amplitude=sub.min()) fitter = fitting.LevMarLSQFitter() par = fitter(g_init, xx, yy, sub) fit = par(xx, yy) cx_final = cx - box + par[0].x_mean cy_final = cy - box + par[0].y_mean spot_centers[idx, s, 0] = cx_final spot_centers[idx, s, 1] = cy_final # plot sattelite spots and fit if save_path: ax.plot([cx_final], [cy_final], marker='D', color=col[s], zorder=1000) ax.add_patch( patches.Rectangle((cx - box, cy - box), 2 * box, 2 * box, ec='white', fc='none')) axs = fig.add_axes((0.17 + s * 0.2, 0.17, 0.1, 0.1)) axs.imshow(sub, aspect='equal', vmin=0, vmax=sub.max(), interpolation='nearest', cmap=global_cmap) axs.plot([par[0].x_mean.value], [par[0].y_mean.value], marker='D', color=col[s]) axs.set_xticks([]) axs.set_yticks([]) axs = fig.add_axes((0.17 + s * 0.2, 0.06, 0.1, 0.1)) axs.imshow(fit, aspect='equal', vmin=0, vmax=sub.max(), interpolation='nearest', cmap=global_cmap) axs.set_xticks([]) axs.set_yticks([]) # lines intersection intersect = lines_intersect(spot_centers[idx, 0, :], spot_centers[idx, 2, :], spot_centers[idx, 1, :], spot_centers[idx, 3, :]) img_centers[idx] = intersect # scaling spot_dist[idx, 0] = np.sqrt( np.sum((spot_centers[idx, 0, :] - spot_centers[idx, 2, :])**2)) spot_dist[idx, 1] = np.sqrt( np.sum((spot_centers[idx, 1, :] - spot_centers[idx, 3, :])**2)) spot_dist[idx, 2] = np.sqrt( np.sum((spot_centers[idx, 0, :] - spot_centers[idx, 1, :])**2)) spot_dist[idx, 3] = np.sqrt( np.sum((spot_centers[idx, 0, :] - spot_centers[idx, 3, :])**2)) spot_dist[idx, 4] = np.sqrt( np.sum((spot_centers[idx, 1, :] - spot_centers[idx, 2, :])**2)) spot_dist[idx, 5] = np.sqrt( np.sum((spot_centers[idx, 2, :] - spot_centers[idx, 3, :])**2)) # finalize plot if save_path: ax.plot([spot_centers[idx, 0, 0], spot_centers[idx, 2, 0]], [spot_centers[idx, 0, 1], spot_centers[idx, 2, 1]], color='w', linestyle='dashed', zorder=900) ax.plot([spot_centers[idx, 1, 0], spot_centers[idx, 3, 0]], [spot_centers[idx, 1, 1], spot_centers[idx, 3, 1]], color='w', linestyle='dashed', zorder=900) ax.plot([intersect[0]], [intersect[1]], marker='+', color='w', ms=15) ext = 1000 / pixel ax.set_xlim(intersect[0] - ext, intersect[0] + ext) ax.set_ylim(intersect[1] - ext, intersect[1] + ext) plt.subplots_adjust(left=0.1, right=0.98, bottom=0.1, top=0.95) if save_path: pdf.savefig() if save_path: pdf.close() return spot_centers, spot_dist, img_centers
def prepare_psf_model(psfmodel, xname=None, yname=None, fluxname=None, renormalize_psf=True): """ Convert a 2D PSF model to one suitable for use with `BasicPSFPhotometry` or its subclasses. The resulting model may be a composite model, but should have only the x, y, and flux related parameters un-fixed. Parameters ---------- psfmodel : a 2D model The model to assume as representative of the PSF. xname : str or None The name of the ``psfmodel`` parameter that corresponds to the x-axis center of the PSF. If None, the model will be assumed to be centered at x=0, and a new parameter will be added for the offset. yname : str or None The name of the ``psfmodel`` parameter that corresponds to the y-axis center of the PSF. If None, the model will be assumed to be centered at y=0, and a new parameter will be added for the offset. fluxname : str or None The name of the ``psfmodel`` parameter that corresponds to the total flux of the star. If None, a scaling factor will be added to the model. renormalize_psf : bool If True, the model will be integrated from -inf to inf and re-scaled so that the total integrates to 1. Note that this renormalization only occurs *once*, so if the total flux of ``psfmodel`` depends on position, this will *not* be correct. Returns ------- outmod : a model A new model ready to be passed into `BasicPSFPhotometry` or its subclasses. """ if xname is None: xinmod = models.Shift(0, name='x_offset') xname = 'offset_0' else: xinmod = models.Identity(1) xname = xname + '_2' xinmod.fittable = True if yname is None: yinmod = models.Shift(0, name='y_offset') yname = 'offset_1' else: yinmod = models.Identity(1) yname = yname + '_2' yinmod.fittable = True outmod = (xinmod & yinmod) | psfmodel if fluxname is None: outmod = outmod * models.Const2D(1, name='flux_scaling') fluxname = 'amplitude_3' else: fluxname = fluxname + '_2' if renormalize_psf: # we do the import here because other machinery works w/o scipy from scipy import integrate integrand = integrate.dblquad(psfmodel, -np.inf, np.inf, lambda x: -np.inf, lambda x: np.inf)[0] normmod = models.Const2D(1. / integrand, name='renormalize_scaling') outmod = outmod * normmod # final setup of the output model - fix all the non-offset/scale # parameters for pnm in outmod.param_names: outmod.fixed[pnm] = pnm not in (xname, yname, fluxname) # and set the names so that BasicPSFPhotometry knows what to do outmod.xname = xname outmod.yname = yname outmod.fluxname = fluxname # now some convenience aliases if reasonable outmod.psfmodel = outmod[2] if 'x_0' not in outmod.param_names and 'y_0' not in outmod.param_names: outmod.x_0 = getattr(outmod, xname) outmod.y_0 = getattr(outmod, yname) if 'flux' not in outmod.param_names: outmod.flux = getattr(outmod, fluxname) return outmod
def fit_alignment_box(region, box_size=30, verbose=False, seeing=None, medfilt=False): pixelscale = u.pixel_scale(0.1798*u.arcsec/u.pixel) if medfilt is True: region = median_filter(region, size=(3,3)) # Estimate center of alignment box threshold_pct = 80 window = region.data > np.percentile(region.data, threshold_pct) alignment_box_position = ndimage.measurements.center_of_mass(window) offset_val = np.median(region.data[~window]) offset = models.Const2D(offset_val) # Determine fluctuations in sky sky_amplitude = np.median(region.data[window]) sky_fluctuations = np.std(region.data[window]) # Detect box edges gradx = np.gradient(region.data, axis=1) horizontal_profile = np.sum(gradx, axis=0) h_edges = fit_CSU_edges(horizontal_profile) grady = np.gradient(region.data, axis=0) vertical_profile = np.sum(grady, axis=1) v_edges = fit_CSU_edges(vertical_profile) # Estimate stellar position maxr = np.max(region.data) starloc = (np.where(region == maxr)[0][0], np.where(region == maxr)[1][0]) # Build model of sky, star, & box boxamplitude = 1 box = mosfireAlignmentBox(boxamplitude, alignment_box_position[1], alignment_box_position[0],\ abs(h_edges[0]-h_edges[1]), abs(v_edges[0]-v_edges[1])) box.amplitude.fixed = True box.x_width.min = 10 box.y_width.min = 10 sky = models.Const2D(sky_amplitude) sky.amplitude.min = 0 star_amplitude = maxr - sky_amplitude star_sigma = star_amplitude / sky_fluctuations if star_sigma < 5: if verbose: print(f'No star detected. sigma={star_sigma:.1f}') return [None]*4 else: if verbose: print(f'Detected peak pixel {star_sigma:.1f} sigma above sky.') star = models.Gaussian2D(amplitude=star_amplitude, x_mean=starloc[1], y_mean=starloc[0], x_stddev=2, y_stddev=2) # print(h_edges) # print(v_edges) # star.y_mean.min = v_edges[0] # star.y_mean.max = v_edges[1] # star.x_mean.min = h_edges[0] # star.x_mean.max = h_edges[1] star.amplitude.min = 5*sky_fluctuations star.x_stddev.min = 1 # FWHM = 2.355*stddev = 0.42 arcsec FWHM star.x_stddev.max = 4 # FWHM = 2.355*stddev = 1.47 arcsec FWHM star.y_stddev.min = 1 star.y_stddev.max = 4 if seeing is not None and seeing > 0: sigma = (seeing / 2.355 * u.arcsec).to(u.pixel, equivalencies=pixelscale) star.x_stddev.min = max(2, sigma.value-1) star.y_stddev.min = max(2, sigma.value-1) star.x_stddev.max = min(sigma.value+1, 4) star.y_stddev.max = min(sigma.value+1, 4) # print(f"Using seeing value {seeing} arcsec. sigma limits {star.x_stddev.min}, {star.x_stddev.max} pix") model = box*(sky + star) + offset # modelim = np.zeros((61,61)) # fitim = np.zeros((61,61)) # for i in range(0,60): # for j in range(0,60): # modelim[j,i] = model(i,j) # fitim[j,i] = model(i,j) # residuals = region.data-fitim # residualsum = np.sum(residuals) # import pdb ; pdb.set_trace() fitter = fitting.LevMarLSQFitter() y, x = np.mgrid[:2*box_size+1, :2*box_size+1] fit = fitter(model, x, y, region.data) FWHMx = 2*(2*np.log(2))**0.5*fit.x_stddev_2.value * u.pix FWHMy = 2*(2*np.log(2))**0.5*fit.y_stddev_2.value * u.pix FWHM = (FWHMx**2 + FWHMy**2)**0.5/2**0.5 FWHMarcsec = FWHM.to(u.arcsec, equivalencies=pixelscale) sky_amplitude = fit.amplitude_1.value star_flux = 2*np.pi*fit.amplitude_2.value*fit.x_stddev_2.value*fit.y_stddev_2.value star_amplitude = fit.amplitude_2.value boxpos_x = fit.x_0_0.value boxpos_y = fit.y_0_0.value star_x = fit.x_mean_2.value star_y = fit.y_mean_2.value if verbose: print(f" Box X Center = {boxpos_x:.0f}") if verbose: print(f" Box Y Center = {boxpos_y:.0f}") if verbose: print(f" Sky Brightness = {fit.amplitude_1.value:.0f} ADU") if verbose: print(f" Stellar FWHM = {FWHMarcsec:.2f}") if verbose: print(f" Stellar Xpos = {star_x:.0f}") if verbose: print(f" Stellar Xpos = {star_y:.0f}") if verbose: print(f" Stellar Amplitude = {star_amplitude:.0f} ADU") if verbose: print(f" Stellar Flux (fit) = {star_flux:.0f} ADU") result = {'Star X': star_x, 'Star Y': star_y, 'Star Amplitude': star_amplitude, 'Sky Amplitude': sky_amplitude, 'FWHM pix': FWHM.value, 'FWHM arcsec': FWHMarcsec, 'Box X': boxpos_x, 'Box Y': boxpos_y, # 'Residuals': residuals, } return result
def star_centers_from_PSF_img_cube(cube, wave, pixel, exclude_fraction=0.1, high_pass=False, box_size=60, save_path=None, logger=_log): ''' Compute star center from PSF images (IRDIS CI, IRDIS DBI, IFS) Parameters ---------- cube : array_like IRDIFS PSF cube wave : array_like Wavelength values, in nanometers pixel : float Pixel scale, in mas/pixel exclude_fraction : float Exclude a fraction of the image borders to avoid getting biased by hot pixels close to the edges. Default is 10% high_pass : bool Apply high-pass filter to the PSF image before searching for the center. Default is False box_size : int Size of the box in which the fit is performed. Default is 60 pixels save_path : str Path where to save the fit images. Default is None, which means that the plot is not produced logger : logHandler object Log handler for the reduction. Default is root logger Returns ------- img_centers : array_like The star center in each frame of the cube ''' # standard parameters nwave = wave.size loD = wave * 1e-9 / 8 * 180 / np.pi * 3600 * 1000 / pixel box = box_size // 2 # spot fitting xx, yy = np.meshgrid(np.arange(2 * box), np.arange(2 * box)) # loop over images img_centers = np.zeros((nwave, 2)) failed_centers = np.zeros(nwave, dtype=np.bool) for idx, (cwave, img) in enumerate(zip(wave, cube)): logger.info(' ==> wave {0:2d}/{1:2d} ({2:4.0f} nm)'.format( idx + 1, nwave, cwave)) # remove any NaN img = np.nan_to_num(cube[idx]) # optional high-pass filter if high_pass: img = img - ndimage.median_filter(img, 15, mode='mirror') cube[idx] = img # center guess cy, cx = np.unravel_index(np.argmax(img), img.shape) # check if we are really too close to the edge dim = img.shape lf = exclude_fraction hf = 1 - exclude_fraction if (cx <= lf*dim[-1]) or (cx >= hf*dim[-1]) or \ (cy <= lf*dim[0]) or (cy >= hf*dim[0]): nimg = img.copy() nimg[:, :int(lf * dim[-1])] = 0 nimg[:, int(hf * dim[-1]):] = 0 nimg[:int(lf * dim[0]), :] = 0 nimg[int(hf * dim[0]):, :] = 0 cy, cx = np.unravel_index(np.argmax(nimg), img.shape) # sub-image sub = img[cy - box:cy + box, cx - box:cx + box] # fit peak with Gaussian + constant imax = np.unravel_index(np.argmax(sub), sub.shape) g_init = models.Gaussian2D(amplitude=sub.max(), x_mean=imax[1], y_mean=imax[0], x_stddev=loD[idx], y_stddev=loD[idx]) + \ models.Const2D(amplitude=sub.min()) fitter = fitting.LevMarLSQFitter() par = fitter(g_init, xx, yy, sub) cx_final = cx - box + par[0].x_mean cy_final = cy - box + par[0].y_mean img_centers[idx, 0] = cx_final img_centers[idx, 1] = cy_final # look for outliers and replace by a linear fit to all good ones # Ticket #81 ibad = [] if nwave > 2: c_med = np.median(img_centers, axis=0) c_std = np.std(img_centers, axis=0) bad = np.any(np.logical_or(img_centers < (c_med - 3 * c_std), img_centers > (c_med + 3 * c_std)), axis=1) ibad = np.where(bad)[0] igood = np.where(np.logical_not(bad))[0] if len(ibad) != 0: logger.info( f' ==> found {len(ibad)} outliers. Will replace them with a linear fit.' ) idx = np.arange(nwave) # x lin = np.polyfit(idx[igood], img_centers[igood, 0], 1) pol = np.poly1d(lin) img_centers[ibad, 0] = pol(idx[ibad]) # y lin = np.polyfit(idx[igood], img_centers[igood, 1], 1) pol = np.poly1d(lin) img_centers[ibad, 1] = pol(idx[ibad]) # # Generate summary plot # # multi-page PDF to save result if save_path is not None: pdf = PdfPages(save_path) for idx, (cwave, img) in enumerate(zip(wave, cube)): cx_final = img_centers[idx, 0] cy_final = img_centers[idx, 1] failed = (idx in ibad) if failed: mcolor = 'r' bcolor = 'r' else: mcolor = 'b' bcolor = 'w' plt.figure('PSF center - imaging', figsize=(8.3, 8)) plt.clf() plt.subplot(111) plt.imshow(img / np.nanmax(img), aspect='equal', norm=colors.LogNorm(vmin=1e-6, vmax=1), interpolation='nearest', cmap=global_cmap) plt.plot([cx_final], [cy_final], marker='D', color=mcolor) plt.gca().add_patch( patches.Rectangle((cx - box, cy - box), 2 * box, 2 * box, ec=bcolor, fc='none')) if failed: plt.text(cx, cy + box, 'Fit failed', color='r', weight='bold', fontsize='x-small', ha='center', va='bottom') plt.title(r'Image #{0} - {1:.0f} nm'.format(idx + 1, cwave)) ext = 1000 / pixel plt.xlim(cx_final - ext, cx_final + ext) plt.xlabel('x position [pix]') plt.ylim(cy_final - ext, cy_final + ext) plt.ylabel('y position [pix]') plt.subplots_adjust(left=0.1, right=0.98, bottom=0.1, top=0.95) pdf.savefig() pdf.close() return img_centers