def test_apertures_exact(): """Test area as measured by exact aperture modes on array of ones""" theta = np.random.uniform(-np.pi/2., np.pi/2., naper) ratio = np.random.uniform(0.2, 1.0, naper) r = 3. for dt in SUPPORTED_IMAGE_DTYPES: data = np.ones(data_shape, dtype=dt) for r in [0.5, 1., 3.]: flux, fluxerr, flag = sep.sum_circle(data, x, y, r, subpix=0) assert_allclose(flux, np.pi*r**2) rout = r*1.1 flux, fluxerr, flag = sep.sum_circann(data, x, y, r, rout, subpix=0) assert_allclose(flux, np.pi*(rout**2 - r**2)) flux, fluxerr, flag = sep.sum_ellipse(data, x, y, 1., ratio, theta, r=r, subpix=0) assert_allclose(flux, np.pi*ratio*r**2) rout = r*1.1 flux, fluxerr, flag = sep.sum_ellipann(data, x, y, 1., ratio, theta, r, rout, subpix=0) assert_allclose(flux, np.pi*ratio*(rout**2 - r**2))
def test_apertures_all(): """Test that aperture subpixel sampling works""" data = np.random.rand(*data_shape) r = 3. rtol = 1.e-8 for subpix in [0, 1, 5]: flux_ref, fluxerr_ref, flag_ref = sep.sum_circle(data, x, y, r, subpix=subpix) flux, fluxerr, flag = sep.sum_circann(data, x, y, 0., r, subpix=subpix) assert_allclose(flux, flux_ref, rtol=rtol) flux, fluxerr, flag = sep.sum_ellipse(data, x, y, r, r, 0., subpix=subpix) assert_allclose(flux, flux_ref, rtol=rtol) flux, fluxerr, flag = sep.sum_ellipse(data, x, y, 1., 1., 0., r=r, subpix=subpix) assert_allclose(flux, flux_ref, rtol=rtol)
def auto_fit(image_data): an_width = 3 temp_image_data = image_data internal_image_data = temp_image_data.byteswap(True).newbyteorder() bkg = sep.Background(internal_image_data) thresh = 1.5 * bkg.globalback objects = sep.extract(internal_image_data, thresh) center_x = internal_image_data.shape[0]/2.0 center_y = internal_image_data.shape[1]/2.0 center = [center_x, center_y] smallest_dFoc = 1000000000 radii = 21 count = 0 for i, j in zip(objects['x'], objects['y']): pos = [i, j] dFoc = math.sqrt(((pos[0]-center[0])**2) + ((pos[1]-center[1])**2)) if abs(dFoc) < smallest_dFoc: smallest_dFoc = dFoc minx = objects['xmin'][count] miny = objects['ymin'][count] maxx = objects['xmax'][count] maxy = objects['ymax'][count] radii = abs(math.sqrt(((maxx-minx)**2)+((maxy-miny)**2))) else: pass count += 1 i = 0 while i < 25: theta = 0 area = 0 while theta <= 2*pi: area += (((i+.1)**2)/2) - ((i**2)/2) theta += .001 flux, fluxerr, flag = sep.sum_circann(internal_image_data, internal_image_data.shape[0]/2, internal_image_data.shape[1]/2, i, i+an_width) metric = (flux - bkg.globalback)/bkg.globalrms metric /= area if i > 1: if metric < smallest_metric: smallest_metric = metric annulus = [i, i+an_width] else: smallest_metric = metric annulus = [i, i+an_width] i += 1 i = 1 an = [math.ceil(x*6) for x in annulus] if radii > an[0]: radii = an[0] - 0.5 if radii > 30: radii = 30 else: pass return {'Ap': radii, 'InAn': an[0], 'OutAn': an[1]}
def sextract_magnitudes( data, aperture_radius, buffer_radius, background_radius, bkg=None, objects=None, sigma=3.0, ): aperture_area = np.pi * (aperture_radius**2) buffer_area = np.pi * (buffer_radius**2) background_area = (np.pi * (background_radius**2)) - buffer_area if bkg is None: data, bkg = sextract_bkg(data) if objects is None: objects = sextract_objects(data, sigma, bkg) aperture_list, _, _ = sep.sum_circle(data, objects["x"], objects["y"], aperture_radius, err=bkg.globalrms, gain=1.0) background_list, _, _ = sep.sum_circann( data, objects["x"], objects["y"], buffer_radius, background_radius, err=bkg.globalrms, gain=1.0, ) background_densities = [ annulus_sum / background_area for annulus_sum in background_list ] fluxes = np.array([ aperture_list[i] - (aperture_area * background_densities[i]) for i in range(len(aperture_list)) ]) return fluxes
def test_apertures_all(): """Test that aperture subpixel sampling works""" data = np.random.rand(*data_shape) r = 3. rtol=1.e-8 for subpix in [0, 1, 5]: flux_ref, fluxerr_ref, flag_ref = sep.sum_circle(data, x, y, r, subpix=subpix) flux, fluxerr, flag = sep.sum_circann(data, x, y, 0., r, subpix=subpix) assert_allclose(flux, flux_ref, rtol=rtol) flux, fluxerr, flag = sep.sum_ellipse(data, x, y, r, r, 0., subpix=subpix) assert_allclose(flux, flux_ref, rtol=rtol) flux, fluxerr, flag = sep.sum_ellipse(data, x, y, 1., 1., 0., r=r, subpix=subpix) assert_allclose(flux, flux_ref, rtol=rtol)
minx = objects['xmin'][count] miny = objects['ymin'][count] maxx = objects['xmax'][count] maxy = objects['ymax'][count] radii = abs(math.sqrt(((maxx-minx)**2)+((maxy-miny)**2))) else: pass count += 1 i = 0 while i < 25: theta = 0 area = 0 while theta <= 2*pi: area += (((i+.1)**2)/2) - ((i**2)/2) theta += .001 flux, fluxerr, flag = sep.sum_circann(image_data, image_data.shape[0]/2, image_data.shape[1]/2, i, i+an_width) metric = (flux - bkg.globalback)/bkg.globalrms metric /= area #print flux, metric if i > 1: if metric < smallest_metric: smallest_metric = metric annulus = [i, i+an_width] else: smallest_metric = metric annulus = [i, i+an_width] i += 1 i = 1 an = [math.ceil(x*6) for x in annulus] if radii > an[0]: radii = an[0] - 0.5
def extract_obj(img, b=30, f=5, sigma=5, pixel_scale=0.168, minarea=5, deblend_nthresh=32, deblend_cont=0.005, clean_param=1.0, sky_subtract=False, show_fig=True, verbose=True, flux_auto=True, flux_aper=None): '''Extract objects for a given image, using `sep`. This is from `slug`. Parameters: ---------- img: 2-D numpy array b: float, size of box f: float, size of convolving kernel sigma: float, detection threshold pixel_scale: float Returns: ------- objects: astropy Table, containing the positions, shapes and other properties of extracted objects. segmap: 2-D numpy array, segmentation map ''' # Subtract a mean sky value to achieve better object detection b = 30 # Box size f = 5 # Filter width bkg = sep.Background(img, bw=b, bh=b, fw=f, fh=f) data_sub = img - bkg.back() sigma = sigma if sky_subtract: input_data = data_sub else: input_data = img objects, segmap = sep.extract(input_data, sigma, err=bkg.globalrms, segmentation_map=True, filter_type='matched', deblend_nthresh=deblend_nthresh, deblend_cont=deblend_cont, clean=True, clean_param=clean_param, minarea=minarea) if verbose: print("# Detect %d objects" % len(objects)) objects = Table(objects) objects.add_column(Column(data=np.arange(len(objects)) + 1, name='index')) # Maximum flux, defined as flux within six 'a' in radius. objects.add_column( Column(data=sep.sum_circle(input_data, objects['x'], objects['y'], 6. * objects['a'])[0], name='flux_max')) # Add FWHM estimated from 'a' and 'b'. # This is suggested here: https://github.com/kbarbary/sep/issues/34 objects.add_column( Column(data=2 * np.sqrt(np.log(2) * (objects['a']**2 + objects['b']**2)), name='fwhm_custom')) # Use Kron radius to calculate FLUX_AUTO in SourceExtractor. # Here PHOT_PARAMETER = 2.5, 3.5 if flux_auto: kronrad, krflag = sep.kron_radius(input_data, objects['x'], objects['y'], objects['a'], objects['b'], objects['theta'], 6.0) flux, fluxerr, flag = sep.sum_circle(input_data, objects['x'], objects['y'], 2.5 * (kronrad), subpix=1) flag |= krflag # combine flags into 'flag' r_min = 1.75 # minimum diameter = 3.5 use_circle = kronrad * np.sqrt(objects['a'] * objects['b']) < r_min cflux, cfluxerr, cflag = sep.sum_circle(input_data, objects['x'][use_circle], objects['y'][use_circle], r_min, subpix=1) flux[use_circle] = cflux fluxerr[use_circle] = cfluxerr flag[use_circle] = cflag objects.add_column(Column(data=flux, name='flux_auto')) objects.add_column(Column(data=kronrad, name='kron_rad')) if flux_aper is not None: objects.add_column( Column(data=sep.sum_circle(input_data, objects['x'], objects['y'], flux_aper[0])[0], name='flux_aper_1')) objects.add_column( Column(data=sep.sum_circle(input_data, objects['x'], objects['y'], flux_aper[1])[0], name='flux_aper_2')) objects.add_column( Column(data=sep.sum_circann(input_data, objects['x'], objects['y'], flux_aper[0], flux_aper[1])[0], name='flux_ann')) ''' objects.add_column(Column(data=sep.sum_circle(input_data, objects['x'], objects['y'], flux_aper[0] * objects['a'])[0], name='flux_aper_1')) objects.add_column(Column(data=sep.sum_circle(input_data, objects['x'], objects['y'], flux_aper[1] * objects['a'])[0], name='flux_aper_2')) objects.add_column(Column(data=sep.sum_circann(input_data, objects['x'], objects['y'], flux_aper[0] * objects['a'], flux_aper[1] * objects['a'])[0], name='flux_ann')) ''' # plot background-subtracted image if show_fig: fig, ax = plt.subplots(1, 2, figsize=(12, 6)) ax[0] = display_single(data_sub, ax=ax[0], scale_bar=False, pixel_scale=pixel_scale) from matplotlib.patches import Ellipse # plot an ellipse for each object for obj in objects: e = Ellipse(xy=(obj['x'], obj['y']), width=8 * obj['a'], height=8 * obj['b'], angle=obj['theta'] * 180. / np.pi) e.set_facecolor('none') e.set_edgecolor('red') ax[0].add_artist(e) ax[1] = display_single(segmap, scale='linear', cmap=SEG_CMAP, ax=ax[1]) return objects, segmap
def compute_photometry(self, extname, extnum, r=3, r_in=14, r_out=16): """ Parameters ---------- extname extnum r r_in r_out Returns ------- """ self.phot_catalog[f"{extname}{extnum}"] = \ self.source_catalog[f"{extname}{extnum}"]['id', 'x', 'y'] LOG.info('Computing local background within a circular annulus...') area_annulus = np.pi * (r_out**2 - r_in**2) bkg_sum, bkg_sum_err, flag = sep.sum_circann( self.data[f"{extname}{extnum}"], x=self.phot_catalog[f"{extname}{extnum}"]['x'], y=self.phot_catalog[f"{extname}{extnum}"]['y'], rin=14, rout=16) self.phot_catalog[f"{extname}{extnum}"][ 'bkg_per_pix'] = bkg_sum / area_annulus LOG.info('Computing aperture sum within a 3 pixel radius...') # Note we use the non-background subtracted one ap_sum, apsumerr, flag = sep.sum_circle( self.data[f"{extname}{extnum}"], self.phot_catalog[f"{extname}{extnum}"]['x'], self.phot_catalog[f"{extname}{extnum}"]['y'], r=3) area_ap = np.pi * r**2 ap_sum_bkg_sub = \ ap_sum - \ area_ap * self.phot_catalog[f"{extname}{extnum}"]['bkg_per_pix'] # Very simplistic error computation ap_sum_err_final = np.sqrt( ap_sum + area_ap * self.phot_catalog[f"{extname}{extnum}"]['bkg_per_pix']) # Record the aperture sum self.phot_catalog[f"{extname}{extnum}"][f'sum_{r:.0f}pix'] = \ ap_sum_bkg_sub # Record the error in the aperture sum self.phot_catalog[f"{extname}{extnum}"][f'sum_{r:.0f}pix_err'] = \ ap_sum_err_final # Record the magnitude self.phot_catalog[f"{extname}{extnum}"][f'mag_{r:.0f}pix'] = \ -2.5 * np.log10(ap_sum_bkg_sub) nan_filter = np.isnan( self.phot_catalog[f"{extname}{extnum}"][f"mag_{r:.0f}pix"]) # Record the error in the magnitude self.phot_catalog[f"{extname}{extnum}"][f'mag_{r:.0f}pix_err'] = \ 1.0857 * ap_sum_err_final / ap_sum_bkg_sub # Conver the X/Y coords to RA/DEC sky_coords = self.convert_to_sky( self.phot_catalog[f"{extname}{extnum}"], self.data[f"{extname}{extnum}_wcs"]) self.phot_catalog[f"{extname}{extnum}"]['ra'] = sky_coords.ra self.phot_catalog[f"{extname}{extnum}"]['dec'] = sky_coords.dec # Filter out all rows where any value is NaN self.phot_catalog[f"{extname}{extnum}"] = \ self.phot_catalog[f"{extname}{extnum}"][~nan_filter]
def auto_fit(image_data, xpos, ypos): """ Refit algorithm :param image_data: fits image data 2D array {Numpy 2D array} xpos: centered position of the regions on the x axis {integer} ypos: centered position of the region on the y axis {integer} ############################################################## :return: Dictionary of values { radii [Ap] -- Aperture radius {float} an[0] [InAn] -- Inner annulus radius {float} an[1] [OutAn] -- Outer annulus radius {float} radii_disagree [disagree] -- The difference between the directly returned radius from SEP and the radius from SEP flux sums {float} } """ an_width = 18 # defines annulus width | TODO Make this a free parameter in the auto fit function temp_image_data = image_data # creates non mutable version of array internally internal_image_data = temp_image_data.byteswap(True).newbyteorder() # SEP requires that data be byte swaped bkg = sep.Background(internal_image_data) thresh = 1.5 * bkg.globalback objects = sep.extract(internal_image_data, thresh) center_x = internal_image_data.shape[0]/2.0 center_y = internal_image_data.shape[1]/2.0 center = [center_x, center_y] smallest_dFoc = 1000000000 # big distance so it can only go down radii = 21 radii_sep = 21 sdev = np.std(internal_image_data) count = 0 # Finds radius of center target from the min and max x and y pixel locations of that target returned from SEP for i, j in zip(objects['x'], objects['y']): pos = [i, j] dFoc = math.sqrt(((pos[0]-center[0])**2) + ((pos[1]-center[1])**2)) if abs(dFoc) < smallest_dFoc: smallest_dFoc = dFoc minx = objects['xmin'][count] miny = objects['ymin'][count] maxx = objects['xmax'][count] maxy = objects['ymax'][count] radii_sep = abs(math.sqrt(((maxx-minx)**2)+((maxy-miny)**2))) else: pass count += 1 i = 1 # Finds optimal inner and outer annulus radii based off SEP flux sums while i < 90: area = (2*np.pi*((i+an_width)**2))-(2*np.pi*(i**2)) flux, fluxerr, flag = sep.sum_circann(internal_image_data, prev_x, prev_y, i, i+an_width) metric = ((flux/area) - bkg.globalback) if i > 1: if metric < smallest_metric: smallest_metric = metric annulus = [i, i+an_width] else: smallest_metric = metric annulus = [i, i+an_width] i += 1 i = 1 # Finds radius of center target using SEP flux sums while i < 90: area = 2*np.pi*(i**2) flux, fluxerr, flag = sep.sum_circle(internal_image_data, prev_x, prev_y, i) flux = sum(flux) metric = flux/area if metric > sdev + bkg.globalback: radii = i i += 1 # used more pixels an = [math.ceil(x) for x in annulus] # sanity checks for size if radii > an[0]: radii = an[0] - 0.5 if radii > 30: radii = 30 else: pass if an[0] > an[1]: an[0] = an[1] - 0.1 * an[1] radii_disagree = abs(radii-radii_sep) return {'Ap': radii, 'InAn': an[0], 'OutAn': an[1], 'disagree': radii_disagree}
def process_file(filename, favor2=None, verbose=False, replace=False, dbname=None, dbhost=None, photodir='photometry'): #### Some parameters aper = 2.0 bkgann = None order = 4 bg_order = 4 color_order = 2 sn = 5 if not posixpath.exists(filename): return None # Rough but fast checking of whether the file is already processed if not replace and posixpath.exists(photodir + '/' + filename.split('/')[-2] + '/' + posixpath.splitext(posixpath.split(filename)[-1])[0] + '.cat'): return #### Preparation header = fits.getheader(filename, -1) if header['TYPE'] not in ['survey', 'imaging', 'widefield', 'Swift', 'Fermi', 'test']: return channel = header.get('CHANNEL ID') fname = header.get('FILTER', 'unknown') time = parse_time(header['TIME']) shutter = header.get('SHUTTER', -1) if fname not in ['Clear']: return if fname == 'Clear': effective_fname = 'V' else: effective_fname = fname night = get_night(time) dirname = '%s/%s' % (photodir, night) basename = posixpath.splitext(posixpath.split(filename)[-1])[0] basename = dirname + '/' + basename catname = basename + '.cat' if not replace and posixpath.exists(catname): return if verbose: print(filename, channel, night, fname, effective_fname) image = fits.getdata(filename, -1).astype(np.double) if favor2 is None: favor2 = Favor2(dbname=options.db, dbhost=options.dbhost) #### Basic calibration darkname = favor2.find_image('masterdark', header=header, debug=False) flatname = favor2.find_image('masterflat', header=header, debug=False) if darkname: dark = fits.getdata(darkname) else: dark = None if flatname: flat = fits.getdata(flatname) else: flat = None if dark is None or flat is None: survey.save_objects(catname, None) return image,header = calibrate.calibrate(image, header, dark=dark) # Check whether the calibration failed if 'SATURATE' not in header: print('Calibration failed for', filename) survey.save_objects(catname, None) return #### Basic masking mask = image > 0.9*header['SATURATE'] fmask = ~np.isfinite(flat) | (flat < 0.5) dmask = dark > 10.0*mad_std(dark) + np.nanmedian(dark) if header.get('BLEMISHCORRECTION', 1): # We have to mask blemished pixels blemish = fits.getdata('calibrations/blemish_shutter_%d_channel_%d.fits' % (header['SHUTTER'], header['CHANNEL ID'])) if verbose: print(100*np.sum(blemish>0)/blemish.shape[0]/blemish.shape[1], '% pixels blemished') dmask |= blemish > 0 image[~fmask] *= np.median(flat[~fmask])/flat[~fmask] #### WCS wcs = WCS(header) pixscale = np.hypot(wcs.pixel_scale_matrix[0,0], wcs.pixel_scale_matrix[0,1]) gain = 0.67 if header.get('SHUTTER') == 0 else 1.9 #### Background mask mask_bg = np.zeros_like(mask) mask_segm = np.zeros_like(mask) # bg2 = sep.Background(image, mask=mask|mask_bg, bw=64, bh=64) # for _ in xrange(3): # bg1 = sep.Background(image, mask=mask|mask_bg, bw=256, bh=256) # ibg = bg2.back() - bg1.back() # tmp = np.abs(ibg - np.median(ibg)) > 5.0*mad_std(ibg) # mask_bg |= survey.dilate(tmp, np.ones([50, 50])) # mask_bg = survey.dilate(tmp, np.ones([50, 50])) # Large objects?.. bg = sep.Background(image, mask=mask|dmask|fmask|mask_bg|mask_segm, bw=128, bh=128) image1 = image - bg.back() obj0,segm = sep.extract(image1, err=bg.rms(), thresh=10, minarea=10, mask=mask|dmask|fmask|mask_bg, filter_kernel=None, clean=False, segmentation_map=True) mask_segm = np.isin(segm, [_+1 for _,npix in enumerate(obj0['npix']) if npix > 500]) mask_segm = survey.dilate(mask_segm, np.ones([20, 20])) if np.sum(mask_bg|mask_segm|mask|fmask|dmask)/mask_bg.shape[0]/mask_bg.shape[1] > 0.4: print(100*np.sum(mask_bg|mask_segm|mask|fmask|dmask)/mask_bg.shape[0]/mask_bg.shape[1], '% of image masked, skipping', filename) survey.save_objects(catname, None) return elif verbose: print(100*np.sum(mask_bg|mask_segm|mask|fmask|dmask)/mask_bg.shape[0]/mask_bg.shape[1], '% of image masked') # Frame footprint at +10 pixels from the edge ra,dec = wcs.all_pix2world([10, 10, image.shape[1]-10, image.shape[1]-10], [10, image.shape[0]-10, image.shape[0]-10, 10], 0) footprint = "(" + ",".join(["(%g,%g)" % (_,__) for _,__ in zip(ra, dec)]) + ")" #### Catalogue ra0,dec0,sr0 = survey.get_frame_center(header=header) cat = favor2.get_stars(ra0, dec0, sr0, catalog='gaia', extra=['g<14', 'q3c_poly_query(ra, dec, \'%s\'::polygon)' % footprint], limit=1000000) if verbose: print(len(cat['ra']), 'star positions from Gaia down to g=%.1f mag' % np.max(cat['g'])) ## Detection of blended and not really needed stars in the catalogue h = htm.HTM(10) m = h.match(cat['ra'], cat['dec'], cat['ra'], cat['dec'], 2.0*aper*pixscale, maxmatch=0) m = [_[m[2]>1e-5] for _ in m] blended = np.zeros_like(cat['ra'], dtype=np.bool) notneeded = np.zeros_like(cat['ra'], dtype=np.bool) for i1,i2,dist in zip(*m): if dist*3600 > 0.5*aper*pixscale: if cat['g'][i1] - cat['g'][i2] < 3: blended[i1] = True blended[i2] = True else: # i1 is fainter by more than 3 mag notneeded[i1] = True if dist*3600 < 0.5*aper*pixscale: if cat['g'][i1] > cat['g'][i2]: notneeded[i1] = True cat,blended = [_[~notneeded] for _ in cat,blended] #### Background subtraction bg = sep.Background(image, mask=mask|dmask|fmask|mask_bg|mask_segm, bw=128, bh=128) # bg = sep.Background(image, mask=mask|dmask|fmask|mask_bg|mask_segm, bw=32, bh=32) image1 = image - bg.back() #### Detection of all objects on the frame obj0,segm = sep.extract(image1, err=bg.rms(), thresh=2, minarea=3, mask=mask|dmask|fmask|mask_bg|mask_segm, filter_kernel=None, clean=False, segmentation_map=True) obj0 = obj0[(obj0['x'] > 10) & (obj0['y'] > 10) & (obj0['x'] < image.shape[1]-10) & (obj0['y'] < image.shape[0]-10)] obj0 = obj0[obj0['flag'] <= 1] # We keep only normal and blended oblects fields = ['ra', 'dec', 'fluxerr', 'mag', 'magerr', 'flags', 'cat'] obj0 = np.lib.recfunctions.append_fields(obj0, fields, [np.zeros_like(obj0['x'], dtype=np.int if _ in ['flags', 'cat'] else np.double) for _ in fields], usemask=False) obj0['ra'],obj0['dec'] = wcs.all_pix2world(obj0['x'], obj0['y'], 0) obj0['flags'] = obj0['flag'] if verbose: print(len(obj0['x']), 'objects detected on the frame') ## Filter out objects not coincident with catalogue positions h = htm.HTM(10) m = h.match(obj0['ra'], obj0['dec'], cat['ra'], cat['dec'], aper*pixscale) nidx = np.isin(np.arange(len(obj0['ra'])), m[0], invert=True) obj0 = obj0[nidx] if verbose: print(len(obj0['x']), 'are outside catalogue apertures') # Catalogue stars xc,yc = wcs.all_world2pix(cat['ra'], cat['dec'], 0) obj = {'x':xc, 'y':yc, 'ra':cat['ra'], 'dec':cat['dec']} obj['flags'] = np.zeros_like(xc, dtype=np.int) obj['flags'][blended] |= FLAG_BLENDED obj['cat'] = np.ones_like(xc, dtype=np.int) for _ in ['mag', 'magerr', 'flux', 'fluxerr']: obj[_] = np.zeros_like(xc) # Merge detected objects for _ in ['x', 'y', 'ra', 'dec', 'flags', 'mag', 'magerr', 'flux', 'fluxerr', 'cat']: obj[_] = np.concatenate((obj[_], obj0[_])) if verbose: print(len(obj['x']), 'objects for photometry') # Simple aperture photometry obj['flux'],obj['fluxerr'],flag = sep.sum_circle(image1, obj['x'], obj['y'], aper, err=bg.rms(), gain=gain, mask=mask|dmask|fmask|mask_bg|mask_segm, bkgann=bkgann) obj['flags'] |= flag # Normalize flags obj['flags'][obj['flags'] & sep.APER_TRUNC] |= FLAG_TRUNCATED obj['flags'][obj['flags'] & sep.APER_ALLMASKED] |= FLAG_MASKED obj['flags'] &= FLAG_NORMAL | FLAG_BLENDED | FLAG_TRUNCATED | FLAG_MASKED | FLAG_NO_BACKGROUND | FLAG_BAD_CALIBRATION area,_,_ = sep.sum_circle(np.ones_like(image1), obj['x'], obj['y'], aper, err=bg.rms(), gain=gain, mask=mask|dmask|fmask|mask_bg|mask_segm, bkgann=bkgann) # Simple local background estimation bgflux,bgfluxerr,bgflag = sep.sum_circann(image1, obj['x'], obj['y'], 10, 15, err=bg.rms(), gain=gain, mask=mask|dmask|fmask|mask_bg|mask_segm|(segm>0)) bgarea,_,_ = sep.sum_circann(np.ones_like(image1), obj['x'], obj['y'], 10, 15, err=bg.rms(), gain=gain, mask=mask|dmask|fmask|mask_bg|mask_segm|(segm>0)) bgidx = np.isfinite(bgarea) & np.isfinite(area) bgidx[bgidx] &= (bgarea[bgidx] > 10) & (area[bgidx] > 1) obj['flux'][bgidx] -= bgflux[bgidx]*area[bgidx]/bgarea[bgidx] obj['flags'][~bgidx] |= FLAG_NO_BACKGROUND # No local background obj['deltabgflux'] = np.zeros_like(obj['x']) obj['deltabgflux'][bgidx] = bgflux[bgidx]*area[bgidx]/bgarea[bgidx] fidx = np.isfinite(obj['flux']) & np.isfinite(obj['fluxerr']) fidx[fidx] &= (obj['flux'][fidx] > 0) obj['mag'][fidx] = -2.5*np.log10(obj['flux'][fidx]) obj['magerr'][fidx] = 2.5/np.log(10)*obj['fluxerr'][fidx]/obj['flux'][fidx] fidx[fidx] &= (obj['magerr'][fidx] > 0) fidx[fidx] &= 1/obj['magerr'][fidx] > sn for _ in obj.keys(): if hasattr(obj[_], '__len__'): obj[_] = obj[_][fidx] obj['aper'] = aper if verbose: print(len(obj['x']), 'objects with S/N >', sn) if len(obj['x']) < 1000: print('Only', len(obj['x']), 'objects on the frame, skipping', filename) survey.save_objects(catname, None) return obj['fwhm'] = 2.0*sep.flux_radius(image1, obj['x'], obj['y'], 2.0*aper*np.ones_like(obj['x']), 0.5, mask=mask|dmask|fmask|mask_bg|mask_segm)[0] #### Check FWHM of all objects and select only 'good' ones idx = obj['flags'] == 0 idx &= obj['magerr'] < 1/20 fwhm0 = survey.fit_2d(obj['x'][idx], obj['y'][idx], obj['fwhm'][idx], obj['x'], obj['y'], weights=1/obj['magerr'][idx]) fwhm_idx = np.abs(obj['fwhm'] - fwhm0 - np.median((obj['fwhm'] - fwhm0)[idx])) < 3.0*mad_std((obj['fwhm'] - fwhm0)[idx]) obj['flags'][~fwhm_idx] |= FLAG_BLENDED #### Catalogue matching idx = obj['flags'] m = htm.HTM(10).match(obj['ra'], obj['dec'], cat['ra'], cat['dec'], 1e-5) fidx = np.in1d(np.arange(len(cat['ra'])), m[1]) # Stars that got successfully measured and not blended cidx = (cat['good'] == 1) & (cat['var'] == 0) cidx &= np.isfinite(cat['B']) & np.isfinite(cat['V']) # & np.isfinite(cat['lum']) cidx[cidx] &= ((cat['B'] - cat['V'])[cidx] > -0.5) & ((cat['B'] - cat['V'])[cidx] < 2.0) # cidx[cidx] &= (cat['lum'][cidx] > 0.3) & (cat['lum'][cidx] < 30) if np.sum(cidx & fidx & (cat['multi_70'] == 0)) > 2000: cidx &= (cat['multi_70'] == 0) obj['cat_multi'] = 70 elif np.sum(cidx & fidx & (cat['multi_45'] == 0)) > 1000: cidx &= (cat['multi_45'] == 0) obj['cat_multi'] = 45 else: cidx &= (cat['multi_30'] == 0) obj['cat_multi'] = 30 if verbose: print(np.sum(obj['flags'] == 0), 'objects without flags') print('Amount of good stars:', np.sum(cidx & fidx & (cat['multi_70'] == 0)), np.sum(cidx & fidx & (cat['multi_45'] == 0)), np.sum(cidx & fidx & (cat['multi_30'] == 0))) print('Using %d arcsec avoidance radius' % obj['cat_multi']) # We match with very small SR to only account for manually placed apertures if verbose: print('Trying full fit:', len(obj['x']), 'objects,', np.sum(cidx), 'stars') match = Match(width=image.shape[1], height=image.shape[0]) prev_ngoodstars = len(obj['x']) for iter in range(10): if not match.match(obj=obj, cat=cat[cidx], sr=1e-5, filter_name='V', order=order, bg_order=bg_order, color_order=color_order, verbose=False) or match.ngoodstars < 500: if verbose: print(match.ngoodstars, 'good matches, matching failed for', filename) survey.save_objects(catname, None) return if verbose: print(match.ngoodstars, 'good matches, std =', match.std) if match.ngoodstars == prev_ngoodstars: if verbose: print('Converged on iteration', iter) break prev_ngoodstars = match.ngoodstars # Match good objects with stars oidx = obj['flags'] == 0 oidx1,cidx1,dist1 = htm.HTM(10).match(obj['ra'][oidx], obj['dec'][oidx], cat['ra'][cidx], cat['dec'][cidx], 1e-5) x = obj['x'][oidx][oidx1] y = obj['y'][oidx][oidx1] cbv = match.color_term[oidx][oidx1] cbv2 = match.color_term2[oidx][oidx1] cbv3 = match.color_term3[oidx][oidx1] bv = (cat['B'] - cat['V'])[cidx][cidx1] cmag = cat[match.cat_filter_name][cidx][cidx1] mag = match.mag[oidx][oidx1] + bv*cbv + bv**2*cbv2 + bv**3*cbv3 magerr = np.hypot(obj['magerr'][oidx][oidx1], 0.02) dmag = mag-cmag ndmag = ((mag-cmag)/magerr) idx = cmag < match.mag_limit[oidx][oidx1] x,y,cbv,cbv2,cbv3,bv,cmag,mag,magerr,dmag,ndmag = [_[idx] for _ in [x,y,cbv,cbv2,cbv3,bv,cmag,mag,magerr,dmag,ndmag]] # Match all objects with good objects xy = np.array([x,y]).T xy0 = np.array([obj['x'], obj['y']]).T kd = cKDTree(xy) dist,m = kd.query(xy0, 101) dist = dist[:,1:] m = m[:,1:] vchi2 = mad_std(ndmag[m]**2, axis=1) # Mark regions of too sparse or too noisy matches as bad obj['flags'][vchi2 > 5] |= FLAG_BAD_CALIBRATION # obj['flags'][vchi2 > np.median(vchi2) + 5.0*mad_std(vchi2)] |= FLAG_BAD_CALIBRATION obj['flags'][dist[:,10] > np.median(dist[:,10]) + 10.0*mad_std(dist[:,10])] |= FLAG_BAD_CALIBRATION match.good_idx = (obj['flags'] & FLAG_BAD_CALIBRATION) == 0 if verbose: print(np.sum(match.good_idx), 'of', len(match.good_idx), 'stars are good') #### Store objects to file try: os.makedirs(dirname) except: pass obj['mag_limit'] = match.mag_limit obj['color_term'] = match.color_term obj['color_term2'] = match.color_term2 obj['color_term3'] = match.color_term3 obj['filename'] = filename obj['night'] = night obj['channel'] = channel obj['filter'] = fname obj['cat_filter'] = match.cat_filter_name obj['time'] = time obj['mag_id'] = match.mag_id obj['good_idx'] = match.good_idx obj['calib_mag'] = match.mag obj['calib_magerr'] = match.magerr obj['std'] = match.std obj['nstars'] = match.ngoodstars survey.save_objects(catname, obj, header=header)
def aperture_photometry(img: Union[ndarray, MaskedArray], sources: ndarray, background: Optional[Union[ndarray, MaskedArray]] = None, background_rms: Optional[Union[ndarray, MaskedArray]] = None, texp: float = 1, gain: Union[float, ndarray, MaskedArray] = 1, sat_level: float = 63000, a: Optional[float] = None, b: Optional[float] = None, theta: Optional[float] = 0, a_in: Optional[float] = None, a_out: Optional[float] = None, b_out: Optional[float] = None, theta_out: Optional[float] = None, k: float = 0, k_in: Optional[float] = None, k_out: Optional[float] = None, radius: float = 6, fix_aper: bool = False, fix_ell: bool = True, fix_rot: bool = True, apcorr_tol: float = 0.0001) -> ndarray: """ Do automatic or fixed aperture photometry :param img: input 2D image array :param sources: record array of sources extracted with :func:`skylib.extraction.extract_sources`; should contain at least "x" and "y" columns :param background: optional sky background map; if omitted, extract background from the annulus around the aperture, see `a_in` below :param background_rms: optional sky background RMS map; if omitted, calculate RMS over the annulus around the aperture :param texp: exposure time in seconds :param gain: electrons to data units conversion factor; used to estimate photometric errors; for variable-gain images (e.g. mosaics), must be an array of the same shape as the input data :param sat_level: saturation level in ADUs; used to select only non-saturated stars for adaptive aperture photometry and aperture correction :param a: fixed aperture radius or semi-major axis in pixels; default: use automatic photometry with a = a_iso*k, where a_iso is the isophotal semi-major axis :param b: semi-minor axis in pixels when using a fixed aperture; default: same as `a` :param theta: rotation angle of semi-major axis in degrees CCW when using a fixed aperture and `b` != `a`; default: 0 :param a_in: inner annulus radius or semi-major axis in pixels; used to estimate the background if `background` or `background_rms` are not provided, ignored otherwise; default: `a`*`k_in` :param a_out: outer annulus radius or semi-major axis in pixels; default: `a`*`k_out` :param b_out: outer annulus semi-minor axis in pixels; default: `b`*`k_out` :param theta_out: annulus orientation in degrees CCW; default: same as `theta` :param k: automatic aperture radius in units of isophotal radius; 0 means find the optimal radius based on SNR; default: 0 :param k_in: inner annulus radius in units of aperture radius (fixed aperture, i.e. `a` is provided) or isophotal radius (adaptive aperture); default: 1.5*`k` or 3.75 if `k` is undefined and `a` = None :param k_out: outer annulus radius in units of aperture radius (fixed aperture) or isophotal radius (adaptive aperture); default: 2*`k` or 5 if `k` is undefined and `a` = None :param radius: isophotal analysis radius in pixels used to compute automatic aperture if ellipse parameters (a,b,theta) are missing :param fix_aper: use the same aperture radius for all sources when doing automatic photometry; calculated as flux-weighted median of aperture sizes based on isophotal parameters :param fix_ell: use the same major to minor aperture axis ratio for all sources during automatic photometry; calculated as flux-weighted median of all ellipticities :param fix_rot: use the same aperture position angle for all sources during automatic photometry; calculated as flux-weighted median of all orientations :param apcorr_tol: growth curve stopping tolerance for aperture correction; 0 = disable aperture correction :return: record array containing the input sources, with the following fields added or updated: "flux", "flux_err", "mag", "mag_err", "aper_a", "aper_b", "aper_theta", "aper_a_in", "aper_a_out", "aper_b_out", "aper_theta_out", "aper_area", "background_area", "background", "background_rms", "phot_flag" """ if not len(sources): return array([]) img = sep_compatible(img) texp = float(texp) if isscalar(gain): gain = float(gain) k = float(k) if k <= 0.1: k = 0 # temporary fix for k = 0 not being allowed in AgA if k_in: k_in = float(k_in) if k_out: k_out = float(k_out) x, y = sources['x'] - 1, sources['y'] - 1 area_img = ones(img.shape, dtype=int32) if isinstance(img, MaskedArray): mask = img.mask img = img.data else: mask = None have_background = background is not None and background_rms is not None if have_background: background = sep_compatible(background) if isinstance(background, MaskedArray): if mask is None: mask = background.mask else: mask |= background.mask background = background.data background_rms = sep_compatible(background_rms) if isinstance(background_rms, MaskedArray): if mask is None: mask = background_rms.mask else: mask |= background_rms.mask background_rms = background_rms.data # Will need this to fill the newly added source table columns z = zeros(len(sources), float) fixed_aper = bool(a) if fixed_aper: # Use the same fixed aperture and annulus parameters for all sources a = float(a) if b: b = float(b) else: b = a if theta: theta = float(theta % 180)*pi/180 if theta > pi/2: theta -= pi else: theta = 0 if not have_background: if theta_out: theta_out = float(theta_out % 180)*pi/180 if theta_out > pi/2: theta_out -= pi elif theta_out != 0: theta_out = theta if a_in: a_in = float(a_in) else: a_in = a*k_in if k_in else a*1.5*k if k else a*3.75 if a_out: a_out = float(a_out) else: a_out = a*k_out if k_out else a*2*k if k else a*5 if b_out: b_out = float(b_out) else: b_out = a_out*b/a else: # Use automatic apertures derived from ellipse axes; will need image # with background subtracted if background is None: # Estimate background on the fly tmp_back, tmp_rms = estimate_background(img, size=64) else: tmp_back, tmp_rms = background, background_rms img_back = img - tmp_back for name in ['a', 'b', 'theta', 'flux']: if name not in sources.dtype.names: sources = append_fields(sources, name, z, usemask=False) a, b, theta = sources['a'], sources['b'], sources['theta'] flux = sources['flux'] bad = (a <= 0) | (b <= 0) | (flux <= 0) if bad.any(): # Do isophotal analysis to compute ellipse parameters if missing yy, xx = indices(img.shape) for i in bad.nonzero()[0]: ap = (xx - sources[i]['x'])**2 + (yy - sources[i]['y'])**2 <= \ radius**2 if ap.any(): yi, xi = ap.nonzero() ap_data = img_back[ap].astype(float) f = ap_data.sum() if f > 0: cx = (xi*ap_data).sum()/f cy = (yi*ap_data).sum()/f x2 = (xi**2*ap_data).sum()/f - cx**2 y2 = (yi**2*ap_data).sum()/f - cy**2 xy = (xi*yi*ap_data).sum()/f - cx*cy else: cx, cy = xi.mean(), yi.mean() x2 = (xi**2).mean() - cx**2 y2 = (yi**2).mean() - cy**2 xy = (xi*yi).mean() - cx*cy if x2 == y2: thetai = 0 else: thetai = arctan(2*xy/(x2 - y2))/2 if y2 > x2: thetai += pi/2 m1 = (x2 + y2)/2 m2 = sqrt(max((x2 - y2)**2/4 + xy**2, 0)) ai = max(1/12, sqrt(max(m1 + m2, 0))) bi = max(1/12, sqrt(max(m1 - m2, 0))) if ai/bi > 2: # Prevent too elongated apertures usually occurring for # faint objects bi = ai else: # Cannot obtain a,b,theta from isophotal analysis, assume # circular aperture ai, bi, thetai, f = radius, radius, 0, 0 a[i] = sources[i]['a'] = ai b[i] = sources[i]['b'] = bi theta[i] = sources[i]['theta'] = thetai flux[i] = sources[i]['flux'] = f bad = (a < b).nonzero() a[bad], b[bad] = b[bad], a[bad] theta[bad] += pi/2 theta %= pi theta[theta > pi/2] -= pi elongation = a/b # Obtain the optimal aperture radius from the brightest non-saturated # source if not k: for i in argsort(flux)[::-1]: if sep.sum_ellipse( img >= sat_level, [x[i]], [y[i]], a[i], b[i], theta[i], 1, subpix=0)[0][0]: # Saturated source continue try: # noinspection PyTypeChecker res = minimize( calc_flux_err, [a[i]*1.6], (img_back, x[i], y[i], elongation[i], theta[i], tmp_rms, mask, gain), bounds=[(1, None)], tol=1e-5) except ValueError: continue if not res.success: continue k = res.x[0]/a[i] break if not k: raise ValueError( 'Not enough data for automatic aperture factor; use ' 'explicit aperture factor') # Calculate weighted median of aperture sizes, elongations, and/or # orientations if requested r = sqrt(a*b)*k if r.size > 1 and any([fix_aper, fix_ell, fix_rot]): flux[flux < 0] = 0 if not flux.any(): raise ValueError( 'Not enough data for weighted median in fixed-aperture ' 'automatic photometry; use static fixed-aperture or fully ' 'adaptive automatic photometry instead') if fix_aper: r = weighted_median(r, flux) if fix_ell: elongation = weighted_median(elongation, flux) if fix_rot: theta = weighted_median(theta, flux, period=pi) if theta > pi/2: theta -= pi # Calculate the final aperture and annulus sizes sqrt_el = sqrt(elongation) a, b = r*sqrt_el, r/sqrt_el if not have_background: if not k_in: k_in = 1.5*k if not k_out: k_out = 2*k a_in = a*(k_in/k) a_out, b_out = a*(k_out/k), b*(k_out/k) theta_out = theta # Calculate mean and RMS of background; to get the pure sigma, set error # to 1 and don't pass the gain if have_background: if fixed_aper and a == b: bk_area = sep.sum_circle(area_img, x, y, a, mask=mask, subpix=0)[0] bk_mean, bk_sigma = sep.sum_circle( background, x, y, a, err=1, mask=mask, subpix=0)[:2] else: bk_area = sep.sum_ellipse( area_img, x, y, a, b, theta, 1, mask=mask, subpix=0)[0] bk_mean, bk_sigma = sep.sum_ellipse( background, x, y, a, b, theta, 1, err=1, mask=mask, subpix=0)[:2] error = background_rms elif fixed_aper and a_out == b_out: bk_area = sep.sum_circann( area_img, x, y, a_in, a_out, mask=mask, subpix=0)[0] bk_mean, bk_sigma = sep.sum_circann( img, x, y, a_in, a_out, err=1, mask=mask, subpix=0)[:2] error = bk_sigma else: bk_area = sep.sum_ellipann( area_img, x, y, a_out, b_out, theta_out, a_in/a_out, 1, mask=mask, subpix=0)[0] bk_mean, bk_sigma = sep.sum_ellipann( img, x, y, a_out, b_out, theta_out, a_in/a_out, 1, err=1, mask=mask, subpix=0)[:2] error = bk_sigma if have_background: area = bk_area elif fixed_aper and a == b: area = sep.sum_circle(area_img, x, y, a, mask=mask, subpix=0)[0] else: area = sep.sum_ellipse( area_img, x, y, a, b, theta, 1, mask=mask, subpix=0)[0] if fixed_aper and a == b: # Fixed circular aperture if ndim(error) == 1: # Separate scalar error for each source flux, flux_err = empty([2, len(sources)], dtype=float) flags = empty(len(sources), dtype=int) for i, (_x, _y, _err) in enumerate(zip(x, y, error)): flux[i], flux_err[i], flags[i] = sep.sum_circle( img, [_x], [_y], a, err=_err, mask=mask, gain=gain, subpix=0) else: flux, flux_err, flags = sep.sum_circle( img, x, y, a, err=error, mask=mask, gain=gain, subpix=0) else: # Variable or elliptic aperture if ndim(error) == 1: # Separate scalar error for each source flux, flux_err = empty([2, len(sources)], dtype=float) flags = empty(len(sources), dtype=int) if isscalar(a): a = full_like(x, a) if isscalar(b): b = full_like(x, b) if isscalar(theta): theta = full_like(x, theta) for i, (_x, _y, _err, _a, _b, _theta) in enumerate(zip( x, y, error, a, b, theta)): flux[i], flux_err[i], flags[i] = sep.sum_ellipse( img, [_x], [_y], _a, _b, _theta, 1, err=_err, mask=mask, gain=gain, subpix=0) else: flux, flux_err, flags = sep.sum_ellipse( img, x, y, a, b, theta, 1, err=error, mask=mask, gain=gain, subpix=0) # Convert background sum to mean and subtract background from fluxes if have_background: # Background area equals aperture area flux -= bk_mean bk_mean = bk_mean/area else: # Background area equals annulus area bk_mean = bk_mean/bk_area flux -= bk_mean*area # Convert ADUs to electrons flux *= gain flux_err *= gain bk_mean *= gain bk_sigma *= gain # Calculate aperture correction for all aperture sizes from the brightest # source aper_corr = {} if apcorr_tol > 0: for i in argsort(flux)[::-1]: xi, yi = x[i], y[i] if isscalar(a): ai = a else: ai = a[i] if isscalar(b): bi = b else: bi = b[i] if isscalar(theta): thetai = theta else: thetai = theta[i] if ndim(error) == 1: err = error[i] else: err = error if ai == bi: nsat = sep.sum_circle( img >= sat_level, [xi], [yi], ai, subpix=0)[0][0] else: nsat = sep.sum_ellipse( img >= sat_level, [xi], [yi], ai, bi, thetai, 1, subpix=0)[0][0] if nsat: # Saturated source continue # Obtain total flux by increasing aperture size until it grows # either more than before (i.e. a nearby source in the aperture) or # less than the threshold (i.e. the growth curve reached saturation) f0 = f_prev = flux[i] dap = 0 f_tot = df_prev = None while True: dap += 0.1 if ai == bi: f, f_err, fl = sep.sum_circle( img, [xi], [yi], ai + dap, err=err, mask=mask, gain=gain, subpix=0) area_i = sep.sum_circle( area_img, [xi], [yi], ai + dap, mask=mask, subpix=0)[0][0] else: f, f_err, fl = sep.sum_ellipse( img, [xi], [yi], ai + dap, bi*(1 + dap/ai), thetai, 1, err=err, mask=mask, gain=gain, subpix=0) area_i = sep.sum_ellipse( area_img, [xi], [yi], ai + dap, bi + dap, thetai, 1, mask=mask, subpix=0)[0][0] f, fl = f[0], fl[0] if fl: break f = (f - bk_mean[i]*area_i)*gain if f <= 0: break df = f/f_prev if df_prev is not None and df > df_prev: # Increasing growth, nearby source hit f_tot = f_prev break if df < 1 + apcorr_tol: # Growth stopped to within the tolerance f_tot = f break f_prev, df_prev = f, df if f_tot is None: continue # Calculate fluxes for the chosen source for all unique aperture # sizes used for other sources fluxes_for_ap = {ai: f0} if not isscalar(a): for aj in set(a) - {ai}: bj = aj*bi/ai if aj == bj: f, fl = sep.sum_circle( img, [xi], [yi], aj, err=err, mask=mask, gain=gain, subpix=0)[::2] area_j = sep.sum_circle( area_img, [xi], [yi], aj, mask=mask, subpix=0)[0][0] else: f, fl = sep.sum_ellipse( img, [xi], [yi], aj, bj, thetai, 1, err=err, mask=mask, gain=gain, subpix=0)[::2] area_j = sep.sum_ellipse( area_img, [xi], [yi], aj, bj, thetai, 1, mask=mask, subpix=0)[0][0] f, fl = f[0], fl[0] f = (f - bk_mean[i]*area_j)*gain if fl or f <= 0: continue fluxes_for_ap[aj] = f # Calculate aperture corrections for aj, f in fluxes_for_ap.items(): if f < f_tot: aper_corr[aj] = -2.5*log10(f_tot/f) break if 'flux' in sources.dtype.names: sources['flux'] = flux else: sources = append_fields(sources, 'flux', flux, usemask=False) if 'flux_err' in sources.dtype.names: sources['flux_err'] = flux_err else: sources = append_fields(sources, 'flux_err', flux_err, usemask=False) if 'flag' in sources.dtype.names: sources['flag'] |= flags else: sources = append_fields(sources, 'flag', flags, usemask=False) for name in ['mag', 'mag_err', 'aper_a', 'aper_b', 'aper_theta', 'aper_a_in', 'aper_a_out', 'aper_b_out', 'aper_theta_out', 'aper_area', 'background_area', 'background', 'background_rms']: if name not in sources.dtype.names: sources = append_fields(sources, name, z, usemask=False) good = (flux > 0).nonzero() if len(good[0]): sources['mag'][good] = -2.5*log10(flux[good]/texp) sources['mag_err'][good] = 2.5*log10(1 + flux_err[good]/flux[good]) sources['aper_a'] = a sources['aper_b'] = b sources['aper_theta'] = theta if not have_background: sources['aper_a_in'] = a_in sources['aper_a_out'] = a_out sources['aper_b_out'] = b_out sources['aper_theta_out'] = theta_out sources['aper_area'] = area sources['background_area'] = bk_area sources['background'] = bk_mean sources['background_rms'] = bk_sigma # Apply aperture correction for i in good[0]: sources['mag'][i] += aper_corr.get(sources['aper_a'][i], 0) return sources