def test_flat_correct_min_value(ccd_data): size = ccd_data.shape[0] # create the flat data = 2 * np.random.normal(loc=1.0, scale=0.05, size=(size, size)) flat = CCDData(data, meta=fits.header.Header(), unit=ccd_data.unit) flat_orig_data = flat.data.copy() min_value = 2.1 # should replace some, but not all, values flat_corrected_data = flat_correct(ccd_data, flat, min_value=min_value) flat_with_min = flat.copy() flat_with_min.data[flat_with_min.data < min_value] = min_value # Check that the flat was normalized. The asserts below, which look a # little odd, are correctly testing that # flat_corrected_data = ccd_data / (flat_with_min / mean(flat_with_min)) np.testing.assert_almost_equal( (flat_corrected_data.data * flat_with_min.data).mean(), (ccd_data.data * flat_with_min.data.mean()).mean() ) np.testing.assert_allclose(ccd_data.data / flat_corrected_data.data, flat_with_min.data / flat_with_min.data.mean()) # Test that flat is not modified. assert (flat_orig_data == flat.data).all() assert flat_orig_data is not flat.data
def make_master_flat(table, mbias, sigma=3, iters=5, min_value=0, colname_file='file', colname_exptime='EXPTIME', colname_nx='NAXIS1', colname_ny='NAXIS2', dtype=np.float32, output=''): ''' Make master flat from the given flat table and master bias. Dark subtraction is not implemented yet. ''' if not isinstance(mbias, CCDData): mbias = CCDData(data=mbias, unit='adu') else: mbias = mbias.copy() Nccd = len(table) exptimes = check_exptime(table=table, colname_file=colname_file, colname_nx=colname_nx, colname_ny=colname_ny, colname_exptime=colname_exptime) flat_orig = initialize_2Dcombiner(table=table, colname_nx=colname_nx, colname_ny=colname_ny, dtype=dtype) for i in range(Nccd): flat_orig[:, :, i] = (fits.getdata(table[colname_file][i]) / exptimes[i]) print_info(Nccd=Nccd, min_value=min_value, sigma=sigma, iters=iters) mflat = clip_median_combine(flat_orig, sigma=sigma, iters=iters, min_value=min_value, axis=-1) print('and bias subtraction ...') mflat -= mbias mflat = mflat.astype(dtype) if output != '': print('Saving to {:s}'.format(output)) hdu = fits.PrimaryHDU(data=mflat.data) hdu.writeto(output, overwrite=True) return mflat.data
def make_master_dark(table, mbias, sigma=3, iters=5, min_value=0, colname_file='file', colname_exptime='EXPTIME', colname_nx='NAXIS1', colname_ny='NAXIS2', dtype=np.float32, output=''): '''Make dark frame from the given dark table ''' if not isinstance(mbias, CCDData): mbias = CCDData(data=mbias, unit='adu') else: mbias = mbias.copy() Nccd = len(table) exptimes = check_exptime(table=table, colname_file=colname_file, colname_nx=colname_nx, colname_ny=colname_ny, colname_exptime=colname_exptime) dark_orig = initialize_2Dcombiner(table=table, colname_nx=colname_nx, colname_ny=colname_ny, dtype=dtype) for i in range(Nccd): dark_orig[:, :, i] = ((fits.getdata(table[colname_file][i]) - mbias) / exptimes[i]) print_info(Nccd=Nccd, min_value=min_value, sigma=sigma, iters=iters) mdark = clip_median_combine(dark_orig, sigma=sigma, iters=iters, min_value=min_value, axis=-1) mdark = mdark.astype(dtype) if output != '': print('Saving to {:s}'.format(output)) hdu = fits.PrimaryHDU(data=mdark.data) hdu.writeto(output, overwrite=True) return mdark.data
class image(object): ''' A single image object. Functions --------- * Read from fits file use CCDData. * get_size : Get the image size. * plot : Plot the image. * sigma_clipped_stats : Calculate the basic statistics of the image. * set_data : Load from numpy array. * set_mask : Set image mask. * set_pixel_scales : Set the pixel scales along two axes. * set_zero_point : Set magnitude zero point. ''' def __init__(self, filename=None, hdu=0, unit=None, zero_point=None, pixel_scales=None, wcs_rotation=None, mask=None, verbose=True): ''' Parameters ---------- filename (optional) : string FITS file name of the image. hdu : int (default: 0) The number of extension to load from the FITS file. unit (optional) : string Unit of the image flux for CCDData. zero_point (optional) : float Magnitude zero point. pixel_scales (optional) : tuple Pixel scales along the first and second directions, units: arcsec. wcs_rotation (optional) : float WCS rotation, east of north, units: radian. mask (optional) : 2D bool array The image mask. verbose : bool (default: True) Print out auxiliary data. ''' if filename is None: self.data = None else: self.data = CCDData.read(filename, hdu=hdu, unit=unit, mask=mask) if self.data.wcs and (pixel_scales is None): pixel_scales = proj_plane_pixel_scales( self.data.wcs) * u.degree.to('arcsec') self.zero_point = zero_point if pixel_scales is None: self.pixel_scales = None else: self.pixel_scales = (pixel_scales[0] * u.arcsec, pixel_scales[1] * u.arcsec) if self.data.wcs and (wcs_rotation is None): self.wcs_rotation = get_wcs_rotation(self.data.wcs) elif wcs_rotation is not None: self.wcs_rotation = wcs_rotation * u.radian else: self.wcs_rotation = None self.sources_catalog = None self.sigma_image = None self.sources_skycord = None self.ss_data = None self.PSF = None def get_size(self, units='pixel'): ''' Get the size of the image. Parameters ---------- units : string Units of the size (pixel or angular units). Returns ------- x, y : float Size along X and Y axes. ''' nrow, ncol = self.data.shape if units == 'pixel': x = ncol y = nrow else: x = ncol * self.pixel_scales[0].to(units).value y = nrow * self.pixel_scales[1].to(units).value return (x, y) def get_size(self, units='pixel'): ''' Get the size of the image. Parameters ---------- units : string Units of the size (pixel or angular units). Returns ------- x, y : float Size along X and Y axes. ''' nrow, ncol = self.data.shape if units == 'pixel': x = ncol y = nrow else: x = ncol * self.pixel_scales[0].to(units).value y = nrow * self.pixel_scales[1].to(units).value return (x, y) def get_data_info(self): ''' Data information to generate model image. Returns ------- d : dict shape : (ny, nx) Image array shape. pixel_scale : (pixelscale_x, pixelscale_y), default units: arcsec Pixel scales. wcs_rotation : angle, default units: radian WCS rotation, east of north. ''' d = dict(shape=self.data.shape, pixel_scale=self.pixel_scale, wcs_rotation=self.wcs_rotation) return d def sigma_clipped_stats(self, **kwargs): ''' Run astropy.stats.sigma_clipped_stats to get the basic statistics of the image. Parameters ---------- All of the parameters go to astropy.stats.sigma_clipped_stats(). Returns ------- mean, median, stddev : float The mean, median, and standard deviation of the sigma-clipped data. ''' return sigma_clipped_stats(self.data.data, mask=self.data.mask, **kwargs) def plot(self, stretch='asinh', units='arcsec', vmin=None, vmax=None, a=None, ax=None, plain=False, **kwargs): ''' Plot an image. Parameters ---------- stretch : string (default: 'asinh') Choice of stretch: asinh, linear, sqrt, log. units : string (default: 'arcsec') Units of pixel scale. vmin (optional) : float Minimal value of imshow. vmax (optional) : float Maximal value of imshow. a (optional) : float Scale factor of some stretch function. ax (optional) : matplotlib.Axis Axis to plot the image. plain : bool (default: False) If False, tune the image. **kwargs : Additional parameters goes into plt.imshow() Returns ------- ax : matplotlib.Axis Axis to plot the image. ''' assert self.data is not None, 'Set data first!' ax = plot_image(self.data, self.pixel_scales, stretch=stretch, units=units, vmin=vmin, vmax=vmax, a=a, ax=ax, plain=plain, **kwargs) if plain is False: ax.set_xlabel(r'$\Delta X$ ({0})'.format(units), fontsize=24) ax.set_ylabel(r'$\Delta Y$ ({0})'.format(units), fontsize=24) return ax def plot_direction(self, ax, xy=(0, 0), len_E=None, len_N=None, color='k', fontsize=20, linewidth=2, frac_len=0.1, units='arcsec', backextend=0.05): ''' Plot the direction arrow. Only applied to plots using WCS. Parameters ---------- ax : Axis Axis to plot the direction. xy : (x, y) Coordinate of the origin of the arrows. length : float Length of the arrows, units: pixel. units: string (default: arcsec) Units of xy. ''' xlim = ax.get_xlim() len_total = np.abs(xlim[1] - xlim[0]) pixelscale = self.pixel_scales[0].to('degree').value if len_E is None: len_E = len_total * frac_len / pixelscale if len_N is None: len_N = len_total * frac_len / pixelscale wcs = self.data.wcs header = wcs.to_header() d_ra = len_E * pixelscale d_dec = len_N * pixelscale ra = [header['CRVAL1'], header['CRVAL1'] + d_ra, header['CRVAL1']] dec = [header['CRVAL2'], header['CRVAL2'], header['CRVAL2'] + d_dec] ra_pix, dec_pix = wcs.all_world2pix(ra, dec, 1) d_arrow1 = [ra_pix[1] - ra_pix[0], dec_pix[1] - dec_pix[0]] d_arrow2 = [ra_pix[2] - ra_pix[0], dec_pix[2] - dec_pix[0]] l_arrow1 = np.sqrt(d_arrow1[0]**2 + d_arrow1[1]**2) l_arrow2 = np.sqrt(d_arrow2[0]**2 + d_arrow2[1]**2) d_arrow1 = np.array(d_arrow1) / l_arrow1 * len_E * pixelscale d_arrow2 = np.array(d_arrow2) / l_arrow2 * len_N * pixelscale def sign_2_align(sign): ''' Determine the alignment of the text. ''' if sign[0] < 0: ha = 'right' else: ha = 'left' if sign[1] < 0: va = 'top' else: va = 'bottom' return ha, va ha1, va1 = sign_2_align(np.sign(d_arrow1)) ha2, va2 = sign_2_align(np.sign(d_arrow2)) xy_e = (xy[0] - d_arrow1[0] * backextend, xy[1] - d_arrow1[1] * backextend) ax.annotate('E', xy=xy_e, xycoords='data', fontsize=fontsize, xytext=(d_arrow1[0] + xy[0], d_arrow1[1] + xy[1]), color=color, arrowprops=dict(color=color, arrowstyle="<-", lw=linewidth), ha=ha1, va=va1) xy_n = (xy[0] - d_arrow2[0] * backextend, xy[1] - d_arrow2[1] * backextend) ax.annotate('N', xy=xy_n, xycoords='data', fontsize=fontsize, xytext=(d_arrow2[0] + xy[0], d_arrow2[1] + xy[1]), color=color, arrowprops=dict(color=color, arrowstyle="<-", lw=linewidth), ha=ha2, va=va2) def set_data(self, data, unit): ''' Parameters ---------- data : 2D array Image data. unit : string Unit for CCDData. ''' self.data = CCDData(data, unit=unit) def source_detection_individual(self, psfFWHM, nsigma=3.0, sc_key=''): ''' Parameters ---------- psfFWHM : float FWHM of the imaging point spread function nsigma : float source detection threshold ''' data = np.array(self.data.copy()) psfFWHMpix = psfFWHM / self.pixel_scales[0].value thresholder = detect_threshold(data, nsigma=nsigma) sigma = psfFWHMpix * gaussian_fwhm_to_sigma kernel = Gaussian2DKernel(sigma, x_size=5, y_size=5) kernel.normalize() segm = detect_sources(data, thresholder, npixels=5, filter_kernel=kernel) props = source_properties(data, segm) tab = Table(props.to_table()) self.sources_catalog = tab srcPstradec = self.data.wcs.all_pix2world(tab['xcentroid'], tab['ycentroid'], 1) sc = SkyCoord(srcPstradec[0], srcPstradec[1], unit='deg') sctab = Table([sc, np.arange(len(sc))], names=['sc', 'sloop_{0}'.format(sc_key)]) self.sources_skycord = sctab def make_mask(self, sources=None, magnification=3.): ''' make mask for the extension. Parameters ---------- sources : a to-be masked source table (can generate from photutils source detection) if None, will use its own source catalog magnification : expand factor to generate mask ''' mask = np.zeros_like(self.data, dtype=bool) mask[np.isnan(self.data)] = True mask[np.isinf(self.data)] = True if sources is None: sources = self.sources_catalog for loop in range(len(sources)): position = (sources['xcentroid'][loop], sources['ycentroid'][loop]) a = sources['semimajor_axis_sigma'][loop] b = sources['semiminor_axis_sigma'][loop] theta = sources['orientation'][loop] * 180. / np.pi mask = Maskellipse(mask, position, magnification * a, (1 - b / a), theta) self.data.mask = mask if self.ss_data is not None: self.ss_data.mask = mask def set_mask(self, mask): ''' Set mask for the extension. Parameters ---------- mask : 2D array The mask. ''' assert self.data.shape == mask.shape, 'Mask shape incorrect!' self.data.mask = mask if self.ss_data is not Nont: self.ss_data.mask = mask def set_pixel_scales(self, pixel_scales): ''' Parameters ---------- pixel_scales (optional) : tuple Pixel scales along the first and second directions, units: arcsec. ''' self.pixel_scales = (pixel_scales[0] * u.arcsec, pixel_scales[1] * u.arcsec) def set_zero_point(self, zp): ''' Set magnitude zero point. ''' self.zero_point = zp def sky_subtraction(self, order=3, filepath=None): ''' Do polynomial-fitting sky subtraction Parameters ---------- order (optional) : int order of the polynomial ''' data = np.array(self.data.copy()) maskplus = self.data.mask.copy() backR = polynomialfit(data, maskplus.astype(bool), order=order) background = backR['bkg'] self.ss_data = CCDData(data - background, unit=self.data.unit) self.ss_data.mask = maskplus if filepath is not None: hdu_temp = fits.PrimaryHDU(data - background) hdu_temp.writeto(filepath, overwrite=True) def read_ss_image(self, filepath): ''' read sky subtracted image from "filepath" ''' hdu = fits.open(filepath) self.ss_data = CCDData(hdu[0].data, unit=self.data.unit) self.ss_data.mask = self.data.mask.copy() def cal_sigma_image(self, filepath=None): ''' Construct sigma map following the same procedure as Galfit (quadruture sum of sigma at each pixel from source and sky background). Note ---------- 'GAIN' keyword must be available in the image header and ADU x GAIN = electron Parameters ---------- filepath: Whether and where to save sigma map ''' GAIN = self.data.header['CELL.GAIN'] if self.ss_data is None: raise ValueError(" Please do sky subtration first !!!") data = np.array(self.ss_data.copy()) mask = self.ss_data.mask.copy() bkgrms = np.nanstd(data[~mask.astype(bool)]) data[~mask.astype(bool)] = 0. sigmap = np.sqrt(data / GAIN + bkgrms**2) self.sigma_image = sigmap if filepath is not None: hdu_temp = fits.PrimaryHDU(sigmap) hdu_temp.writeto(filepath, overwrite=True) def read_sigmap(self, filepath): ''' read sigma image from "filepath" ''' hdu = fits.open(filepath) self.sigma_image = hdu[0].data def read_PSF(self, filepath): ''' read PSF image from "filepath" ''' hdu = fits.open(filepath) self.PSF = hdu[0].data
vmin, vmax = np.percentile(scidata.data, (15., 99.5)) ax1.imshow(scidata.data, vmin=vmin, vmax=vmax) ax1.plot(x, y, marker='o', mec='r', mfc='none', ls='none') norm = ImageNormalize(refdata_reproj, PercentileInterval(99.)) ax2.imshow(refdata_reproj, norm=norm) ax2.plot(x, y, marker='o', mec='r', mfc='none', ls='none') template_filename = os.path.join(workdir, 'template.fits') refdata_reproj[np.isnan(refdata_reproj)] = 0. refdata = CCDData(refdata_reproj, wcs=scidata.wcs, mask=1-refdata_foot, unit='adu') refdata.write(template_filename, overwrite=True) # # Make the PSF for the reference image refdata_unmasked = refdata.copy() refdata_unmasked.mask = np.zeros_like(refdata, bool) ref_psf, _ = make_psf(refdata_unmasked, catalog, show=show) # # Subtract the images and view the result output_filename = os.path.join(workdir, 'diff.fits') science = ImageClass(scidata.data, sci_psf.data, header=scidata.header, saturation=65565) reference = ImageClass(refdata.data, ref_psf.data, refdata.mask) difference = calculate_difference_image(science, reference, show=show) difference_zero_point = calculate_difference_image_zero_point(science, reference) normalized_difference = normalize_difference_image(difference, difference_zero_point, science, reference, 'i') save_difference_image_to_file(normalized_difference, science, 'i', output_filename) if show: vmin, vmax = np.percentile(difference, (15., 99.5))