Exemplo n.º 1
0
Arquivo: test.py Projeto: cmccully/sep
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))
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
        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]}
Exemplo n.º 4
0
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
Exemplo n.º 5
0
Arquivo: test.py Projeto: cmccully/sep
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)
Exemplo n.º 6
0
         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
Exemplo n.º 7
0
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]
Exemplo n.º 9
0
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}
Exemplo n.º 10
0
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)
Exemplo n.º 11
0
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