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
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)
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
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
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_)
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
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
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
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)
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)
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
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