Example #1
0
def get_winpos(data, x, y, a, subsampling=5):
    """
    Get windowed position. These are more accurate positions calculated
    by SEP (SExtractor) created by iteratively recentering on the area
    contained by the half-flux radius.
    
    Parameters
    ----------
    data: `~numpy.array`
        Image data
    x: array-like
        X coordinates of the sources
    y: array-like
        Y coordinates of the sources
    subsampling: int, optional
        Number of subpixels for each image pixel
        (used to calculate the half flux radius).
        Default=5.
    """
    import sep
    r, flag = sep.flux_radius(data, x, y, 
        6.*a, 0.5, subpix=subsampling)
    sig = 2. / 2.35 * r
    xwin, ywin, flags = sep.winpos(data, x, y, sig)
    return xwin, ywin
Example #2
0
def refineCentroid(data, coords, sigma):
    """ Refines the centroid for each star for a set of test slices of the data cube """

    xInit = [pos[0] for pos in coords]
    yInit = [pos[1] for pos in coords]
    new_pos = np.array(sep.winpos(data, xInit, yInit, sigma, subpix=5))[0:2, :]
    x = new_pos[:][0].tolist()
    y = new_pos[:][1].tolist()
    return zip(x, y)
Example #3
0
def find_flux(tbdat_sub, objects, kronrad, kronflag):
	flux, fluxerr, flag = sep.sum_ellipse(tbdat_sub, objects['x'], objects['y'], objects['a'], objects['b'], objects['theta'], pho_auto_A = (2.5*kronrad), err = bkg.globalrms, subpix=1)
	flag |=kronflag #combines all flags
	r_min = 1.75 #minimum diameter = 3.5
	use_circle = kronrad * np.sqrt(a * b) < r_min
	cflux, cfluxerr, cflag = sep.sum_circle(tbdat_sub, objects['x'][use_circle], objects['y'][use_circle], r_min, subpix=1)
	flux[use_circle] = cflux
	fluxerr[use_circle] = cfluxerr
	flag[use_circle] = cflag
	r, rflag = sep.flux_radius(data, x, y, 6.0*objects['a'], rmax = 0.5, normflux = flux, subpix =5)
	sig = 2.0 / (2.35*r) # r from sep.flux_radius() above, with fluxfrac = 0.5
	xwin, ywin, wflag = sep.winpos(tbdat_sub, objects['x'], objects['y'], sig)
	return flux, fluxerr, flag, r, xwin, ywin
Example #4
0
def centroid_sources(data: Union[numpy.ndarray, numpy.ma.MaskedArray],
                     x: Union[float, numpy.ndarray],
                     y: Union[float, numpy.ndarray],
                     radius: Union[float, numpy.ndarray] = 5,
                     method: str = 'iraf') \
        -> Union[Tuple[float, float], Tuple[numpy.ndarray, numpy.ndarray]]:
    """
    Given the initial guess, obtain a more accurate source centroid position(s)
    using SExtractor, IRAF, or PSF fitting method

    :param data: 2D pixel data array
    :param x: initial guess for the source X position (1-based)
    :param y: initial guess for the source Y position (1-based)
    :param radius: centroiding radius, either an array of the same shape as `x`
        and `y` or a scalar if using the same radius for all sources
    :param method: "iraf" (default), "win" (windowed method, SExtractor),
        or "psf" (Gaussian PSF fitting)

    :return: (x, y) - a pair of centroid coordinates, same shape as input
    """
    if method == 'win':
        data = sep_compatible(data)
        if isinstance(data, numpy.ma.MaskedArray):
            mask = data.mask
            data = data.data
        else:
            mask = None
        xc, yc, flags = sep.winpos(data, x - 1, y - 1, radius, mask=mask)
        if numpy.ndim(flags):
            bad = flags.nonzero()
            xc[bad] = x[bad] - 1
            yc[bad] = y[bad] - 1
            return xc + 1, yc + 1
        if flags:
            return x, y
        return xc + 1, yc + 1

    x, y = tuple(
        zip(*[(centroid_psf if method == 'psf' else centroid_iraf
               )(data, x0, y0, r) for x0, y0, r in numpy.transpose([
                   numpy.atleast_1d(x),
                   numpy.atleast_1d(y),
                   numpy.full_like(numpy.atleast_1d(x), radius)
               ])]))
    if not numpy.ndim(x):
        x, y = x[0], y[0]
    return x, y
Example #5
0
def winpos(components, observation=None):
    """Calculate more accurate object centroids using ‘windowed’ algorithm.
    https://sep.readthedocs.io/en/v1.0.x/api/sep.winpos.html

    Parameters
    ----------
    components: a list of `scarlet.Component` or `scarlet.ComponentTree`
        Components to analyze

    Returns
    -------
        y, x: winpos in each channel
    """
    if not isinstance(components, list):
        components = [components]

    # Determine the centroid, averaged through channels
    _, y_cen, x_cen = centroid(components, observation=observation)

    blend = scarlet.Blend(components, observation)
    model = blend.get_model()
    mask = (observation.weights == 0)
    model = model * ~mask

    R50 = flux_radius(components, observation, frac=0.5)
    sig = 2. / 2.35 * R50  # R50 is half-light radius for each channel

    depth = model.shape[0]

    x_ = []
    y_ = []
    if depth > 1:
        for i in range(depth):
            xwin, ywin, flag = sep.winpos(model[i], x_cen, y_cen, sig[i])
            x_.append(xwin)
            y_.append(ywin)

    return np.array(y_), np.array(x_)
Example #6
0
    def do_stage(self, images):
        for i, image in enumerate(images):
            try:
                # Set the number of source pixels to be 5% of the total. This keeps us safe from
                # satellites and airplanes.
                sep.set_extract_pixstack(int(image.nx * image.ny * 0.05))

                data = image.data.copy()
                error = (np.abs(data) + image.readnoise ** 2.0) ** 0.5
                mask = image.bpm > 0

                # Fits can be backwards byte order, so fix that if need be and subtract
                # the background
                try:
                    bkg = sep.Background(data, mask=mask, bw=32, bh=32, fw=3, fh=3)
                except ValueError:
                    data = data.byteswap(True).newbyteorder()
                    bkg = sep.Background(data, mask=mask, bw=32, bh=32, fw=3, fh=3)
                bkg.subfrom(data)

                # Do an initial source detection
                # TODO: Add back in masking after we are sure SEP works
                sources = sep.extract(data, self.threshold, minarea=self.min_area,
                                      err=error, deblend_cont=0.005)

                # Convert the detections into a table
                sources = Table(sources)

                # Calculate the ellipticity
                sources['ellipticity'] = 1.0 - (sources['b'] / sources['a'])

                # Fix any value of theta that are invalid due to floating point rounding
                # -pi / 2 < theta < pi / 2
                sources['theta'][sources['theta'] > (np.pi / 2.0)] -= np.pi
                sources['theta'][sources['theta'] < (-np.pi / 2.0)] += np.pi

                # Calculate the kron radius
                kronrad, krflag = sep.kron_radius(data, sources['x'], sources['y'],
                                                  sources['a'], sources['b'],
                                                  sources['theta'], 6.0)
                sources['flag'] |= krflag
                sources['kronrad'] = kronrad

                # Calcuate the equivilent of flux_auto
                flux, fluxerr, flag = sep.sum_ellipse(data, sources['x'], sources['y'],
                                                      sources['a'], sources['b'],
                                                      np.pi / 2.0, 2.5 * kronrad,
                                                      subpix=1, err=error)
                sources['flux'] = flux
                sources['fluxerr'] = fluxerr
                sources['flag'] |= flag

                # Calculate the FWHMs of the stars:
                fwhm = 2.0 * (np.log(2) * (sources['a'] ** 2.0 + sources['b'] ** 2.0)) ** 0.5
                sources['fwhm'] = fwhm

                # Cut individual bright pixels. Often cosmic rays
                sources = sources[fwhm > 1.0]

                # Measure the flux profile
                flux_radii, flag = sep.flux_radius(data, sources['x'], sources['y'],
                                                   6.0 * sources['a'], [0.25, 0.5, 0.75],
                                                   normflux=sources['flux'], subpix=5)
                sources['flag'] |= flag
                sources['fluxrad25'] = flux_radii[:, 0]
                sources['fluxrad50'] = flux_radii[:, 1]
                sources['fluxrad75'] = flux_radii[:, 2]

                # Calculate the windowed positions
                sig = 2.0 / 2.35 * sources['fluxrad50']
                xwin, ywin, flag = sep.winpos(data, sources['x'], sources['y'], sig)
                sources['flag'] |= flag
                sources['xwin'] = xwin
                sources['ywin'] = ywin

                # Calculate the average background at each source
                bkgflux, fluxerr, flag = sep.sum_ellipse(bkg.back(), sources['x'], sources['y'],
                                                         sources['a'], sources['b'], np.pi / 2.0,
                                                         2.5 * sources['kronrad'], subpix=1)
                #masksum, fluxerr, flag = sep.sum_ellipse(mask, sources['x'], sources['y'],
                #                                         sources['a'], sources['b'], np.pi / 2.0,
                #                                         2.5 * kronrad, subpix=1)

                background_area = (2.5 * sources['kronrad']) ** 2.0 * sources['a'] * sources['b'] * np.pi # - masksum
                sources['background'] = bkgflux
                sources['background'][background_area > 0] /= background_area[background_area > 0]
                # Update the catalog to match fits convention instead of python array convention
                sources['x'] += 1.0
                sources['y'] += 1.0

                sources['xpeak'] += 1
                sources['ypeak'] += 1

                sources['xwin'] += 1.0
                sources['ywin'] += 1.0

                sources['theta'] = np.degrees(sources['theta'])

                image.catalog = sources['x', 'y', 'xwin', 'ywin', 'xpeak', 'ypeak',
                                        'flux', 'fluxerr', 'background', 'fwhm',
                                        'a', 'b', 'theta', 'kronrad', 'ellipticity',
                                        'fluxrad25', 'fluxrad50', 'fluxrad75',
                                        'x2', 'y2', 'xy', 'flag']

                # Add the units and description to the catalogs
                image.catalog['x'].unit = 'pixel'
                image.catalog['x'].description = 'X coordinate of the object'
                image.catalog['y'].unit = 'pixel'
                image.catalog['y'].description = 'Y coordinate of the object'
                image.catalog['xwin'].unit = 'pixel'
                image.catalog['xwin'].description = 'Windowed X coordinate of the object'
                image.catalog['ywin'].unit = 'pixel'
                image.catalog['ywin'].description = 'Windowed Y coordinate of the object'
                image.catalog['xpeak'].unit = 'pixel'
                image.catalog['xpeak'].description = 'X coordinate of the peak'
                image.catalog['ypeak'].unit = 'pixel'
                image.catalog['ypeak'].description = 'Windowed Y coordinate of the peak'
                image.catalog['flux'].unit = 'counts'
                image.catalog['flux'].description = 'Flux within a Kron-like elliptical aperture'
                image.catalog['fluxerr'].unit = 'counts'
                image.catalog['fluxerr'].description = 'Erronr on the flux within a Kron-like elliptical aperture'
                image.catalog['background'].unit = 'counts'
                image.catalog['background'].description = 'Average background value in the aperture'
                image.catalog['fwhm'].unit = 'pixel'
                image.catalog['fwhm'].description = 'FWHM of the object'
                image.catalog['a'].unit = 'pixel'
                image.catalog['a'].description = 'Semi-major axis of the object'
                image.catalog['b'].unit = 'pixel'
                image.catalog['b'].description = 'Semi-minor axis of the object'
                image.catalog['theta'].unit = 'degrees'
                image.catalog['theta'].description = 'Position angle of the object'
                image.catalog['kronrad'].unit = 'pixel'
                image.catalog['kronrad'].description = 'Kron radius used for extraction'
                image.catalog['ellipticity'].description = 'Ellipticity'
                image.catalog['fluxrad25'].unit = 'pixel'
                image.catalog['fluxrad25'].description = 'Radius containing 25% of the flux'
                image.catalog['fluxrad50'].unit = 'pixel'
                image.catalog['fluxrad50'].description = 'Radius containing 50% of the flux'
                image.catalog['fluxrad75'].unit = 'pixel'
                image.catalog['fluxrad75'].description = 'Radius containing 75% of the flux'
                image.catalog['x2'].unit = 'pixel^2'
                image.catalog['x2'].description = 'Variance on X coordinate of the object'
                image.catalog['y2'].unit = 'pixel^2'
                image.catalog['y2'].description = 'Variance on Y coordinate of the object'
                image.catalog['xy'].unit = 'pixel^2'
                image.catalog['xy'].description = 'XY covariance of the object'
                image.catalog['flag'].description = 'Bit mask combination of extraction and photometry flags'

                image.catalog.sort('flux')
                image.catalog.reverse()

                logging_tags = logs.image_config_to_tags(image, self.group_by_keywords)
                logs.add_tag(logging_tags, 'filename', os.path.basename(image.filename))

                # Save some background statistics in the header
                mean_background = stats.sigma_clipped_mean(bkg.back(), 5.0)
                image.header['L1MEAN'] = (mean_background,
                                          '[counts] Sigma clipped mean of frame background')
                logs.add_tag(logging_tags, 'L1MEAN', float(mean_background))

                median_background = np.median(bkg.back())
                image.header['L1MEDIAN'] = (median_background,
                                            '[counts] Median of frame background')
                logs.add_tag(logging_tags, 'L1MEDIAN', float(median_background))

                std_background = stats.robust_standard_deviation(bkg.back())
                image.header['L1SIGMA'] = (std_background,
                                           '[counts] Robust std dev of frame background')
                logs.add_tag(logging_tags, 'L1SIGMA', float(std_background))

                # Save some image statistics to the header
                good_objects = image.catalog['flag'] == 0

                seeing = np.median(image.catalog['fwhm'][good_objects]) * image.pixel_scale
                image.header['L1FWHM'] = (seeing, '[arcsec] Frame FWHM in arcsec')
                logs.add_tag(logging_tags, 'L1FWHM', float(seeing))

                mean_ellipticity = stats.sigma_clipped_mean(sources['ellipticity'][good_objects],
                                                            3.0)
                image.header['L1ELLIP'] = (mean_ellipticity, 'Mean image ellipticity (1-B/A)')
                logs.add_tag(logging_tags, 'L1ELLIP', float(mean_ellipticity))

                mean_position_angle = stats.sigma_clipped_mean(sources['theta'][good_objects], 3.0)
                image.header['L1ELLIPA'] = (mean_position_angle,
                                            '[deg] PA of mean image ellipticity')
                logs.add_tag(logging_tags, 'L1ELLIPA', float(mean_position_angle))

                self.logger.info('Extracted sources', extra=logging_tags)

            except Exception as e:
                logging_tags = logs.image_config_to_tags(image, self.group_by_keywords)
                logs.add_tag(logging_tags, 'filename', os.path.basename(image.filename))
                self.logger.error(e, extra=logging_tags)
        return images
Example #7
0
    def do_stage(self, image):
        try:
            # Set the number of source pixels to be 5% of the total. This keeps us safe from
            # satellites and airplanes.
            sep.set_extract_pixstack(int(image.nx * image.ny * 0.05))

            data = image.data.copy()
            error = (np.abs(data) + image.readnoise**2.0)**0.5
            mask = image.bpm > 0

            # Fits can be backwards byte order, so fix that if need be and subtract
            # the background
            try:
                bkg = sep.Background(data, mask=mask, bw=32, bh=32, fw=3, fh=3)
            except ValueError:
                data = data.byteswap(True).newbyteorder()
                bkg = sep.Background(data, mask=mask, bw=32, bh=32, fw=3, fh=3)
            bkg.subfrom(data)

            # Do an initial source detection
            # TODO: Add back in masking after we are sure SEP works
            sources = sep.extract(data,
                                  self.threshold,
                                  minarea=self.min_area,
                                  err=error,
                                  deblend_cont=0.005)

            # Convert the detections into a table
            sources = Table(sources)

            # We remove anything with a detection flag >= 8
            # This includes memory overflows and objects that are too close the edge
            sources = sources[sources['flag'] < 8]

            sources = array_utils.prune_nans_from_table(sources)

            # Calculate the ellipticity
            sources['ellipticity'] = 1.0 - (sources['b'] / sources['a'])

            # Fix any value of theta that are invalid due to floating point rounding
            # -pi / 2 < theta < pi / 2
            sources['theta'][sources['theta'] > (np.pi / 2.0)] -= np.pi
            sources['theta'][sources['theta'] < (-np.pi / 2.0)] += np.pi

            # Calculate the kron radius
            kronrad, krflag = sep.kron_radius(data, sources['x'], sources['y'],
                                              sources['a'], sources['b'],
                                              sources['theta'], 6.0)
            sources['flag'] |= krflag
            sources['kronrad'] = kronrad

            # Calcuate the equivilent of flux_auto
            flux, fluxerr, flag = sep.sum_ellipse(data,
                                                  sources['x'],
                                                  sources['y'],
                                                  sources['a'],
                                                  sources['b'],
                                                  np.pi / 2.0,
                                                  2.5 * kronrad,
                                                  subpix=1,
                                                  err=error)
            sources['flux'] = flux
            sources['fluxerr'] = fluxerr
            sources['flag'] |= flag

            # Do circular aperture photometry for diameters of 1" to 6"
            for diameter in [1, 2, 3, 4, 5, 6]:
                flux, fluxerr, flag = sep.sum_circle(data,
                                                     sources['x'],
                                                     sources['y'],
                                                     diameter / 2.0 /
                                                     image.pixel_scale,
                                                     gain=1.0,
                                                     err=error)
                sources['fluxaper{0}'.format(diameter)] = flux
                sources['fluxerr{0}'.format(diameter)] = fluxerr
                sources['flag'] |= flag

            # Calculate the FWHMs of the stars:
            fwhm = 2.0 * (np.log(2) *
                          (sources['a']**2.0 + sources['b']**2.0))**0.5
            sources['fwhm'] = fwhm

            # Cut individual bright pixels. Often cosmic rays
            sources = sources[fwhm > 1.0]

            # Measure the flux profile
            flux_radii, flag = sep.flux_radius(data,
                                               sources['x'],
                                               sources['y'],
                                               6.0 * sources['a'],
                                               [0.25, 0.5, 0.75],
                                               normflux=sources['flux'],
                                               subpix=5)
            sources['flag'] |= flag
            sources['fluxrad25'] = flux_radii[:, 0]
            sources['fluxrad50'] = flux_radii[:, 1]
            sources['fluxrad75'] = flux_radii[:, 2]

            # Calculate the windowed positions
            sig = 2.0 / 2.35 * sources['fluxrad50']
            xwin, ywin, flag = sep.winpos(data, sources['x'], sources['y'],
                                          sig)
            sources['flag'] |= flag
            sources['xwin'] = xwin
            sources['ywin'] = ywin

            # Calculate the average background at each source
            bkgflux, fluxerr, flag = sep.sum_ellipse(bkg.back(),
                                                     sources['x'],
                                                     sources['y'],
                                                     sources['a'],
                                                     sources['b'],
                                                     np.pi / 2.0,
                                                     2.5 * sources['kronrad'],
                                                     subpix=1)
            # masksum, fluxerr, flag = sep.sum_ellipse(mask, sources['x'], sources['y'],
            #                                         sources['a'], sources['b'], np.pi / 2.0,
            #                                         2.5 * kronrad, subpix=1)

            background_area = (
                2.5 * sources['kronrad']
            )**2.0 * sources['a'] * sources['b'] * np.pi  # - masksum
            sources['background'] = bkgflux
            sources['background'][background_area > 0] /= background_area[
                background_area > 0]
            # Update the catalog to match fits convention instead of python array convention
            sources['x'] += 1.0
            sources['y'] += 1.0

            sources['xpeak'] += 1
            sources['ypeak'] += 1

            sources['xwin'] += 1.0
            sources['ywin'] += 1.0

            sources['theta'] = np.degrees(sources['theta'])

            catalog = sources['x', 'y', 'xwin', 'ywin', 'xpeak', 'ypeak',
                              'flux', 'fluxerr', 'peak', 'fluxaper1',
                              'fluxerr1', 'fluxaper2', 'fluxerr2', 'fluxaper3',
                              'fluxerr3', 'fluxaper4', 'fluxerr4', 'fluxaper5',
                              'fluxerr5', 'fluxaper6', 'fluxerr6',
                              'background', 'fwhm', 'a', 'b', 'theta',
                              'kronrad', 'ellipticity', 'fluxrad25',
                              'fluxrad50', 'fluxrad75', 'x2', 'y2', 'xy',
                              'flag']

            # Add the units and description to the catalogs
            catalog['x'].unit = 'pixel'
            catalog['x'].description = 'X coordinate of the object'
            catalog['y'].unit = 'pixel'
            catalog['y'].description = 'Y coordinate of the object'
            catalog['xwin'].unit = 'pixel'
            catalog['xwin'].description = 'Windowed X coordinate of the object'
            catalog['ywin'].unit = 'pixel'
            catalog['ywin'].description = 'Windowed Y coordinate of the object'
            catalog['xpeak'].unit = 'pixel'
            catalog['xpeak'].description = 'X coordinate of the peak'
            catalog['ypeak'].unit = 'pixel'
            catalog['ypeak'].description = 'Windowed Y coordinate of the peak'
            catalog['flux'].unit = 'count'
            catalog[
                'flux'].description = 'Flux within a Kron-like elliptical aperture'
            catalog['fluxerr'].unit = 'count'
            catalog[
                'fluxerr'].description = 'Error on the flux within Kron aperture'
            catalog['peak'].unit = 'count'
            catalog['peak'].description = 'Peak flux (flux at xpeak, ypeak)'
            for diameter in [1, 2, 3, 4, 5, 6]:
                catalog['fluxaper{0}'.format(diameter)].unit = 'count'
                catalog['fluxaper{0}'.format(
                    diameter
                )].description = 'Flux from fixed circular aperture: {0}" diameter'.format(
                    diameter)
                catalog['fluxerr{0}'.format(diameter)].unit = 'count'
                catalog['fluxerr{0}'.format(
                    diameter
                )].description = 'Error on Flux from circular aperture: {0}"'.format(
                    diameter)

            catalog['background'].unit = 'count'
            catalog[
                'background'].description = 'Average background value in the aperture'
            catalog['fwhm'].unit = 'pixel'
            catalog['fwhm'].description = 'FWHM of the object'
            catalog['a'].unit = 'pixel'
            catalog['a'].description = 'Semi-major axis of the object'
            catalog['b'].unit = 'pixel'
            catalog['b'].description = 'Semi-minor axis of the object'
            catalog['theta'].unit = 'degree'
            catalog['theta'].description = 'Position angle of the object'
            catalog['kronrad'].unit = 'pixel'
            catalog['kronrad'].description = 'Kron radius used for extraction'
            catalog['ellipticity'].description = 'Ellipticity'
            catalog['fluxrad25'].unit = 'pixel'
            catalog[
                'fluxrad25'].description = 'Radius containing 25% of the flux'
            catalog['fluxrad50'].unit = 'pixel'
            catalog[
                'fluxrad50'].description = 'Radius containing 50% of the flux'
            catalog['fluxrad75'].unit = 'pixel'
            catalog[
                'fluxrad75'].description = 'Radius containing 75% of the flux'
            catalog['x2'].unit = 'pixel^2'
            catalog[
                'x2'].description = 'Variance on X coordinate of the object'
            catalog['y2'].unit = 'pixel^2'
            catalog[
                'y2'].description = 'Variance on Y coordinate of the object'
            catalog['xy'].unit = 'pixel^2'
            catalog['xy'].description = 'XY covariance of the object'
            catalog[
                'flag'].description = 'Bit mask of extraction/photometry flags'

            catalog.sort('flux')
            catalog.reverse()

            # Save some background statistics in the header
            mean_background = stats.sigma_clipped_mean(bkg.back(), 5.0)
            image.header['L1MEAN'] = (
                mean_background,
                '[counts] Sigma clipped mean of frame background')

            median_background = np.median(bkg.back())
            image.header['L1MEDIAN'] = (median_background,
                                        '[counts] Median of frame background')

            std_background = stats.robust_standard_deviation(bkg.back())
            image.header['L1SIGMA'] = (
                std_background, '[counts] Robust std dev of frame background')

            # Save some image statistics to the header
            good_objects = catalog['flag'] == 0
            for quantity in ['fwhm', 'ellipticity', 'theta']:
                good_objects = np.logical_and(
                    good_objects, np.logical_not(np.isnan(catalog[quantity])))
            if good_objects.sum() == 0:
                image.header['L1FWHM'] = ('NaN',
                                          '[arcsec] Frame FWHM in arcsec')
                image.header['L1ELLIP'] = ('NaN',
                                           'Mean image ellipticity (1-B/A)')
                image.header['L1ELLIPA'] = (
                    'NaN', '[deg] PA of mean image ellipticity')
            else:
                seeing = np.median(
                    catalog['fwhm'][good_objects]) * image.pixel_scale
                image.header['L1FWHM'] = (seeing,
                                          '[arcsec] Frame FWHM in arcsec')

                mean_ellipticity = stats.sigma_clipped_mean(
                    catalog['ellipticity'][good_objects], 3.0)
                image.header['L1ELLIP'] = (mean_ellipticity,
                                           'Mean image ellipticity (1-B/A)')

                mean_position_angle = stats.sigma_clipped_mean(
                    catalog['theta'][good_objects], 3.0)
                image.header['L1ELLIPA'] = (
                    mean_position_angle, '[deg] PA of mean image ellipticity')

            logging_tags = {
                key: float(image.header[key])
                for key in [
                    'L1MEAN', 'L1MEDIAN', 'L1SIGMA', 'L1FWHM', 'L1ELLIP',
                    'L1ELLIPA'
                ]
            }

            logger.info('Extracted sources',
                        image=image,
                        extra_tags=logging_tags)
            # adding catalog (a data table) to the appropriate images attribute.
            image.data_tables['catalog'] = DataTable(data_table=catalog,
                                                     name='CAT')
        except Exception:
            logger.error(logs.format_exception(), image=image)
        return image
Example #8
0
    def do_stage(self, images):
        for i, image in enumerate(images):
            try:
                # Set the number of source pixels to be 5% of the total. This keeps us safe from
                # satellites and airplanes.
                sep.set_extract_pixstack(int(image.nx * image.ny * 0.05))

                data = image.data.copy()
                error = (np.abs(data) + image.readnoise**2.0)**0.5
                mask = image.bpm > 0

                # Fits can be backwards byte order, so fix that if need be and subtract
                # the background
                try:
                    bkg = sep.Background(data,
                                         mask=mask,
                                         bw=32,
                                         bh=32,
                                         fw=3,
                                         fh=3)
                except ValueError:
                    data = data.byteswap(True).newbyteorder()
                    bkg = sep.Background(data,
                                         mask=mask,
                                         bw=32,
                                         bh=32,
                                         fw=3,
                                         fh=3)
                bkg.subfrom(data)

                # Do an initial source detection
                # TODO: Add back in masking after we are sure SEP works
                sources = sep.extract(data,
                                      self.threshold,
                                      minarea=self.min_area,
                                      err=error,
                                      deblend_cont=0.005)

                # Convert the detections into a table
                sources = Table(sources)

                # Calculate the ellipticity
                sources['ellipticity'] = 1.0 - (sources['b'] / sources['a'])

                # Fix any value of theta that are invalid due to floating point rounding
                # -pi / 2 < theta < pi / 2
                sources['theta'][sources['theta'] > (np.pi / 2.0)] -= np.pi
                sources['theta'][sources['theta'] < (-np.pi / 2.0)] += np.pi

                # Calculate the kron radius
                kronrad, krflag = sep.kron_radius(data, sources['x'],
                                                  sources['y'], sources['a'],
                                                  sources['b'],
                                                  sources['theta'], 6.0)
                sources['flag'] |= krflag
                sources['kronrad'] = kronrad

                # Calcuate the equivilent of flux_auto
                flux, fluxerr, flag = sep.sum_ellipse(data,
                                                      sources['x'],
                                                      sources['y'],
                                                      sources['a'],
                                                      sources['b'],
                                                      np.pi / 2.0,
                                                      2.5 * kronrad,
                                                      subpix=1,
                                                      err=error)
                sources['flux'] = flux
                sources['fluxerr'] = fluxerr
                sources['flag'] |= flag

                # Calculate the FWHMs of the stars:
                fwhm = 2.0 * (np.log(2) *
                              (sources['a']**2.0 + sources['b']**2.0))**0.5
                sources['fwhm'] = fwhm

                # Cut individual bright pixels. Often cosmic rays
                sources = sources[fwhm > 1.0]

                # Measure the flux profile
                flux_radii, flag = sep.flux_radius(data,
                                                   sources['x'],
                                                   sources['y'],
                                                   6.0 * sources['a'],
                                                   [0.25, 0.5, 0.75],
                                                   normflux=sources['flux'],
                                                   subpix=5)
                sources['flag'] |= flag
                sources['fluxrad25'] = flux_radii[:, 0]
                sources['fluxrad50'] = flux_radii[:, 1]
                sources['fluxrad75'] = flux_radii[:, 2]

                # Calculate the windowed positions
                sig = 2.0 / 2.35 * sources['fluxrad50']
                xwin, ywin, flag = sep.winpos(data, sources['x'], sources['y'],
                                              sig)
                sources['flag'] |= flag
                sources['xwin'] = xwin
                sources['ywin'] = ywin

                # Calculate the average background at each source
                bkgflux, fluxerr, flag = sep.sum_ellipse(bkg.back(),
                                                         sources['x'],
                                                         sources['y'],
                                                         sources['a'],
                                                         sources['b'],
                                                         np.pi / 2.0,
                                                         2.5 *
                                                         sources['kronrad'],
                                                         subpix=1)
                #masksum, fluxerr, flag = sep.sum_ellipse(mask, sources['x'], sources['y'],
                #                                         sources['a'], sources['b'], np.pi / 2.0,
                #                                         2.5 * kronrad, subpix=1)

                background_area = (
                    2.5 * sources['kronrad']
                )**2.0 * sources['a'] * sources['b'] * np.pi  # - masksum
                sources['background'] = bkgflux
                sources['background'][background_area > 0] /= background_area[
                    background_area > 0]
                # Update the catalog to match fits convention instead of python array convention
                sources['x'] += 1.0
                sources['y'] += 1.0

                sources['xpeak'] += 1
                sources['ypeak'] += 1

                sources['xwin'] += 1.0
                sources['ywin'] += 1.0

                sources['theta'] = np.degrees(sources['theta'])

                image.catalog = sources['x', 'y', 'xwin', 'ywin', 'xpeak',
                                        'ypeak', 'flux', 'fluxerr',
                                        'background', 'fwhm', 'a', 'b',
                                        'theta', 'kronrad', 'ellipticity',
                                        'fluxrad25', 'fluxrad50', 'fluxrad75',
                                        'x2', 'y2', 'xy', 'flag']

                # Add the units and description to the catalogs
                image.catalog['x'].unit = 'pixel'
                image.catalog['x'].description = 'X coordinate of the object'
                image.catalog['y'].unit = 'pixel'
                image.catalog['y'].description = 'Y coordinate of the object'
                image.catalog['xwin'].unit = 'pixel'
                image.catalog[
                    'xwin'].description = 'Windowed X coordinate of the object'
                image.catalog['ywin'].unit = 'pixel'
                image.catalog[
                    'ywin'].description = 'Windowed Y coordinate of the object'
                image.catalog['xpeak'].unit = 'pixel'
                image.catalog['xpeak'].description = 'X coordinate of the peak'
                image.catalog['ypeak'].unit = 'pixel'
                image.catalog[
                    'ypeak'].description = 'Windowed Y coordinate of the peak'
                image.catalog['flux'].unit = 'counts'
                image.catalog[
                    'flux'].description = 'Flux within a Kron-like elliptical aperture'
                image.catalog['fluxerr'].unit = 'counts'
                image.catalog[
                    'fluxerr'].description = 'Erronr on the flux within a Kron-like elliptical aperture'
                image.catalog['background'].unit = 'counts'
                image.catalog[
                    'background'].description = 'Average background value in the aperture'
                image.catalog['fwhm'].unit = 'pixel'
                image.catalog['fwhm'].description = 'FWHM of the object'
                image.catalog['a'].unit = 'pixel'
                image.catalog[
                    'a'].description = 'Semi-major axis of the object'
                image.catalog['b'].unit = 'pixel'
                image.catalog[
                    'b'].description = 'Semi-minor axis of the object'
                image.catalog['theta'].unit = 'degrees'
                image.catalog[
                    'theta'].description = 'Position angle of the object'
                image.catalog['kronrad'].unit = 'pixel'
                image.catalog[
                    'kronrad'].description = 'Kron radius used for extraction'
                image.catalog['ellipticity'].description = 'Ellipticity'
                image.catalog['fluxrad25'].unit = 'pixel'
                image.catalog[
                    'fluxrad25'].description = 'Radius containing 25% of the flux'
                image.catalog['fluxrad50'].unit = 'pixel'
                image.catalog[
                    'fluxrad50'].description = 'Radius containing 50% of the flux'
                image.catalog['fluxrad75'].unit = 'pixel'
                image.catalog[
                    'fluxrad75'].description = 'Radius containing 75% of the flux'
                image.catalog['x2'].unit = 'pixel^2'
                image.catalog[
                    'x2'].description = 'Variance on X coordinate of the object'
                image.catalog['y2'].unit = 'pixel^2'
                image.catalog[
                    'y2'].description = 'Variance on Y coordinate of the object'
                image.catalog['xy'].unit = 'pixel^2'
                image.catalog['xy'].description = 'XY covariance of the object'
                image.catalog[
                    'flag'].description = 'Bit mask combination of extraction and photometry flags'

                image.catalog.sort('flux')
                image.catalog.reverse()

                logging_tags = logs.image_config_to_tags(
                    image, self.group_by_keywords)
                logs.add_tag(logging_tags, 'filename',
                             os.path.basename(image.filename))

                # Save some background statistics in the header
                mean_background = stats.sigma_clipped_mean(bkg.back(), 5.0)
                image.header['L1MEAN'] = (
                    mean_background,
                    '[counts] Sigma clipped mean of frame background')
                logs.add_tag(logging_tags, 'L1MEAN', float(mean_background))

                median_background = np.median(bkg.back())
                image.header['L1MEDIAN'] = (
                    median_background, '[counts] Median of frame background')
                logs.add_tag(logging_tags, 'L1MEDIAN',
                             float(median_background))

                std_background = stats.robust_standard_deviation(bkg.back())
                image.header['L1SIGMA'] = (
                    std_background,
                    '[counts] Robust std dev of frame background')
                logs.add_tag(logging_tags, 'L1SIGMA', float(std_background))

                # Save some image statistics to the header
                good_objects = image.catalog['flag'] == 0

                seeing = np.median(
                    image.catalog['fwhm'][good_objects]) * image.pixel_scale
                image.header['L1FWHM'] = (seeing,
                                          '[arcsec] Frame FWHM in arcsec')
                logs.add_tag(logging_tags, 'L1FWHM', float(seeing))

                mean_ellipticity = stats.sigma_clipped_mean(
                    sources['ellipticity'][good_objects], 3.0)
                image.header['L1ELLIP'] = (mean_ellipticity,
                                           'Mean image ellipticity (1-B/A)')
                logs.add_tag(logging_tags, 'L1ELLIP', float(mean_ellipticity))

                mean_position_angle = stats.sigma_clipped_mean(
                    sources['theta'][good_objects], 3.0)
                image.header['L1ELLIPA'] = (
                    mean_position_angle, '[deg] PA of mean image ellipticity')
                logs.add_tag(logging_tags, 'L1ELLIPA',
                             float(mean_position_angle))

                self.logger.info('Extracted sources', extra=logging_tags)

            except Exception as e:
                logging_tags = logs.image_config_to_tags(
                    image, self.group_by_keywords)
                logs.add_tag(logging_tags, 'filename',
                             os.path.basename(image.filename))
                self.logger.error(e, extra=logging_tags)
        return images
Example #9
0
File: test.py Project: cmccully/sep
def test_vs_sextractor():
    """Test behavior of sep versus sextractor.

    Note: we turn deblending off for this test. This is because the
    deblending algorithm uses a random number generator. Since the sequence
    of random numbers is not the same between sextractor and sep or between
    different platforms, object member pixels (and even the number of objects)
    can differ when deblending is on.

    Deblending is turned off by setting DEBLEND_MINCONT=1.0 in the sextractor
    configuration file and by setting deblend_cont=1.0 in sep.extract().
    """

    data = np.copy(image_data)  # make an explicit copy so we can 'subfrom'
    bkg = sep.Background(data, bw=64, bh=64, fw=3, fh=3)

    # Test that SExtractor background is same as SEP:
    bkgarr = bkg.back(dtype=np.float32)
    assert_allclose(bkgarr, image_refback, rtol=1.e-5)

    # Extract objects (use deblend_cont=1.0 to disable deblending).
    bkg.subfrom(data)
    objs = sep.extract(data, 1.5*bkg.globalrms, deblend_cont=1.0)
    objs = np.sort(objs, order=['y'])

    # Read SExtractor result
    refobjs = np.loadtxt(IMAGECAT_FNAME, dtype=IMAGECAT_DTYPE)
    refobjs = np.sort(refobjs, order=['y'])

    # Found correct number of sources at the right locations?
    assert_allclose(objs['x'], refobjs['x'] - 1., atol=1.e-3)
    assert_allclose(objs['y'], refobjs['y'] - 1., atol=1.e-3)

    # Test aperture flux
    flux, fluxerr, flag = sep.sum_circle(data, objs['x'], objs['y'], 5.,
                                         err=bkg.globalrms)
    assert_allclose(flux, refobjs['flux_aper'], rtol=2.e-4)
    assert_allclose(fluxerr, refobjs['fluxerr_aper'], rtol=1.0e-5)

    # check if the flags work at all (comparison values 
    assert ((flag & sep.APER_TRUNC) != 0).sum() == 4
    assert ((flag & sep.APER_HASMASKED) != 0).sum() == 0

    # Test "flux_auto"
    kr, flag = sep.kron_radius(data, objs['x'], objs['y'], objs['a'],
                               objs['b'], objs['theta'], 6.0)

    flux, fluxerr, flag = sep.sum_ellipse(data, objs['x'], objs['y'],
                                          objs['a'], objs['b'],
                                          objs['theta'], r=2.5 * kr,
                                          err=bkg.globalrms, subpix=1)

    # For some reason, one object doesn't match. It's very small
    # and kron_radius is set to 0.0 in SExtractor, but 0.08 in sep.
    # Could be due to a change in SExtractor between v2.8.6 (used to
    # generate "truth" catalog) and v2.18.11 (from which sep was forked).
    i = 56  # index is 59 when deblending is on.
    kr[i] = 0.0
    flux[i] = 0.0
    fluxerr[i] = 0.0

    # We use atol for radius because it is reported to nearest 0.01 in
    # reference objects.
    assert_allclose(2.5*kr, refobjs['kron_radius'], atol=0.01, rtol=0.) 
    assert_allclose(flux, refobjs['flux_auto'], rtol=0.0005)
    assert_allclose(fluxerr, refobjs['fluxerr_auto'], rtol=0.0005)

    # Test ellipse representation conversion
    cxx, cyy, cxy = sep.ellipse_coeffs(objs['a'], objs['b'], objs['theta'])
    assert_allclose(cxx, objs['cxx'], rtol=1.e-4)
    assert_allclose(cyy, objs['cyy'], rtol=1.e-4)
    assert_allclose(cxy, objs['cxy'], rtol=1.e-4)

    a, b, theta = sep.ellipse_axes(objs['cxx'], objs['cyy'], objs['cxy'])
    assert_allclose(a, objs['a'], rtol=1.e-4)
    assert_allclose(b, objs['b'], rtol=1.e-4)
    assert_allclose(theta, objs['theta'], rtol=1.e-4)

    #test round trip
    cxx, cyy, cxy = sep.ellipse_coeffs(a, b, theta)
    assert_allclose(cxx, objs['cxx'], rtol=1.e-4)
    assert_allclose(cyy, objs['cyy'], rtol=1.e-4)
    assert_allclose(cxy, objs['cxy'], rtol=1.e-4)

    # test flux_radius
    fr, flags = sep.flux_radius(data, objs['x'], objs['y'], 6.*refobjs['a'],
                                [0.1, 0.5, 0.6], normflux=refobjs['flux_auto'],
                                subpix=5)
    assert_allclose(fr, refobjs["flux_radius"], rtol=0.04, atol=0.01)

    # test winpos
    sig = 2. / 2.35 * fr[:, 1]  # flux_radius = 0.5
    xwin, ywin, flag = sep.winpos(data, objs['x'], objs['y'], sig)
    assert_allclose(xwin, refobjs["xwin"] - 1., rtol=0., atol=0.0025)
    assert_allclose(ywin, refobjs["ywin"] - 1., rtol=0., atol=0.0025)
Example #10
0
def test_vs_sextractor():
    data = np.copy(image_data)  # make an explicit copy so we can 'subfrom'
    bkg = sep.Background(data, bw=64, bh=64, fw=3, fh=3)

    # Test that SExtractor background is same as SEP:
    bkgarr = bkg.back(dtype=np.float32)
    assert_allclose(bkgarr, image_refback, rtol=1.e-5)

    # Extract objects
    bkg.subfrom(data)
    objs = sep.extract(data, 1.5*bkg.globalrms)
    objs = np.sort(objs, order=['y'])

    # Read SExtractor result
    refobjs = np.loadtxt(IMAGECAT_FNAME, dtype=IMAGECAT_DTYPE)
    refobjs = np.sort(refobjs, order=['y'])

    # Found correct number of sources at the right locations?
    assert_allclose(objs['x'], refobjs['x'] - 1., atol=1.e-3)
    assert_allclose(objs['y'], refobjs['y'] - 1., atol=1.e-3)

    # Test aperture flux
    flux, fluxerr, flag = sep.sum_circle(data, objs['x'], objs['y'], 5.,
                                         err=bkg.globalrms)
    assert_allclose(flux, refobjs['flux_aper'], rtol=2.e-4)
    assert_allclose(fluxerr, refobjs['fluxerr_aper'], rtol=1.0e-5)

    # check if the flags work at all (comparison values 
    assert ((flag & sep.APER_TRUNC) != 0).sum() == 4
    assert ((flag & sep.APER_HASMASKED) != 0).sum() == 0

    # Test "flux_auto"
    kr, flag = sep.kron_radius(data, objs['x'], objs['y'], objs['a'],
                               objs['b'], objs['theta'], 6.0)

    flux, fluxerr, flag = sep.sum_ellipse(data, objs['x'], objs['y'],
                                          objs['a'], objs['b'],
                                          objs['theta'], r=2.5 * kr,
                                          err=bkg.globalrms, subpix=1)

    # For some reason, object at index 59 doesn't match. It's very small
    # and kron_radius is set to 0.0 in SExtractor, but 0.08 in sep.
    # Most of the other values are within 1e-4 except one which is only
    # within 0.01. This might be due to a change in SExtractor between
    # v2.8.6 (used to generate "truth" catalog) and v2.18.11.
    kr[59] = 0.0
    flux[59] = 0.0
    fluxerr[59] = 0.0
    assert_allclose(2.5*kr, refobjs['kron_radius'], rtol=0.01)
    assert_allclose(flux, refobjs['flux_auto'], rtol=0.01)
    assert_allclose(fluxerr, refobjs['fluxerr_auto'], rtol=0.01)

    # Test ellipse representation conversion
    cxx, cyy, cxy = sep.ellipse_coeffs(objs['a'], objs['b'], objs['theta'])
    assert_allclose(cxx, objs['cxx'], rtol=1.e-4)
    assert_allclose(cyy, objs['cyy'], rtol=1.e-4)
    assert_allclose(cxy, objs['cxy'], rtol=1.e-4)

    a, b, theta = sep.ellipse_axes(objs['cxx'], objs['cyy'], objs['cxy'])
    assert_allclose(a, objs['a'], rtol=1.e-4)
    assert_allclose(b, objs['b'], rtol=1.e-4)
    assert_allclose(theta, objs['theta'], rtol=1.e-4)

    #test round trip
    cxx, cyy, cxy = sep.ellipse_coeffs(a, b, theta)
    assert_allclose(cxx, objs['cxx'], rtol=1.e-4)
    assert_allclose(cyy, objs['cyy'], rtol=1.e-4)
    assert_allclose(cxy, objs['cxy'], rtol=1.e-4)

    # test flux_radius
    fr, flags = sep.flux_radius(data, objs['x'], objs['y'], 6.*refobjs['a'],
                                [0.1, 0.5, 0.6], normflux=refobjs['flux_auto'],
                                subpix=5)
    assert_allclose(fr, refobjs["flux_radius"], rtol=0.04, atol=0.01)

    # test winpos
    sig = 2. / 2.35 * fr[:, 1]  # flux_radius = 0.5
    xwin, ywin, flag = sep.winpos(data, objs['x'], objs['y'], sig)
    assert_allclose(xwin, refobjs["xwin"] - 1., rtol=0., atol=0.0025)
    assert_allclose(ywin, refobjs["ywin"] - 1., rtol=0., atol=0.0025)
Example #11
0
    def find_stars(self, image: Image) -> Table:
        """Find stars in given image and append catalog.

        Args:
            image: Image to find stars in.

        Returns:
            Full table with results.
        """
        import sep

        # get data and make it continuous
        data = image.data.copy()

        # mask?
        mask = image.mask.data if image.mask is not None else None

        # estimate background, probably we need to byte swap, and subtract it
        try:
            bkg = sep.Background(data, mask=mask, bw=32, bh=32, fw=3, fh=3)
        except ValueError as e:
            data = data.byteswap(True).newbyteorder()
            bkg = sep.Background(data, mask=mask, bw=32, bh=32, fw=3, fh=3)
        bkg.subfrom(data)

        # extract sources
        try:
            sources = sep.extract(data, self.threshold, err=bkg.globalrms, minarea=self.minarea,
                                  deblend_nthresh=self.deblend_nthresh, deblend_cont=self.deblend_cont,
                                  clean=self.clean, clean_param=self.clean_param, mask=mask)
        except:
            log.exception('An error has occured.')
            return Table()

        # convert to astropy table
        sources = Table(sources)

        # only keep sources with detection flag < 8
        sources = sources[sources['flag'] < 8]

        # Calculate the ellipticity
        sources['ellipticity'] = 1.0 - (sources['b'] / sources['a'])

        # calculate the FWHMs of the stars
        fwhm = 2.0 * (np.log(2) * (sources['a'] ** 2.0 + sources['b'] ** 2.0)) ** 0.5
        sources['fwhm'] = fwhm

        # get gain
        gain = image.header['DET-GAIN'] if 'DET-GAIN' in image.header else None

        # Kron radius
        kronrad, krflag = sep.kron_radius(data, sources['x'], sources['y'], sources['a'], sources['b'],
                                          sources['theta'], 6.0)
        sources['flag'] |= krflag
        sources['kronrad'] = kronrad

        # equivalent of FLUX_AUTO
        flux, fluxerr, flag = sep.sum_ellipse(data, sources['x'], sources['y'], sources['a'], sources['b'],
                                              sources['theta'], 2.5 * kronrad, subpix=1, mask=mask,
                                              err=bkg.rms(), gain=gain)
        sources['flag'] |= flag
        sources['flux'] = flux
        sources['fluxerr'] = fluxerr

        # radii at 0.25, 0.5, and 0.75 flux
        flux_radii, flag = sep.flux_radius(data, sources['x'], sources['y'], 6.0 * sources['a'], [0.25, 0.5, 0.75],
                                           normflux=sources['flux'], subpix=5)
        sources['flag'] |= flag
        sources['fluxrad25'] = flux_radii[:, 0]
        sources['fluxrad50'] = flux_radii[:, 1]
        sources['fluxrad75'] = flux_radii[:, 2]

        # xwin/ywin
        sig = 2. / 2.35 * sources['fluxrad50']
        xwin, ywin, flag = sep.winpos(data, sources['x'], sources['y'], sig)
        sources['flag'] |= flag
        sources['xwin'] = xwin
        sources['ywin'] = ywin

        # perform aperture photometry for diameters of 1" to 8"
        for diameter in [1, 2, 3, 4, 5, 6, 7, 8]:
            flux, fluxerr, flag = sep.sum_circle(data, sources['x'], sources['y'],
                                                 diameter / 2. / image.pixel_scale,
                                                 mask=mask, err=bkg.rms(), gain=gain)
            sources['fluxaper{0}'.format(diameter)] = flux
            sources['fluxerr{0}'.format(diameter)] = fluxerr
            sources['flag'] |= flag

        # average background at each source
        # since SEP sums up whole pixels, we need to do the same on an image of ones for the background_area
        bkgflux, fluxerr, flag = sep.sum_ellipse(bkg.back(), sources['x'], sources['y'],
                                                 sources['a'], sources['b'], np.pi / 2.0,
                                                 2.5 * sources['kronrad'], subpix=1)
        background_area, _, _ = sep.sum_ellipse(np.ones(shape=bkg.back().shape), sources['x'], sources['y'],
                                                sources['a'], sources['b'], np.pi / 2.0,
                                                2.5 * sources['kronrad'], subpix=1)
        sources['background'] = bkgflux
        sources['background'][background_area > 0] /= background_area[background_area > 0]

        # match fits conventions
        sources['x'] += 1.0
        sources['xpeak'] += 1
        sources['xwin'] += 1.0
        sources['xmin'] += 1
        sources['xmax'] += 1
        sources['y'] += 1.0
        sources['ypeak'] += 1
        sources['ywin'] += 1.0
        sources['ymin'] += 1
        sources['ymax'] += 1
        sources['theta'] = np.degrees(sources['theta'])

        # pick columns for catalog
        cat = sources['x', 'y', 'xwin', 'ywin', 'xpeak', 'ypeak',
                      'flux', 'fluxerr', 'peak', 'fluxaper1', 'fluxerr1',
                      'fluxaper2', 'fluxerr2', 'fluxaper3', 'fluxerr3',
                      'fluxaper4', 'fluxerr4', 'fluxaper5', 'fluxerr5',
                      'fluxaper6', 'fluxerr6', 'fluxaper7', 'fluxerr7',
                      'fluxaper8', 'fluxerr8', 'background', 'fwhm',
                      'a', 'b', 'theta', 'kronrad', 'ellipticity',
                      'fluxrad25', 'fluxrad50', 'fluxrad75',
                      'x2', 'y2', 'xy', 'flag']

        # set it
        image.catalog = cat

        # return full catalog
        return sources
Example #12
0
    async def __call__(self, image: Image) -> Image:
        """Find stars in given image and append catalog.

        Args:
            image: Image to find stars in.

        Returns:
            Image with attached catalog.
        """
        import sep

        loop = asyncio.get_running_loop()

        # got data?
        if image.data is None:
            log.warning("No data found in image.")
            return image

        # no mask?
        mask = image.mask if image.mask is not None else np.zeros(
            image.data.shape, dtype=bool)

        # remove background
        data, bkg = SepSourceDetection.remove_background(image.data, mask)

        # extract sources
        sources = await loop.run_in_executor(
            None,
            partial(
                sep.extract,
                data,
                self.threshold,
                err=bkg.globalrms,
                minarea=self.minarea,
                deblend_nthresh=self.deblend_nthresh,
                deblend_cont=self.deblend_cont,
                clean=self.clean,
                clean_param=self.clean_param,
                mask=image.mask,
            ),
        )

        # convert to astropy table
        sources = pd.DataFrame(sources)

        # only keep sources with detection flag < 8
        sources = sources[sources["flag"] < 8]
        x, y = sources["x"], sources["y"]

        # Calculate the ellipticity
        sources["ellipticity"] = 1.0 - (sources["b"] / sources["a"])

        # calculate the FWHMs of the stars
        fwhm = 2.0 * (np.log(2) * (sources["a"]**2.0 + sources["b"]**2.0))**0.5
        sources["fwhm"] = fwhm

        # clip theta to [-pi/2,pi/2]
        sources["theta"] = sources["theta"].clip(lower=np.pi / 2,
                                                 upper=np.pi / 2)

        # Kron radius
        kronrad, krflag = sep.kron_radius(data, x, y, sources["a"],
                                          sources["b"], sources["theta"], 6.0)
        sources["flag"] |= krflag
        sources["kronrad"] = kronrad

        # equivalent of FLUX_AUTO
        gain = image.header["DET-GAIN"] if "DET-GAIN" in image.header else None
        flux, fluxerr, flag = await loop.run_in_executor(
            None,
            partial(
                sep.sum_ellipse,
                data,
                x,
                y,
                sources["a"],
                sources["b"],
                sources["theta"],
                2.5 * kronrad,
                subpix=5,
                mask=image.mask,
                gain=gain,
            ),
        )
        sources["flag"] |= flag
        sources["flux"] = flux

        # radii at 0.25, 0.5, and 0.75 flux
        flux_radii, flag = sep.flux_radius(data,
                                           x,
                                           y,
                                           6.0 * sources["a"],
                                           [0.25, 0.5, 0.75],
                                           normflux=sources["flux"],
                                           subpix=5)
        sources["flag"] |= flag
        sources["fluxrad25"] = flux_radii[:, 0]
        sources["fluxrad50"] = flux_radii[:, 1]
        sources["fluxrad75"] = flux_radii[:, 2]

        # xwin/ywin
        sig = 2.0 / 2.35 * sources["fluxrad50"]
        xwin, ywin, flag = sep.winpos(data, x, y, sig)
        sources["flag"] |= flag
        sources["xwin"] = xwin
        sources["ywin"] = ywin

        # theta in degrees
        sources["theta"] = np.degrees(sources["theta"])

        # only keep sources with detection flag < 8
        sources = sources[sources["flag"] < 8]

        # match fits conventions
        sources["x"] += 1
        sources["y"] += 1

        # pick columns for catalog
        cat = sources[[
            "x",
            "y",
            "peak",
            "flux",
            "fwhm",
            "a",
            "b",
            "theta",
            "ellipticity",
            "tnpix",
            "kronrad",
            "fluxrad25",
            "fluxrad50",
            "fluxrad75",
            "xwin",
            "ywin",
        ]]

        # copy image, set catalog and return it
        img = image.copy()
        img.catalog = Table.from_pandas(cat)
        return img