Beispiel #1
0
    def detect(self):

        # Source detection using segmentation
        kernel = astro.convolution.Gaussian2DKernel(self.sigma,
                                                    x_size=3,
                                                    y_size=3)
        kernel.normalize()
        segm = phot.detect_sources(self.data,
                                   self.threshold,
                                   npixels=5,
                                   filter_kernel=kernel)

        # Deblending sources
        segm_deblend = phot.deblend_sources(self.data,
                                            segm,
                                            npixels=5,
                                            filter_kernel=kernel,
                                            nlevels=32,
                                            contrast=0.001)

        cat = phot.source_properties(self.data, segm_deblend, wcs=self.wcs)
        sources = cat.to_table()
        sources['xcentroid'].info.format = '.2f'  # optional format
        sources['ycentroid'].info.format = '.2f'
        sources['cxx'].info.format = '.2f'
        sources['cxy'].info.format = '.2f'
        sources['cyy'].info.format = '.2f'
        sources['gini'].info.format = '.2f'

        return sources.to_pandas().sort_values('max_value', ascending=True)
Beispiel #2
0
def deblend_segments(image, segm, npixels=None, fwhm=8., kernel_size=4, nlevels=30, contrast=1/1000):
    """
    Deblend overlapping sources labeled in a segmentation image.

    Parameters
    ----------
    image : array like
        Input image.

    segm : `~photutils.segmentation.SegmentationImage` or `None`
        A 2D segmentation image, with the same shape as ``data``, where
        sources are marked by different positive integer values.  A
        value of zero is reserved for the background.  If no sources
        are found then `None` is returned.

    npixels : int
        The number of connected pixels, each greater than ``threshold``,
        that an object must have to be detected.  ``npixels`` must be a
        positive integer.

    fwhm : float
        FWHM of smoothing gaussian kernel.

    kernel_size : int
        Size of smoothing kernel.

    nlevels : int, optional
        The number of multi-thresholding levels to use.  Each source
        will be re-thresholded at ``nlevels`` levels spaced
        exponentially or linearly (see the ``mode`` keyword) between its
        minimum and maximum values within the source segment.

    contrast : float, optional
        The fraction of the total (blended) source flux that a local
        peak must have (at any one of the multi-thresholds) to be
        considered as a separate object.  ``contrast`` must be between 0
        and 1, inclusive.  If ``contrast = 0`` then every local peak
        will be made a separate object (maximum deblending).  If
        ``contrast = 1`` then no deblending will occur.  The default is
        0.001, which will deblend sources with a 7.5 magnitude
        difference.

    Returns
    -------
    segment_image : `~photutils.segmentation.SegmentationImage`
        A segmentation image, with the same shape as ``data``, where
        sources are marked by different positive integer values.  A
        value of zero is reserved for the background.
    """

    if npixels is None:
        npixels = fwhm ** 2

    kernel = make_kernel(fwhm, kernel_size) if kernel_size else None

    segm_deblend = deblend_sources(image, segm,
                                   npixels=npixels, kernel=kernel,
                                   nlevels=nlevels, contrast=contrast)

    return segm_deblend
Beispiel #3
0
def _segmap_base(data,
                 numpix,
                 mask=None,
                 nsigma=2,
                 contrast=0.4,
                 nlevels=5,
                 kernel=None):
    """Returns a generic segmentation map of the input image
    INPUTS:
      data:     image data
      numpix:   minimum region size in pixels (default: 10)
      mask:     mask for the image (default: None)
      snr:      signal-to-noise threshold for detecting objects (default: 2)
      contrast: contrast ratio used in deblending (default: 0.4)
      nlevels:  number of lebels to split image to when deblending (default: 5)
      kernel:   kernel to use for image smoothing (default: None)
    """

    # Convert mask to boolean
    if mask is not None and (mask.dtype != "bool"):
        mask = np.array(mask, dtype="bool")

    threshold = phot.detect_threshold(data, nsigma=nsigma, mask=mask)
    segmap = phot.detect_sources(data,
                                 threshold,
                                 numpix,
                                 filter_kernel=kernel,
                                 mask=mask)
    segmap = phot.deblend_sources(data,
                                  segmap,
                                  npixels=numpix,
                                  filter_kernel=kernel,
                                  nlevels=nlevels,
                                  contrast=contrast)
    return segmap
Beispiel #4
0
def make_segmentation_image(data,
                            fwhm=2.0,
                            snr=5.0,
                            x_size=5,
                            y_size=5,
                            npixels=7,
                            nlevels=32,
                            contrast=0.001,
                            deblend=True):
    """
    Use photutils to create a segmentation image containing detected sources.

    data : 2D `~numpy.ndarray`
        Image to segment into sources.

    fwhm : float (default: 2.0)
        FWHM of the kernel used to filter the image.

    snr : float (default: 5.0)
        Source S/N used to set detection threshold.

    x_size : int (default: 5)
        X size of the 2D `~astropy.convolution.Gaussian2DKernel` filter.

    y_size : int (default: 5)
        Y size of the 2D `~astropy.convolution.Gaussian2DKernel` filter.

    npixels : int (default: 7)
        Number of connected pixels required to be considered a source.

    nlevels : int (default: 32)
        Number of multi-thresholding levels to use when deblending sources.

    contrast : float (default: 0.001)
        Fraction of the total blended flux that a local peak must have to be considered a separate object.

    deblend : bool (default: True)
        If true, deblend sources after creating segmentation image.
    """
    sigma = fwhm * stats.gaussian_fwhm_to_sigma
    kernel = Gaussian2DKernel(sigma, x_size=x_size, y_size=y_size)
    kernel.normalize()
    threshold = photutils.detect_threshold(data, nsigma=snr)

    segm = photutils.detect_sources(data,
                                    threshold,
                                    npixels=npixels,
                                    filter_kernel=kernel)
    if deblend:
        segm = photutils.deblend_sources(data,
                                         segm,
                                         npixels=npixels,
                                         filter_kernel=kernel,
                                         nlevels=nlevels,
                                         contrast=contrast)

    return segm
Beispiel #5
0
def deblend_sources(in_image, segm_obj, kernel, errmap, ext_name):
    fo = fits.open(in_image, "append")
    hdu = fo[ext_name]

    if segm_obj is None:
        nhdu = fits.ImageHDU()

        # save segmap and info
        nhdu.header["EXTNAME"] = "DEBLEND"

        thdu = fits.BinTableHDU()
        thdu.header["EXTNAME"] = "DEBLEND_PROPS"

        fo.append(nhdu)
        fo.append(thdu)

        fo.flush()
        fo.close()
        return None

    segm_obj = photutils.deblend_sources(hdu.data,
                                         segm_obj,
                                         npixels=5,
                                         filter_kernel=kernel)
    segmap = segm_obj.data

    props = photutils.source_properties(hdu.data, segmap, errmap)
    props_table = astropy.table.Table(props.to_table())
    # these give problems given their format/NoneType objects
    props_table.remove_columns([
        "sky_centroid",
        "sky_centroid_icrs",
        "source_sum_err",
        "background_sum",
        "background_mean",
        "background_at_centroid",
    ])
    nhdu = fits.ImageHDU(segmap)

    # save segmap and info
    nhdu.header["EXTNAME"] = "DEBLEND"

    thdu = fits.BinTableHDU(props_table)
    thdu.header["EXTNAME"] = "DEBLEND_PROPS"

    fo.append(nhdu)
    fo.append(thdu)

    fo.flush()
    fo.close()
    return segm_obj
Beispiel #6
0
    def make_segmentation_image(self,
                                threshold=2.5,
                                npixels=5,
                                nlevels=32,
                                save_segmentation_image=False):

        SegmentationImage = detect_sources(self.DetectionImage.sig,
                                           threshold,
                                           npixels=npixels)
        self.DeblendedSegmentationImage = deblend_sources(
            self.DetectionImage.sig,
            SegmentationImage,
            npixels=npixels,
            nlevels=nlevels)

        if save_segmentation_image:
            pickle.dump(DeblendedSegmentationImage,
                        open('temp/DeblendedSegmentationImage.p', 'wb'))
Beispiel #7
0
def detect_obj(img, snr=2.8, exp_sz= 1.2, plt_show = True):
    threshold = detect_threshold(img, snr=snr)
    center_img = len(img)/2
    sigma = 3.0 * gaussian_fwhm_to_sigma# FWHM = 3.
    kernel = Gaussian2DKernel(sigma, x_size=5, y_size=5)
    kernel.normalize()
    segm = detect_sources(img, threshold, npixels=10, filter_kernel=kernel)
    npixels = 20
    segm_deblend = deblend_sources(img, segm, npixels=npixels,
                                    filter_kernel=kernel, nlevels=25,
                                    contrast=0.001)
    #Number of objects segm_deblend.data.max()
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12.5, 10))
    import copy, matplotlib
    my_cmap = copy.copy(matplotlib.cm.get_cmap('gist_heat')) # copy the default cmap
    my_cmap.set_bad('black')
    vmin = 1.e-3
    vmax = 2.1 
    ax1.imshow(img, origin='lower', cmap=my_cmap, norm=LogNorm(), vmin=vmin, vmax=vmax)
    ax1.set_title('Data')
    ax2.imshow(segm_deblend, origin='lower', cmap=segm_deblend.cmap(random_state=12345))
    ax2.set_title('Segmentation Image')
    plt.show()
    
    columns = ['id', 'xcentroid', 'ycentroid', 'source_sum', 'area']
    cat = source_properties(img, segm_deblend)
    tbl = cat.to_table(columns=columns)
    tbl['xcentroid'].info.format = '.2f'  # optional format
    tbl['ycentroid'].info.format = '.2f'
    print(tbl)
    cat = source_properties(img, segm_deblend)
    objs = []
    for obj in cat:
        position = (obj.xcentroid.value-center_img, obj.ycentroid.value-center_img)
        a_o = obj.semimajor_axis_sigma.value
        b_o = obj.semiminor_axis_sigma.value
        Re = np.pi * a_o * b_o /2.
        q = 1 - obj.ellipticity.to_value()
        objs.append((position,Re,q))
    dis_sq = [np.sqrt((objs[i][0][0])**2+(objs[i][0][1])**2) for i in range(len(objs))]
    dis_sq = np.array(dis_sq)
    c_index= np.where(dis_sq == dis_sq.min())[0][0]
    return objs, c_index
Beispiel #8
0
    def find_sources(data_sub, x_size = 5, y_size = 5, npixels = 10, connectivity = 8):
    	'''
		Using photutils to detect the sources within the full fits files

		Arguments:
			data_sub: Background subtracted fits file

		Optional Arguments:
			x_size: x extent of the kernel which slides over the image to detect the sources 
					-- defaults to 5 pixels
			y_size: y extent of the kernel which slides over the image to detect the sources 
					-- defaults to 5 pixels
			n_pixels: number of connected pixels that are greater than the threshold to count a source 
					-- defaults to 10 pixels
			connectivity: The type of pixel connectivity used in determining how pixels are grouped into a detected source.
					-- defaults to 8 pixels which touch along their edges or corners.

    	'''

    	print('Finding sources using photutils')
    	start = time.time()

    	median = np.median(data_sub)
        std = mad_std(data_sub)

        threshold = bkg + (5.0 * bkg_rms)
        sigma = 5.0 * gaussian_fwhm_to_sigma
        kernel = Gaussian2DKernel(sigma, x_size = x_size, y_size = y_size) # Kernel defaults to 8*stddev
        kernel.normalize()

        segmented_image = detect_sources(data_sub, threshold, npixels = npixels, filter_kernel = kernel, connectivity = connectivity)
        segmented_image_deblend = deblend_sources(data_sub, segmented_image, npixels = npixels, filter_kernel = kernel, connectivity = connectivity)

        cat = source_properties(data_sub, segmented_image_deblend)

        # Getting values of the individual stars to place into a table
        x_pos = cat.xcentroid.value
        y_pos = cat.ycentroid.value
        area = cat.area.value
        max_pixel_val = cat.max_value
        ids = cat.id

        return ids, x_pos, y_pos, area, max_pixel_val
                       filter_size=(3, 3),
                       bkg_estimator=bkg_estimator)
    threshold = bkg.background + (10. * bkg.background_rms)

    sigma = 3.0 * gaussian_fwhm_to_sigma  # FWHM = 3.
    kernel = Gaussian2DKernel(sigma, x_size=3, y_size=3)
    kernel.normalize()
    npixels = 5
    segm = detect_sources(data,
                          threshold,
                          npixels=npixels,
                          filter_kernel=kernel)
    #if ever we want to deblend the source
    segm_deblend = deblend_sources(data,
                                   segm,
                                   npixels=npixels,
                                   filter_kernel=kernel,
                                   nlevels=32,
                                   contrast=0.001)

    cat = source_properties(data, segm_deblend, wcs=w)
    tbl = cat.to_table()
    df = tbl.to_pandas()

    #Setting parameters on data
    indexNames = df[df['xcentroid'] < 400].index
    dfsel = df.drop(indexNames)
    indexNames = dfsel[dfsel['xcentroid'] > 7776].index
    dfsel = dfsel.drop(indexNames)
    indexNames = dfsel[dfsel['ycentroid'] < 300].index
    dfsel = dfsel.drop(indexNames)
    indexNames = dfsel[dfsel['ycentroid'] > 5832].index
Beispiel #10
0
def extract_sources(img, **pars):
    """Use photutils to find sources in image based on segmentation.

    Parameters
    ==========
    dqmask : array
        Bitmask which identifies whether a pixel should be used (1) in source
        identification or not(0). If provided, this mask will be applied to the
        input array prior to source identification.

    fwhm : float
        Full-width half-maximum (fwhm) of the PSF in pixels.
        Default: 3.0

    threshold : float or None
        Value from the image which serves as the limit for determining sources.
        If None, compute a default value of (background+5*rms(background)).
        If threshold < 0.0, use absolute value as scaling factor for default value.
        Default: None

    source_box : int
        Size of box (in pixels) which defines the minimum size of a valid source

    classify : boolean
        Specify whether or not to apply classification based on invarient moments
        of each source to determine whether or not a source is likely to be a
        cosmic-ray, and not include those sources in the final catalog.
        Default: True

    centering_mode : {'segmentation', 'starfind'}
        Algorithm to use when computing the positions of the detected sources.
        Centering will only take place after `threshold` has been determined, and
        sources are identified using segmentation.  Centering using `segmentation`
        will rely on `photutils.segmentation.source_properties` to generate the
        properties for the source catalog.  Centering using `starfind` will use
        `photutils.IRAFStarFinder` to characterize each source in the catalog.
        Default: 'starfind'

    nlargest : int, None
        Number of largest (brightest) sources in each chip/array to measure
        when using 'starfind' mode.  Default: None (all)

    output : str
        If specified, write out the catalog of sources to the file with this name

    plot : boolean
        Specify whether or not to create a plot of the sources on a view of the image
        Default: False

    vmax : float
        If plotting the sources, scale the image to this maximum value.

    """
    fwhm= pars.get('fwhm', 3.0)
    threshold= pars.get('threshold', None)
    source_box = pars.get('source_box', 7)
    classify = pars.get('classify', True)
    output = pars.get('output', None)
    plot = pars.get('plot', False)
    vmax = pars.get('vmax', None)
    centering_mode = pars.get('centering_mode', 'starfind')
    deblend = pars.get('deblend', False)
    dqmask = pars.get('dqmask',None)
    nlargest = pars.get('nlargest', None)
    # apply any provided dqmask for segmentation only
    if dqmask is not None:
        imgarr = img.copy()
        imgarr[dqmask] = 0
    else:
        imgarr = img

    bkg_estimator = MedianBackground()
    bkg = None

    exclude_percentiles = [10,25,50,75]
    for percentile in exclude_percentiles:
        try:
            bkg = Background2D(imgarr, (50, 50), filter_size=(3, 3),
                           bkg_estimator=bkg_estimator,
                           exclude_percentile=percentile)
            # If it succeeds, stop and use that value
            bkg_rms = (5. * bkg.background_rms)
            bkg_rms_mean = bkg.background.mean() + 5. * bkg_rms.std()
            default_threshold = bkg.background + bkg_rms
            if threshold is None or threshold < 0.0:
                if threshold is not None and threshold < 0.0:
                    threshold = -1*threshold*default_threshold
                    log.info("{} based on {}".format(threshold.max(), default_threshold.max()))
                    bkg_rms_mean = threshold.max()
                else:
                    threshold = default_threshold
            else:
                bkg_rms_mean = 3. * threshold
            if bkg_rms_mean < 0:
                bkg_rms_mean = 0.
            break
        except Exception:
            bkg = None

    # If Background2D does not work at all, define default scalar values for
    # the background to be used in source identification
    if bkg is None:
        bkg_rms_mean = max(0.01, imgarr.min())
        bkg_rms = bkg_rms_mean * 5

    sigma = fwhm * gaussian_fwhm_to_sigma
    kernel = Gaussian2DKernel(sigma, x_size=source_box, y_size=source_box)
    kernel.normalize()
    segm = detect_sources(imgarr, threshold, npixels=source_box,
                          filter_kernel=kernel)
    if deblend:
        segm = deblend_sources(imgarr, segm, npixels=5,
                           filter_kernel=kernel, nlevels=16,
                           contrast=0.01)
    # If classify is turned on, it should modify the segmentation map
    if classify:
        cat = source_properties(imgarr, segm)
        if len(cat) > 0:
            # Remove likely cosmic-rays based on central_moments classification
            bad_srcs = np.where(classify_sources(cat) == 0)[0]+1
            segm.remove_labels(bad_srcs) # CAUTION: May be time-consuming!!!


    # convert segm to mask for daofind
    if centering_mode == 'starfind':
        src_table = None
        #daofind = IRAFStarFinder(fwhm=fwhm, threshold=5.*bkg.background_rms_median)
        log.info("Setting up DAOStarFinder with: \n    fwhm={}  threshold={}".format(fwhm, bkg_rms_mean))
        daofind = DAOStarFinder(fwhm=fwhm, threshold=bkg_rms_mean)
        # Identify nbrightest/largest sources
        if nlargest is not None:
            if nlargest > len(segm.labels):
                nlargest = len(segm.labels)
            large_labels = np.flip(np.argsort(segm.areas)+1)[:nlargest]
        log.info("Looking for sources in {} segments".format(len(segm.labels)))

        for label in segm.labels:
            if nlargest is not None and label not in large_labels:
                continue # Move on to the next segment
            # Get slice definition for the segment with this label
            seg_slice = segm.segments[label-1].slices
            seg_yoffset = seg_slice[0].start
            seg_xoffset = seg_slice[1].start

            #Define raw data from this slice
            detection_img = img[seg_slice]
            # zero out any pixels which do not have this segments label
            detection_img[np.where(segm.data[seg_slice]==0)] = 0

            # Detect sources in this specific segment
            seg_table = daofind(detection_img)
            # Pick out brightest source only
            if src_table is None and len(seg_table) > 0:
                # Initialize final master source list catalog
                src_table = Table(names=seg_table.colnames,
                                  dtype=[dt[1] for dt in seg_table.dtype.descr])
            if len(seg_table) > 0:
                max_row = np.where(seg_table['peak'] == seg_table['peak'].max())[0][0]
                # Add row for detected source to master catalog
                # apply offset to slice to convert positions into full-frame coordinates
                seg_table['xcentroid'] += seg_xoffset
                seg_table['ycentroid'] += seg_yoffset
                src_table.add_row(seg_table[max_row])

    else:
        cat = source_properties(img, segm)
        src_table = cat.to_table()
        # Make column names consistent with IRAFStarFinder column names
        src_table.rename_column('source_sum', 'flux')
        src_table.rename_column('source_sum_err', 'flux_err')

    if src_table is not None:
        log.info("Total Number of detected sources: {}".format(len(src_table)))
    else:
        log.info("No detected sources!")
        return None, None

    # Move 'id' column from first to last position
    # Makes it consistent for remainder of code
    cnames = src_table.colnames
    cnames.append(cnames[0])
    del cnames[0]
    tbl = src_table[cnames]

    if output:
        tbl['xcentroid'].info.format = '.10f'  # optional format
        tbl['ycentroid'].info.format = '.10f'
        tbl['flux'].info.format = '.10f'
        if not output.endswith('.cat'):
            output += '.cat'
        tbl.write(output, format='ascii.commented_header')
        log.info("Wrote source catalog: {}".format(output))

    if plot and plt is not None:
        norm = None
        if vmax is None:
            norm = ImageNormalize(stretch=SqrtStretch())
        fig, ax = plt.subplots(2, 2, figsize=(8, 8))
        ax[0][0].imshow(imgarr, origin='lower', cmap='Greys_r', norm=norm, vmax=vmax)
        ax[0][1].imshow(segm, origin='lower', cmap=segm.cmap(random_state=12345))
        ax[0][1].set_title('Segmentation Map')
        ax[1][0].imshow(bkg.background, origin='lower')
        if not isinstance(threshold, float):
            ax[1][1].imshow(threshold, origin='lower')
    return tbl, segm
Beispiel #11
0
 def _seg_image(self, x, y, r_cut=100):
     """
     detect and deblend sources into segmentation maps
     :param x: int, x coordinate in pixel unit
     :param y: int, y coordinate in pixel unit
     :param r_cut:  int format value, radius of cut out image
     :return:
     """
     snr = self.snr
     npixels = self.npixels
     bakground = self.bakground
     error = self.bkg_rms(x, y, r_cut)
     kernel = self.kernel
     image_cutted = self.cut_image(x, y, r_cut)
     image_data = image_cutted
     threshold_detect_objs = detect_threshold(data=image_data,
                                              nsigma=snr,
                                              background=bakground,
                                              error=error)
     segments = detect_sources(image_data,
                               threshold_detect_objs,
                               npixels=npixels,
                               filter_kernel=kernel)
     segments_deblend = deblend_sources(image_data,
                                        segments,
                                        npixels=npixels,
                                        nlevels=10)
     segments_deblend_info = source_properties(image_data, segments_deblend)
     nobjs = segments_deblend_info.to_table(columns=['id'])['id'].max()
     xcenter = segments_deblend_info.to_table(
         columns=['xcentroid'])['xcentroid'].value
     ycenter = segments_deblend_info.to_table(
         columns=['ycentroid'])['ycentroid'].value
     image_data_size = np.int((image_data.shape[0] + 1) / 2.)
     dist = ((xcenter - image_data_size)**2 +
             (ycenter - image_data_size)**2)**0.5
     c_index = np.where(dist == dist.min())[0][0]
     center_mask = (segments_deblend.data
                    == c_index + 1) * 1  #supposed to be the data mask
     obj_masks = []
     for i in range(nobjs):
         mask = ((segments_deblend.data == i + 1) * 1)
         obj_masks.append(mask)
     xmin = segments_deblend_info.to_table(
         columns=['bbox_xmin'])['bbox_xmin'].value
     xmax = segments_deblend_info.to_table(
         columns=['bbox_xmax'])['bbox_xmax'].value
     ymin = segments_deblend_info.to_table(
         columns=['bbox_ymin'])['bbox_ymin'].value
     ymax = segments_deblend_info.to_table(
         columns=['bbox_ymax'])['bbox_ymax'].value
     xmin_c, xmax_c = xmin[c_index], xmax[c_index]
     ymin_c, ymax_c = ymin[c_index], ymax[c_index]
     xsize_c = xmax_c - xmin_c
     ysize_c = ymax_c - ymin_c
     if xsize_c > ysize_c:
         r_center = np.int(xsize_c)
     else:
         r_center = np.int(ysize_c)
     center_mask_info = [center_mask, r_center, xcenter, ycenter, c_index]
     return obj_masks, center_mask_info, segments_deblend
Beispiel #12
0
def get_sources(detection_frame,
                mask=False,
                sigma=5.0,
                mode='DAO',
                fwhm=2.5,
                threshold=None,
                npix=4,
                return_segm_image=False):
    """
    Main method used to identify sources in a detection frame and estimate their position.
    Different modes are available, accesible through the ``mode`` keyword :

    * DAO : uses the :class:`photutils:photutils.DAOStarFinder` method, adapted from DAOPHOT.
    * IRAF : uses the :class:`photutils:photutils.IRAFStarFinder` method, adapted from IRAF.
    * PEAK : uses the :func:`photutils:photutils.find_peaks` method, looking for local peaks above a given threshold.
    * ORB : uses the :func:`ORB:orb.utils.astrometry.detect_stars` method, fitting stars in the frame
    * SEGM : uses the :func:`photutils:photutils.detect_sources` method, segmenting the image.

    The most reliable is SEGM.

    Parameters
    ----------
    detection_frame : 2D :class:`~numpy:numpy.ndarray`
        Map on which the sources should be visible.
    mask : 2D :class:`~numpy:numpy.ndarray` or bool,  Default = False
        (Optional) If passed, only sources inside the mask are detected.
    sigma : float
        (Optional) Signal to Noise of the detections we want to keep. Only used if threshold is None. In this case, the signal and the noise are computed with sigma-clipping on the deteciton frame. Default = 5
    threshold : float or 2D :class:`~numpy:numpy.ndarray` of floats
        (Optional) Threshold above which we consider having a detection. Default is None
    mode : str
        (Optional) One of the detection mode listed above. Dafault = 'DAO'
    fwhm : float
        (Optional) Expected FWHM of the sources. Default : 2.5
    npix : int
        (Optional) Only used by the 'SEGM' method : minimum number of connected pixels with flux above the threshold to make a credible source. Default = 4
    return_segm_image : bool, Default = False
        (Optional) Only used in the 'SEGM' mode. If True, returns the obtained segmentation image.

    Returns
    -------
    sources : :class:`~pandas:pandas.DataFrame`
        A DataFrame where each row represents a detection, with at least the positions named as ``xcentroid``, ``ycentroid`` (WARNING : using astropy convention). The other columns depend on the mode used.

    """
    if mask is False:
        mask = np.ones_like(detection_frame)
    if threshold is None:
        mean, median, std = sigma_clipped_stats(
            detection_frame, sigma=3.0, iters=5,
            mask=~mask.astype(bool))  #On masque la region hors de l'anneau
        threshold = median + sigma * std
    #On detecte sur toute la frame, mais on garde que ce qui est effectivement dans l'anneau
    if mode == 'DAO':
        daofind = DAOStarFinder(fwhm=fwhm, threshold=threshold)
        sources = daofind(detection_frame)
    elif mode == 'IRAF':
        irafind = IRAFStarFinder(threshold=threshold, fwhm=fwhm)
        sources = irafind(detection_frame)
    elif mode == 'PEAK':
        sources = find_peaks(detection_frame, threshold=threshold)
        sources.rename_column('x_peak', 'xcentroid')
        sources.rename_column('y_peak', 'ycentroid')
    elif mode == 'ORB':
        astro = Astrometry(detection_frame, instrument='sitelle')
        path, fwhm_arc = astro.detect_stars(min_star_number=5000,
                                            r_max_coeff=1.,
                                            filter_image=False)
        star_list = astro.load_star_list(path)
        sources = Table([star_list[:, 0], star_list[:, 1]],
                        names=('ycentroid', 'xcentroid'))
    elif mode == 'SEGM':
        logging.info('Detecting')
        segm = detect_sources(detection_frame, threshold, npixels=npix)
        deblend = True
        labels = segm.labels
        if deblend:
            # while labels.shape != (0,):
            #     try:
            #         #logging.info('Deblending')
            #         # fwhm = 3.
            #         # s = fwhm / (2.0 * np.sqrt(2.0 * np.log(2.0)))
            #         # kernel = Gaussian2DKernel(s, x_size = 3, y_size = 3)
            #         # kernel = Box2DKernel(3, mode='integrate')
            #         deblended = deblend_sources(detection_frame, segm, npixels=npix, labels=labels)#, filter_kernel=kernel)
            #         success = True
            #     except ValueError as e:
            #         #warnings.warn('Deblend was not possible.\n %s'%e)
            #         source_id = int(e.args[0].split('"')[1])
            #         id = np.argwhere(labels == source_id)[0,0]
            #         labels = np.concatenate((labels[:id], labels[id+1:]))
            #         success = False
            #     if success is True:
            #         break
            try:
                logging.info('Deblending')
                # fwhm = 3.
                # s = fwhm / (2.0 * np.sqrt(2.0 * np.log(2.0)))
                # kernel = Gaussian2DKernel(s, x_size = 3, y_size = 3)
                # kernel = Box2DKernel(3, mode='integrate')
                deblended = deblend_sources(
                    detection_frame, segm,
                    npixels=npix)  #, filter_kernel=kernel)
            except ValueError as e:
                warnings.warn('Deblend was not possible.\n %s' % e)
                deblended = segm
            logging.info('Retieving properties')
            sources = source_properties(detection_frame, deblended).to_table()
        else:
            deblended = segm
            logging.info('Retieving properties')
            sources = source_properties(detection_frame, deblended).to_table()
        logging.info('Filtering Quantity columns')
        for col in sources.colnames:
            if type(sources[col]) is Quantity:
                sources[col] = sources[col].value
    sources = mask_sources(sources, mask)  # On filtre
    df = sources.to_pandas()
    if return_segm_image:
        return deblended.array, df
    else:
        return df
Beispiel #13
0
def make_source_catalog(model, kernel_fwhm, kernel_xsize, kernel_ysize,
                        snr_threshold, npixels, deblend_nlevels=32,
                        deblend_contrast=0.001, deblend_mode='exponential',
                        connectivity=8, deblend=False):
    """
    Create a final catalog of source photometry and morphologies.

    Parameters
    ----------
    model : `DrizProductModel`
        The input `DrizProductModel` of a single drizzled image.  The
        input image is assumed to be background subtracted.

    kernel_fwhm : float
        The full-width at half-maximum (FWHM) of the 2D Gaussian kernel
        used to filter the image before thresholding.  Filtering the
        image will smooth the noise and maximize detectability of
        objects with a shape similar to the kernel.

    kernel_xsize : odd int
        The size in the x dimension (columns) of the kernel array.

    kernel_ysize : odd int
        The size in the y dimension (row) of the kernel array.

    snr_threshold : float
        The signal-to-noise ratio per pixel above the ``background`` for
        which to consider a pixel as possibly being part of a source.

    npixels : int
        The number of connected pixels, each greater than the threshold
        that an object must have to be detected.  ``npixels`` must be a
        positive integer.

    deblend_nlevels : int, optional
        The number of multi-thresholding levels to use for deblending
        sources.  Each source will be re-thresholded at
        ``deblend_nlevels``, spaced exponentially or linearly (see the
        ``deblend_mode`` keyword), between its minimum and maximum
        values within the source segment.

    deblend_contrast : float, optional
        The fraction of the total (blended) source flux that a local
        peak must have to be considered as a separate object.
        ``deblend_contrast`` must be between 0 and 1, inclusive.  If
        ``deblend_contrast = 0`` then every local peak will be made a
        separate object (maximum deblending).  If ``deblend_contrast =
        1`` then no deblending will occur.  The default is 0.001, which
        will deblend sources with a magnitude differences of about 7.5.

    deblend_mode : {'exponential', 'linear'}, optional
        The mode used in defining the spacing between the
        multi-thresholding levels (see the ``deblend_nlevels`` keyword)
        when deblending sources.

    connectivity : {4, 8}, optional
        The type of pixel connectivity used in determining how pixels
        are grouped into a detected source.  The options are 4 or 8
        (default).  4-connected pixels touch along their edges.
        8-connected pixels touch along their edges or corners.  For
        reference, SExtractor uses 8-connected pixels.

    deblend : bool, optional
        Whether to deblend overlapping sources.  Source deblending
        requires scikit-image.

    Returns
    -------
    catalog : `~astropy.Table`
        An astropy Table containing the source photometry and
        morphologies.
    """

    if not isinstance(model, DrizProductModel):
        raise ValueError('The input model must be a DrizProductModel.')

    # Use this when model.wht contains an IVM map
    # Calculate "background-only" error assuming the weight image is an
    # inverse-variance map (IVM).  The weight image is clipped because it
    # may contain zeros.
    # bkg_error = np.sqrt(1.0 / np.clip(model.wht, 1.0e-20, 1.0e20))
    # threshold = snr_threshold * bkg_error

    # Estimate the 1-sigma noise in the image empirically because model.wht
    # does not yet contain an IVM map
    mask = (model.wht == 0)
    data_mean, data_median, data_std = sigma_clipped_stats(
        model.data, mask=mask, sigma=3.0, maxiters=10)
    threshold = data_median + (data_std * snr_threshold)

    sigma = kernel_fwhm * gaussian_fwhm_to_sigma
    kernel = Gaussian2DKernel(sigma, x_size=kernel_xsize, y_size=kernel_ysize)
    kernel.normalize()

    segm = photutils.detect_sources(model.data, threshold, npixels=npixels,
                                    filter_kernel=kernel,
                                    connectivity=connectivity)

    # source deblending requires scikit-image
    if deblend:
        segm = photutils.deblend_sources(model.data, segm, npixels=npixels,
                                         filter_kernel=kernel,
                                         nlevels=deblend_nlevels,
                                         contrast=deblend_contrast,
                                         mode=deblend_mode,
                                         connectivity=connectivity,
                                         relabel=True)

    # Calculate total error, including source Poisson noise.
    # This calculation assumes that the data and bkg_error images are in
    # units of electron/s.  Poisson noise is not included for pixels
    # where data < 0.
    exptime = model.meta.resample.product_exposure_time    # total exptime
    # total_error = np.sqrt(bkg_error**2 +
    #                       np.maximum(model.data / exptime, 0))
    total_error = np.sqrt(data_std**2 + np.maximum(model.data / exptime, 0))

    wcs = model.get_fits_wcs()
    source_props = photutils.source_properties(
        model.data, segm, error=total_error, filter_kernel=kernel, wcs=wcs)

    if len(source_props) == 0:
        return QTable()    # empty table

    columns = ['id', 'xcentroid', 'ycentroid', 'sky_centroid', 'area',
               'source_sum', 'source_sum_err', 'semimajor_axis_sigma',
               'semiminor_axis_sigma', 'orientation',
               'sky_bbox_ll', 'sky_bbox_ul', 'sky_bbox_lr', 'sky_bbox_ur']
    catalog = source_props.to_table(columns=columns)

    # convert orientation to degrees
    orient_deg = catalog['orientation'].to(u.deg)
    catalog.replace_column('orientation', orient_deg)

    # define orientation position angle
    rot = _get_rotation(wcs)
    catalog['orientation_sky'] = ((270. - rot +
                                   catalog['orientation'].value) * u.deg)

    # define flux in microJanskys
    nsources = len(catalog)
    pixelarea = model.meta.photometry.pixelarea_arcsecsq
    if pixelarea is None:
        micro_Jy = np.full(nsources, np.nan)
    else:
        micro_Jy = (catalog['source_sum'] *
                    model.meta.photometry.conversion_microjanskys *
                    model.meta.photometry.pixelarea_arcsecsq)

    # define AB mag
    abmag = np.full(nsources, np.nan)
    mask = np.isfinite(micro_Jy)
    abmag[mask] = -2.5 * np.log10(micro_Jy[mask]) + 23.9
    catalog['abmag'] = abmag

    # define AB mag error
    # assuming SNR >> 1 (otherwise abmag_error is asymmetric)
    abmag_error = (2.5 * np.log10(np.e) * catalog['source_sum_err'] /
                   catalog['source_sum'])
    abmag_error[~mask] = np.nan
    catalog['abmag_error'] = abmag_error

    return catalog
Beispiel #14
0
def make_source_catalog(model,
                        kernel_fwhm,
                        kernel_xsize,
                        kernel_ysize,
                        snr_threshold,
                        npixels,
                        deblend_nlevels=32,
                        deblend_contrast=0.001,
                        deblend_mode='exponential',
                        connectivity=8,
                        deblend=False):
    """
    Create a final catalog of source photometry and morphologies.

    Parameters
    ----------
    model : `DrizProductModel`
        The input `DrizProductModel` of a single drizzled image.  The
        input image is assumed to be background subtracted.

    kernel_fwhm : float
        The full-width at half-maximum (FWHM) of the 2D Gaussian kernel
        used to filter the image before thresholding.  Filtering the
        image will smooth the noise and maximize detectability of
        objects with a shape similar to the kernel.

    kernel_xsize : odd int
        The size in the x dimension (columns) of the kernel array.

    kernel_ysize : odd int
        The size in the y dimension (row) of the kernel array.

    snr_threshold : float
        The signal-to-noise ratio per pixel above the ``background`` for
        which to consider a pixel as possibly being part of a source.

    npixels : int
        The number of connected pixels, each greater than the threshold
        that an object must have to be detected.  ``npixels`` must be a
        positive integer.

    deblend_nlevels : int, optional
        The number of multi-thresholding levels to use for deblending
        sources.  Each source will be re-thresholded at
        ``deblend_nlevels``, spaced exponentially or linearly (see the
        ``deblend_mode`` keyword), between its minimum and maximum
        values within the source segment.

    deblend_contrast : float, optional
        The fraction of the total (blended) source flux that a local
        peak must have to be considered as a separate object.
        ``deblend_contrast`` must be between 0 and 1, inclusive.  If
        ``deblend_contrast = 0`` then every local peak will be made a
        separate object (maximum deblending).  If ``deblend_contrast =
        1`` then no deblending will occur.  The default is 0.001, which
        will deblend sources with a magnitude differences of about 7.5.

    deblend_mode : {'exponential', 'linear'}, optional
        The mode used in defining the spacing between the
        multi-thresholding levels (see the ``deblend_nlevels`` keyword)
        when deblending sources.

    connectivity : {4, 8}, optional
        The type of pixel connectivity used in determining how pixels
        are grouped into a detected source.  The options are 4 or 8
        (default).  4-connected pixels touch along their edges.
        8-connected pixels touch along their edges or corners.  For
        reference, SExtractor uses 8-connected pixels.

    deblend : bool, optional
        Whether to deblend overlapping sources.  Source deblending
        requires scikit-image.

    Returns
    -------
    catalog : `~astropy.Table`
        An astropy Table containing the source photometry and
        morphologies.
    """

    if not isinstance(model, DrizProductModel):
        raise ValueError('The input model must be a DrizProductModel.')

    # Use this when model.wht contains an IVM map
    # Calculate "background-only" error assuming the weight image is an
    # inverse-variance map (IVM).  The weight image is clipped because it
    # may contain zeros.
    # bkg_error = np.sqrt(1.0 / np.clip(model.wht, 1.0e-20, 1.0e20))
    # threshold = snr_threshold * bkg_error

    # Estimate the 1-sigma noise in the image empirically because model.wht
    # does not yet contain an IVM map
    mask = (model.wht == 0)
    data_mean, data_median, data_std = sigma_clipped_stats(model.data,
                                                           mask=mask,
                                                           sigma=3.0,
                                                           maxiters=10)
    threshold = data_median + (data_std * snr_threshold)

    sigma = kernel_fwhm * gaussian_fwhm_to_sigma
    kernel = Gaussian2DKernel(sigma, x_size=kernel_xsize, y_size=kernel_ysize)
    kernel.normalize()

    segm = photutils.detect_sources(model.data,
                                    threshold,
                                    npixels=npixels,
                                    filter_kernel=kernel,
                                    connectivity=connectivity)

    # source deblending requires scikit-image
    if deblend:
        segm = photutils.deblend_sources(model.data,
                                         segm,
                                         npixels=npixels,
                                         filter_kernel=kernel,
                                         nlevels=deblend_nlevels,
                                         contrast=deblend_contrast,
                                         mode=deblend_mode,
                                         connectivity=connectivity,
                                         relabel=True)

    # Calculate total error, including source Poisson noise.
    # This calculation assumes that the data and bkg_error images are in
    # units of electron/s.  Poisson noise is not included for pixels
    # where data < 0.
    exptime = model.meta.resample.product_exposure_time  # total exptime
    #total_error = np.sqrt(bkg_error**2 +
    #                      np.maximum(model.data / exptime, 0))
    total_error = np.sqrt(data_std**2 + np.maximum(model.data / exptime, 0))

    wcs = model.get_fits_wcs()
    source_props = photutils.source_properties(model.data,
                                               segm,
                                               error=total_error,
                                               filter_kernel=kernel,
                                               wcs=wcs)

    if len(source_props) == 0:
        return QTable()  # empty table

    columns = [
        'id', 'xcentroid', 'ycentroid', 'sky_centroid', 'area', 'source_sum',
        'source_sum_err', 'semimajor_axis_sigma', 'semiminor_axis_sigma',
        'orientation', 'sky_bbox_ll', 'sky_bbox_ul', 'sky_bbox_lr',
        'sky_bbox_ur'
    ]
    catalog = source_props.to_table(columns=columns)

    # convert orientation to degrees
    orient_deg = catalog['orientation'].to(u.deg)
    catalog.replace_column('orientation', orient_deg)

    # define orientation position angle
    rot = _get_rotation(wcs)
    catalog['orientation_sky'] = ((270. - rot + catalog['orientation'].value) *
                                  u.deg)

    # define flux in microJanskys
    nsources = len(catalog)
    pixelarea = model.meta.photometry.pixelarea_arcsecsq
    if pixelarea is None:
        micro_Jy = np.full(nsources, np.nan)
    else:
        micro_Jy = (catalog['source_sum'] *
                    model.meta.photometry.conversion_microjanskys *
                    model.meta.photometry.pixelarea_arcsecsq)

    # define AB mag
    abmag = np.full(nsources, np.nan)
    mask = np.isfinite(micro_Jy)
    abmag[mask] = -2.5 * np.log10(micro_Jy[mask]) + 23.9
    catalog['abmag'] = abmag

    # define AB mag error
    # assuming SNR >> 1 (otherwise abmag_error is asymmetric)
    abmag_error = (2.5 * np.log10(np.e) * catalog['source_sum_err'] /
                   catalog['source_sum'])
    abmag_error[~mask] = np.nan
    catalog['abmag_error'] = abmag_error

    return catalog
Beispiel #15
0
    def obj_center(self):
        """Returns center pixel coords of Jupiter whether or not Jupiter is on ND filter.  Unbinned pixel coords are returned.  Use [Cor]Obs_Data.binned() to convert to binned pixels.
        """
        # Returns stored center for object, None for flats
        if self._obj_center is not None or self.isflat:
            return self._obj_center

        # Work with unbinned image
        im = self.HDU_unbinned
        back_level = self.back_level / (np.prod(self._binning))

        satlevel = self.header.get('SATLEVEL')
        if satlevel is None:
            satlevel = sx694.satlevel

        # Establish some metrics to see if Jupiter is on or off the ND
        # filter.  Easiest one is number of saturated pixels
        # /data/io/IoIO/raw/2018-01-28/R-band_off_ND_filter.fit gives
        # 4090 of these.  Calculation below suggests 1000 should be a
        # good minimum number of saturated pixels (assuming no
        # additional scattered light).  A star off the ND filter
        # /data/io/IoIO/raw/2017-05-28/Sky_Flat-0001_SII_on-band.fit
        # gives 124 num_sat
        satc = np.where(im >= satlevel)
        num_sat = len(satc[0])
        #log.debug('Number of saturated pixels in image: ' + str(num_sat))


        # --> this is from photometry_process to see if Jupiter is on
        # --> the ND filter.  It would be great if we could use that
        # --> code generically

        sigma = self.seeing * gaussian_fwhm_to_sigma
        kernel = Gaussian2DKernel(sigma)
        kernel.normalize()
        # Make a source mask to enable optimal background estimation
        mask = make_source_mask(self.HDUList[0].data, nsigma=2, npixels=5,
                                filter_kernel=kernel, mask=self.ND_only_mask,
                                dilate_size=11)
        #impl = plt.imshow(mask, origin='lower',
        #                  cmap=plt.cm.gray,
        #                  filternorm=0, interpolation='none')
        #plt.show()
        
        mean, median, std = sigma_clipped_stats(self.HDUList[0].data,
                                                sigma=3.0,
                                                mask=self.ND_only_mask)
        threshold = median + (2.0 * std)
        
        ### This seems too fancy for narrow ND filter
        ##box_size = int(np.mean(self.HDUList[0].data.shape) / 10)
        ##back = Background2D(self.HDUList[0].data, box_size,
        ##                    mask=mask, coverage_mask=self.ND_only_mask)
        ##threshold = back.background + (2.0* back.background_rms)
        ##
        ##print(f'background_median = {back.background_median}, background_rms_median = {back.background_rms_median}')
        
        #impl = plt.imshow(back.background, origin='lower',
        #                  cmap=plt.cm.gray,
        #                  filternorm=0, interpolation='none')
        #back.plot_meshes()
        #plt.show()
        
        npixels = 5
        segm = detect_sources(self.HDUList[0].data, threshold, npixels=npixels,
                              filter_kernel=kernel,  mask=self.ND_only_mask)
        
        if segm is not None:
            # Some object found on the ND filter

            # It does save a little time and a factor ~1.3 in memory if we
            # don't deblend
            segm_deblend = deblend_sources(self.HDUList[0].data,
                                           segm, npixels=npixels,
                                           filter_kernel=kernel, nlevels=32,
                                           contrast=0.001)

            #impl = plt.imshow(segm, origin='lower',
            #                  cmap=plt.cm.gray,
            #                  filternorm=0, interpolation='none')
            #plt.show()

            cat = source_properties(self.HDUList[0].data,
                                    segm_deblend,
                                    mask=self.ND_only_mask)
            tbl = cat.to_table(('source_sum',
                                'moments',
                                'moments_central',
                                'inertia_tensor',
                                'centroid'))
            tbl.sort('source_sum', reverse=True)

            #xcentrd = tbl['xcentroid'][0].value
            #ycentrd = tbl['ycentroid'][0].value
            print(tbl['moments'][0])
            print(tbl['moments_central'][0])
            print(tbl['inertia_tensor'][0])
            print(tbl['centroid'][0])
            centroid = tbl['centroid'][0]

            self._obj_center = centroid

            #self._obj_center = np.asarray((ycentrd, xcentrd))
            log.debug('Object center (X, Y; binned) = '
                      + str(self.binned(self._obj_center)[::-1]))
            self.quality = 6
        else:
            log.warning('No object found on ND filter')
            # Outside the ND filter, Jupiter should be saturating.  To
            # make the center of mass calc more accurate, just set
            # everything that is not getting toward saturation to 0
            # --> Might want to fine-tune or remove this so bright
            im[np.where(im < satlevel*0.7)] = 0
            
            #log.debug('Approx number of saturating pixels ' + str(np.sum(im)/65000))

            # 25 worked for a star, 250 should be conservative for
            # Jupiter (see above calcs)
            # if np.sum(im) < satlevel * 25:
            if np.sum(im) < satlevel * 250:
                self.quality = 4
                log.warning('Jupiter (or suitably bright object) not found in image.  This object is unlikely to show up on the ND filter.  Seeting quality to ' + str(self.quality) + ', center to [-99, -99]')
                self._obj_center = np.asarray([-99, -99])
            else:
                self.quality = 6
                # If we made it here, Jupiter is outside the ND filter,
                # but shining bright enough to be found
                # --> Try iterative approach
                ny, nx = im.shape
                y_x = np.asarray(ndimage.measurements.center_of_mass(im))
                print(y_x)
                y = np.arange(ny) - y_x[0]
                x = np.arange(nx) - y_x[1]
                # input/output Cartesian direction by default
                xx, yy = np.meshgrid(x, y)
                rr = np.sqrt(xx**2 + yy**2)
                im[np.where(rr > 200)] = 0
                y_x = np.asarray(ndimage.measurements.center_of_mass(im))
    
                self._obj_center = y_x
                log.info('Object center (X, Y; binned) = ' +
                      str(self.binned(self._obj_center)[::-1]))

        self.header['OBJ_CR0'] = (self._obj_center[1], 'Object center X')
        self.header['OBJ_CR1'] = (self._obj_center[0], 'Object center Y')
        self.header['QUALITY'] = (self.quality, 'Quality on 0-10 scale of center determination')
        return self._obj_center
Beispiel #16
0
def detect_obj(image,
               nsigma=2.8,
               exp_sz=1.2,
               npixels=15,
               if_plot=False,
               auto_sort_center=True):
    """
    Define the apeatures for all the objects in the image.
    
    Parameter
    --------
        img : 2-D array type image.
            The input image
    
        exp_sz : float.
            The level to expand the mask region.
    
        nsigma : float.
            The number of standard deviations per pixel above the
            ``background`` for which to consider a pixel as possibly being
            part of a source.
        
        npixels: int.
            The number of connected pixels, each greater than ``threshold``,
            that an object must have to be detected.  ``npixels`` must be a
            positive integer.
        
        if_plot: bool.
            If ture, plot the detection figure.    
        
    Return
    --------
        A list of photutils defined apeatures that cover the detected objects.
    """
    from photutils import detect_threshold
    from astropy.stats import gaussian_fwhm_to_sigma
    from astropy.convolution import Gaussian2DKernel
    from photutils import detect_sources, deblend_sources
    from photutils import source_properties
    if version.parse(photutils.__version__) > version.parse("0.7"):
        threshold = detect_threshold(image, nsigma=nsigma)
    else:
        threshold = detect_threshold(image, snr=nsigma)
    # center_image = len(image)/2
    sigma = 3.0 * gaussian_fwhm_to_sigma  # FWHM = 3.
    kernel = Gaussian2DKernel(sigma, x_size=3, y_size=3)
    kernel.normalize()
    segm = detect_sources(image,
                          threshold,
                          npixels=npixels,
                          filter_kernel=kernel)
    segm_deblend = deblend_sources(image,
                                   segm,
                                   npixels=npixels,
                                   filter_kernel=kernel,
                                   nlevels=25,
                                   contrast=0.001)
    #Number of objects segm_deblend.data.max()
    cat = source_properties(image, segm_deblend)
    columns = [
        'id', 'xcentroid', 'ycentroid', 'source_sum', 'orientation', 'area'
    ]
    tbl = cat.to_table(columns=columns)
    tbl['xcentroid'].info.format = '.2f'  # optional format
    tbl['ycentroid'].info.format = '.2f'
    tbl['id'] -= 1

    apertures = []
    segm_deblend_size = segm_deblend.areas
    from photutils import EllipticalAperture
    for obj in cat:
        size = segm_deblend_size[obj.id - 1]
        position = (obj.xcentroid.value, obj.ycentroid.value)
        a_o = obj.semimajor_axis_sigma.value
        b_o = obj.semiminor_axis_sigma.value
        size_o = np.pi * a_o * b_o
        r = np.sqrt(size / size_o) * exp_sz
        a, b = a_o * r, b_o * r
        if version.parse(photutils.__version__) > version.parse("0.7"):
            theta = obj.orientation.value / 180 * np.pi
        else:
            theta = obj.orientation.value
        apertures.append(EllipticalAperture(position, a, b, theta=theta))

    if auto_sort_center == True:
        center = np.array([len(image) / 2, len(image) / 2])
        dis_sq = [
            np.sum((apertures[i].positions - center)**2)
            for i in range(len(apertures))
        ]
        dis_sq = np.array(dis_sq)
        c_idx = np.where(dis_sq == dis_sq.min())[0][0]
        apertures = [apertures[c_idx]] + [
            apertures[i] for i in range(len(apertures)) if i != c_idx
        ]
        cat = [cat[c_idx]] + [cat[i] for i in range(len(cat)) if i != c_idx]
        tbl['id'][0] = c_idx
        tbl['id'][c_idx] = 0

    if if_plot == True:
        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12.5, 10))
        vmin = 1.e-3
        vmax = 2.1
        ax1.imshow(image,
                   origin='lower',
                   cmap=my_cmap,
                   norm=LogNorm(),
                   vmin=vmin,
                   vmax=vmax)
        ax1.set_title('Data')
        if version.parse(photutils.__version__) > version.parse("0.7"):
            ax2.imshow(segm_deblend,
                       origin='lower',
                       cmap=segm_deblend.make_cmap(random_state=12345))
        else:
            ax2.imshow(segm_deblend,
                       origin='lower',
                       cmap=segm_deblend.cmap(random_state=12345))
        for i in range(len(cat)):
            ax2.text(cat[i].xcentroid.value,
                     cat[i].ycentroid.value,
                     '{0}'.format(i),
                     fontsize=15,
                     bbox={
                         'facecolor': 'white',
                         'alpha': 0.5,
                         'pad': 1
                     })
        for i in range(len(apertures)):
            aperture = apertures[i]
            if version.parse(photutils.__version__) > version.parse("0.7"):
                aperture.plot(color='white', lw=1.5, axes=ax1)
                aperture.plot(color='white', lw=1.5, axes=ax2)
            else:
                aperture.plot(color='white', lw=1.5, ax=ax1)
                aperture.plot(color='white', lw=1.5, ax=ax2)
        ax2.set_title('Segmentation Image')
        plt.show()
        print(tbl)
    return apertures
def extract_sources(img, **pars):
    """Use photutils to find sources in image based on segmentation.

    Parameters
    ==========
    dqmask : array
        Bitmask which identifies whether a pixel should be used (1) in source
        identification or not(0). If provided, this mask will be applied to the
        input array prior to source identification.

    fwhm : float
        Full-width half-maximum (fwhm) of the PSF in pixels.
        Default: 3.0

    threshold : float or None
        Value from the image which serves as the limit for determining sources.
        If None, compute a default value of (background+5*rms(background)).
        If threshold < 0.0, use absolute value as scaling factor for default value.
        Default: None

    source_box : int
        Size of box (in pixels) which defines the minimum size of a valid source

    classify : boolean
        Specify whether or not to apply classification based on invarient moments
        of each source to determine whether or not a source is likely to be a
        cosmic-ray, and not include those sources in the final catalog.
        Default: True

    centering_mode : {'segmentation', 'starfind'}
        Algorithm to use when computing the positions of the detected sources.
        Centering will only take place after `threshold` has been determined, and
        sources are identified using segmentation.  Centering using `segmentation`
        will rely on `photutils.segmentation.source_properties` to generate the
        properties for the source catalog.  Centering using `starfind` will use
        `photutils.IRAFStarFinder` to characterize each source in the catalog.
        Default: 'starfind'

    nlargest : int, None
        Number of largest (brightest) sources in each chip/array to measure
        when using 'starfind' mode.  Default: None (all)

    output : str
        If specified, write out the catalog of sources to the file with this name

    plot : boolean
        Specify whether or not to create a plot of the sources on a view of the image
        Default: False

    vmax : float
        If plotting the sources, scale the image to this maximum value.

    """
    fwhm = pars.get('fwhm', 3.0)
    threshold = pars.get('threshold', None)
    source_box = pars.get('source_box', 7)
    classify = pars.get('classify', True)
    output = pars.get('output', None)
    plot = pars.get('plot', False)
    vmax = pars.get('vmax', None)
    centering_mode = pars.get('centering_mode', 'starfind')
    deblend = pars.get('deblend', False)
    dqmask = pars.get('dqmask', None)
    nlargest = pars.get('nlargest', None)
    # apply any provided dqmask for segmentation only
    if dqmask is not None:
        imgarr = img.copy()
        imgarr[dqmask] = 0
    else:
        imgarr = img

    bkg_estimator = MedianBackground()
    bkg = None

    exclude_percentiles = [10, 25, 50, 75]
    for percentile in exclude_percentiles:
        try:
            bkg = Background2D(imgarr, (50, 50),
                               filter_size=(3, 3),
                               bkg_estimator=bkg_estimator,
                               exclude_percentile=percentile)
            # If it succeeds, stop and use that value
            bkg_rms = (5. * bkg.background_rms)
            bkg_rms_mean = bkg.background.mean() + 5. * bkg_rms.std()
            default_threshold = bkg.background + bkg_rms
            if threshold is None or threshold < 0.0:
                if threshold is not None and threshold < 0.0:
                    threshold = -1 * threshold * default_threshold
                    print("{} based on {}".format(threshold.max(),
                                                  default_threshold.max()))
                    bkg_rms_mean = threshold.max()
                else:
                    threshold = default_threshold
            else:
                bkg_rms_mean = 3. * threshold
            if bkg_rms_mean < 0:
                bkg_rms_mean = 0.
            break
        except Exception:
            bkg = None

    # If Background2D does not work at all, define default scalar values for
    # the background to be used in source identification
    if bkg is None:
        bkg_rms_mean = max(0.01, imgarr.min())
        bkg_rms = bkg_rms_mean * 5

    sigma = fwhm * gaussian_fwhm_to_sigma
    kernel = Gaussian2DKernel(sigma, x_size=source_box, y_size=source_box)
    kernel.normalize()
    segm = detect_sources(imgarr,
                          threshold,
                          npixels=source_box,
                          filter_kernel=kernel)
    if deblend:
        segm = deblend_sources(imgarr,
                               segm,
                               npixels=5,
                               filter_kernel=kernel,
                               nlevels=16,
                               contrast=0.01)
    # If classify is turned on, it should modify the segmentation map
    if classify:
        cat = source_properties(imgarr, segm)
        # Remove likely cosmic-rays based on central_moments classification
        bad_srcs = np.where(classify_sources(cat) == 0)[0] + 1
        segm.remove_labels(bad_srcs)  # CAUTION: May be time-consuming!!!

    # convert segm to mask for daofind
    if centering_mode == 'starfind':
        src_table = None
        #daofind = IRAFStarFinder(fwhm=fwhm, threshold=5.*bkg.background_rms_median)
        print("Setting up DAOStarFinder with: \n    fwhm={}  threshold={}".
              format(fwhm, bkg_rms_mean))
        daofind = DAOStarFinder(fwhm=fwhm, threshold=bkg_rms_mean)
        # Identify nbrightest/largest sources
        if nlargest is not None:
            if nlargest > len(segm.labels):
                nlargest = len(segm.labels)
            large_labels = np.flip(np.argsort(segm.areas) + 1)[:nlargest]
        print("Looking for sources in {} segments".format(len(segm.labels)))

        for label in segm.labels:
            if nlargest is not None and label not in large_labels:
                continue  # Move on to the next segment
            # Get slice definition for the segment with this label
            seg_slice = segm.segments[label - 1].slices
            seg_yoffset = seg_slice[0].start
            seg_xoffset = seg_slice[1].start

            #Define raw data from this slice
            detection_img = img[seg_slice]
            # zero out any pixels which do not have this segments label
            detection_img[np.where(segm.data[seg_slice] == 0)] = 0

            # Detect sources in this specific segment
            seg_table = daofind(detection_img)
            # Pick out brightest source only
            if src_table is None and len(seg_table) > 0:
                # Initialize final master source list catalog
                src_table = Table(
                    names=seg_table.colnames,
                    dtype=[dt[1] for dt in seg_table.dtype.descr])
            if len(seg_table) > 0:
                max_row = np.where(
                    seg_table['peak'] == seg_table['peak'].max())[0][0]
                # Add row for detected source to master catalog
                # apply offset to slice to convert positions into full-frame coordinates
                seg_table['xcentroid'] += seg_xoffset
                seg_table['ycentroid'] += seg_yoffset
                src_table.add_row(seg_table[max_row])

    else:
        cat = source_properties(img, segm)
        src_table = cat.to_table()
        # Make column names consistent with IRAFStarFinder column names
        src_table.rename_column('source_sum', 'flux')
        src_table.rename_column('source_sum_err', 'flux_err')

    if src_table is not None:
        print("Total Number of detected sources: {}".format(len(src_table)))
    else:
        print("No detected sources!")
        return None, None

    # Move 'id' column from first to last position
    # Makes it consistent for remainder of code
    cnames = src_table.colnames
    cnames.append(cnames[0])
    del cnames[0]
    tbl = src_table[cnames]

    if output:
        tbl['xcentroid'].info.format = '.10f'  # optional format
        tbl['ycentroid'].info.format = '.10f'
        tbl['flux'].info.format = '.10f'
        if not output.endswith('.cat'):
            output += '.cat'
        tbl.write(output, format='ascii.commented_header')
        print("Wrote source catalog: {}".format(output))

    if plot:
        norm = None
        if vmax is None:
            norm = ImageNormalize(stretch=SqrtStretch())
        fig, ax = plt.subplots(2, 2, figsize=(8, 8))
        ax[0][0].imshow(imgarr,
                        origin='lower',
                        cmap='Greys_r',
                        norm=norm,
                        vmax=vmax)
        ax[0][1].imshow(segm,
                        origin='lower',
                        cmap=segm.cmap(random_state=12345))
        ax[0][1].set_title('Segmentation Map')
        ax[1][0].imshow(bkg.background, origin='lower')
        if not isinstance(threshold, float):
            ax[1][1].imshow(threshold, origin='lower')
    return tbl, segm
Beispiel #18
0
    def detect(self, DetectionImage, CutoutImages, threshold, npixels):

        SegmentationImage = detect_sources(DetectionImage.sig,
                                           threshold,
                                           npixels=npixels)

        if type(SegmentationImage) is not type(None):

            print('HERE')

            DeblendedSegmentationImage = deblend_sources(DetectionImage.sig,
                                                         SegmentationImage,
                                                         npixels=npixels,
                                                         nlevels=32)
            AllSourceProperties = source_properties(
                DetectionImage.sig, DeblendedSegmentationImage)

            Cat = AllSourceProperties.to_table()
            x, y = Cat['xcentroid'].value, Cat['ycentroid'].value
            r = np.sqrt((x - self.CutoutWidth / 2)**2 +
                        (y - self.CutoutWidth / 2)**2)
            tol = 2.  # pixels, impossible for more than one source I think, but at this tolerance the source could be shifted.
            s = r < tol

            if len(x[s]) == 1:

                detected = True
                idx = np.where(s == True)[0][0]
                SourceProperties = AllSourceProperties[idx]
                DetectionProperties, Mask, ExclusionMask = FLARE.obs.photometry.measure_core_properties(
                    SourceProperties,
                    DetectionImage,
                    DeblendedSegmentationImage,
                    verbose=self.verbose)

                if self.verbose:
                    print()
                    print('-' * 10, 'Observed Properties')
                ObservedProperties = {
                    filter: FLARE.obs.photometry.measure_properties(
                        DetectionProperties,
                        CutoutImages[filter],
                        Mask,
                        ExclusionMask,
                        verbose=self.verbose)
                    for filter in self.Filters
                }

                return detected, DetectionProperties, Mask, ExclusionMask, ObservedProperties

            else:

                detected = False
                if self.verbose:
                    print(
                        '**** SOURCES DETECTED BUT NOT IN MIDDLE OF NO SOURCE DETECTED'
                    )

                return detected, None, None, None, None
        else:

            detected = False
            if self.verbose: print('**** NO SOURCES DETECTED AT ALL')

            return detected, None, None, None, None
def find_hits(out_filter, out_marked, out_dir, in1, dark_sub, desc):

    sigma = 3.0 * gaussian_fwhm_to_sigma  # FWHM = 3.
    kernel = Gaussian2DKernel(sigma, x_size=3, y_size=3)
    kernel.normalize()
    threshold = detect_threshold(dark_sub, snr=2.)
    segm = detect_sources(dark_sub, threshold, npixels=5, filter_kernel=kernel)

    hdu = fits.open(in1)

    exposure = hdu[0].header.get('EXPTIME')
    shot_time = hdu[0].header.get('DATE-OBS')
    others = []
    for i in metadata:
        others.append(hdu[0].header.get(i))

    hdu.close()

    #hdu = fits.open(in1)
    #hdu[0].data = segm
    #hdu.header['telescop'] = 'CREDO'
    #hdu.writeto(out_marked, overwrite=True)
    #hdu.close()

    thr1 = np.percentile(dark_sub, 25)
    #thr2 = np.max(dark_sub)
    thr2 = np.percentile(dark_sub, 99.999)

    save_as_png(dark_sub, thr1, thr2, out_filter + ".png")

    npixels = 5
    segm_deblend = deblend_sources(dark_sub, segm, npixels=npixels,
                    filter_kernel=kernel, nlevels=32,
                    contrast=0.001)

    cat = source_properties(segm, segm_deblend)
    r = 3.  # approximate isophotal extent

    dots_count = 0
    comet_count = 0
    worm_count = 0
    group_count = 0

    dots_area = 0
    comet_area = 0
    worm_area = 0
    group_area = 0

    groups_connections = {}

    for obj in cat:
        position = (obj.xcentroid.value, obj.ycentroid.value)
        x, y = position
        cropx, cropy = 60, 60
        startx = int(x - (cropx // 2))
        starty = int(y - (cropy // 2))
        #crop = dark_sub[10:60,10:60]
        crop = dark_sub[max(0, starty):min(starty + cropy, dark_sub.data.shape[1]), max(startx, 0):min(startx + cropx, dark_sub.data.shape[0])]
        area = obj.area.value

        fn = '%s/%04d-%04dx%04d' % (out_dir, int(area), int(position[0]), int(position[1]))
        fnc = '%s/class/%04d-%04dx%04d' % (out_dir, int(area), int(position[0]), int(position[1]))

        hdu = fits.open(in1)
        hdu[0].data = crop
        hdu.writeto(fn + '.fits', overwrite=True)
        hdu.close()

        clsasse = 'dot'
        group = False
        if obj.ellipticity.value >= 0.6:
            clsasse = 'comet'
            comet_area += obj.area.value
            comet_count += 1
        elif obj.ellipticity.value >= 0.2:
            clsasse = 'worm'
            worm_area += obj.area.value
            worm_count += 1
        else:
            dots_area += obj.area.value
            dots_count += 1

        for i in cat:
            xx = i.xcentroid.value
            yy = i.ycentroid.value
            if pow(xx - x, 2) + pow(yy - y, 2) < pow(30, 2) and i.id != obj.id:
                if not group:
                    group = True
                    group_area += obj.area.value
                    group_count += 1
                gc = groups_connections.get(obj.id, [])
                gc.append(i.id)
                groups_connections[obj.id] = gc

        suffix = "-%s-%s-%.3f-%.3f-%.3f.png" % (clsasse, 'g' if group else 'n', obj.ellipticity.value, obj.elongation.value, obj.eccentricity.value)

        save_as_png(crop, thr1, thr2, fn + suffix)

        #cl = segm.data[int(obj.ymin.value):int(obj.ymax.value), int(obj.xmin.value):int(obj.xmax.value)]
        save_as_png(obj.data_cutout, np.min(obj.data_cutout), np.max(obj.data_cutout), fnc + suffix)
        #rotated = imrotate(obj.data_cutout, degrees(obj.orientation.value))
        #save_as_png(rotated, np.min(rotated), np.max(rotated), fnc + "-rotated.png")

        #if obj.area.value >= 39:
        #    print('big')

        #a = obj.semimajor_axis_sigma.value * r
        #b = obj.semiminor_axis_sigma.value * r
        #theta = obj.orientation.value
        #print("x: %d, y: %d" % (int(position[0]), int(position[1])))
        #apertures.append(EllipticalAperture(position, a, b, theta=theta))
    groups_count, groups_assigns = analyse_groups(groups_connections)

    count = dots_count + comet_count + worm_count
    area = dots_area + comet_area + worm_area
    meta = '\t'.join(map(str, others))
    print('%s\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%f\t%s\t# %s' % (desc, dots_count, dots_area, comet_count, comet_area, worm_count, worm_area, count, area, group_count, group_area, groups_count, exposure, shot_time, meta))
Beispiel #20
0
def mask_obj(img, snr=3.0, exp_sz= 1.2, plt_show = True):
    threshold = detect_threshold(img, snr=snr)
    center_img = len(img)/2
    sigma = 3.0 * gaussian_fwhm_to_sigma# FWHM = 3.
    kernel = Gaussian2DKernel(sigma, x_size=5, y_size=5)
    kernel.normalize()
    segm = detect_sources(img, threshold, npixels=10, filter_kernel=kernel)
    npixels = 20
    segm_deblend = deblend_sources(img, segm, npixels=npixels,
                                    filter_kernel=kernel, nlevels=15,
                                    contrast=0.001)
    #Number of objects segm_deblend.data.max()
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12.5, 10))
    import copy, matplotlib
    my_cmap = copy.copy(matplotlib.cm.get_cmap('gist_heat')) # copy the default cmap
    my_cmap.set_bad('black')
    vmin = 1.e-3
    vmax = 2.1 
    ax1.imshow(img, origin='lower', cmap=my_cmap, norm=LogNorm(), vmin=vmin, vmax=vmax)
    ax1.set_title('Data')
    ax2.imshow(segm_deblend, origin='lower', cmap=segm_deblend.cmap(random_state=12345))
    ax2.set_title('Segmentation Image')
    plt.close()
    columns = ['id', 'xcentroid', 'ycentroid', 'source_sum', 'area']
    cat = source_properties(img, segm_deblend)
    tbl = cat.to_table(columns=columns)
    tbl['xcentroid'].info.format = '.2f'  # optional format
    tbl['ycentroid'].info.format = '.2f'
    print(tbl)
    
    from photutils import EllipticalAperture
    cat = source_properties(img, segm_deblend)
    segm_deblend_size = segm_deblend.areas
    apertures = []
    for obj in cat:
        size = segm_deblend_size[obj.id]
        print 'obj.id', obj.id
        position = (obj.xcentroid.value, obj.ycentroid.value)
        a_o = obj.semimajor_axis_sigma.value
        b_o = obj.semiminor_axis_sigma.value
        size_o = np.pi * a_o * b_o
        r = np.sqrt(size/size_o)*exp_sz
        a, b = a_o*r, b_o*r
        theta = obj.orientation.value
        apertures.append(EllipticalAperture(position, a, b, theta=theta))
    
    dis_sq = [np.sqrt((apertures[i].positions[0][0]-center_img)**2+(apertures[i].positions[0][1]-center_img)**2) for i in range(len(apertures))]
    dis_sq = np.asarray(dis_sq)
    c_index= np.where(dis_sq == dis_sq.min())[0][0]
    #from astropy.visualization.mpl_normalize import ImageNormalize
    #norm = ImageNormalize(stretch=SqrtStretch())
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12.5, 10))
    ax1.imshow(img, origin='lower', cmap=my_cmap, norm=LogNorm(), vmin=vmin, vmax=vmax)
    ax1.set_title('Data')
    ax2.imshow(segm_deblend, origin='lower',
               cmap=segm_deblend.cmap(random_state=12345))
    ax2.set_title('Segmentation Image')
    for i in range(len(apertures)):
        aperture = apertures[i]
        aperture.plot(color='white', lw=1.5, ax=ax1)
        aperture.plot(color='white', lw=1.5, ax=ax2)
    if plt_show == True:
        plt.show()
    else:
        plt.close()
    
    from regions import PixCoord, EllipsePixelRegion
    from astropy.coordinates import Angle
    
    obj_masks = []  # In the script, the objects are 1, emptys are 0.
    for i in range(len(apertures)):
        aperture = apertures[i]
        x, y = aperture.positions[0]
        center = PixCoord(x=x, y=y)
        theta = Angle(aperture.theta/np.pi*180.,'deg')
        reg = EllipsePixelRegion(center=center, width=aperture.a*2, height=aperture.b*2, angle=theta)
        patch = reg.as_artist(facecolor='none', edgecolor='red', lw=2)
        fig, axi = plt.subplots(1, 1, figsize=(10, 12.5))
        axi.add_patch(patch)
        mask_set = reg.to_mask(mode='center')
        mask = mask_set.to_image((len(img),len(img)))
        axi.imshow(mask, origin='lower')
        plt.close()
        obj_masks.append(mask)
    return obj_masks
Beispiel #21
0
            # img = convolve_fft(img, kernel)

            # img[img < 10**21] = 0

            # threshold = phut.detect_threshold(img, nsigma=5)
            threshold = 10**20

            try:
                segm = phut.detect_sources(img,
                                           threshold,
                                           npixels=10,
                                           filter_kernel=kernel)
                segm = phut.deblend_sources(img,
                                            segm,
                                            npixels=10,
                                            filter_kernel=kernel,
                                            nlevels=8,
                                            contrast=0.1)
            except TypeError:
                continue
            # x_cent = []
            # y_cent = []
            # for i in range(1, np.max(segm.data) + 1):
            #     test_img = img
            #     test_img[segm.data != i] = 0.0
            #     tbl = find_peaks(test_img, threshold, box_size=5)
            #     print(tbl)
            #     x_cent.append((tbl["x_peak"] - 0.5 - (img.shape[0] / 2.)) * csoft)
            #     y_cent.append((tbl["y_peak"] - 0.5 - (img.shape[0] / 2.)) * csoft)

            for i in range(np.max(segm.data + 1)):
Beispiel #22
0
def make_mask_map_dual(image,
                       stars,
                       xx=None,
                       yy=None,
                       by='aper',
                       pad=0,
                       r_core=24,
                       r_out=None,
                       count=None,
                       seg_base=None,
                       n_bright=25,
                       sn_thre=3,
                       nlevels=64,
                       contrast=0.001,
                       npix=4,
                       b_size=64):
    """ Make mask map in dual mode: for faint stars, mask with S/N > sn_thre;
    for bright stars, mask core (r < r_core pix) """
    from photutils import detect_sources, deblend_sources
    from photutils.segmentation import SegmentationImage

    if (xx is None) | (yy is None):
        yy, xx = np.mgrid[:image.shape[0] + 2 * pad, :image.shape[1] + 2 * pad]

    star_pos = stars.star_pos_bright + pad

    if by == 'aper':
        if len(np.unique(r_core)) == 1:
            r_core_A, r_core_B = r_core, r_core
            r_core_s = np.ones(len(star_pos)) * r_core
        else:
            r_core_A, r_core_B = r_core[:2]
            r_core_s = np.array([
                r_core_A if F >= stars.F_verybright else r_core_B
                for F in stars.Flux_bright
            ])

        if r_out is not None:
            if len(np.unique(r_out)) == 1:
                r_out_A, r_out_B = r_out, r_out
                r_out_s = np.ones(len(star_pos)) * r_out_s
            else:
                r_out_A, r_out_B = r_out[:2]
                r_out_s = np.array([
                    r_out_A if F >= stars.F_verybright else r_out_B
                    for F in stars.Flux_bright
                ])
            print("Mask outer regions: r > %d (%d) pix " % (r_out_A, r_out_B))

    if sn_thre is not None:
        print("Detect and deblend source... Mask S/N > %.1f" % (sn_thre))
        # detect all source first
        back, back_rms = background_extraction(image, b_size=b_size)
        threshold = back + (sn_thre * back_rms)
        segm0 = detect_sources(image, threshold, npixels=npix)

        # deblend source
        segm_deb = deblend_sources(image,
                                   segm0,
                                   npixels=npix,
                                   nlevels=nlevels,
                                   contrast=contrast)

        #     for pos in star_pos:
        #         if (min(pos[0],pos[1]) > 0) & (pos[0] < image.shape[0]) & (pos[1] < image.shape[1]):
        #             star_lab = segmap[coord_Im2Array(pos[0], pos[1])]
        #             segm_deb.remove_label(star_lab)

        segmap = segm_deb.data.copy()
        max_lab = segm_deb.max_label

        # remove S/N mask map for input (bright) stars
        for pos in star_pos:
            rr2 = (xx - pos[0])**2 + (yy - pos[1])**2
            lab = segmap[np.where(rr2 == np.min(rr2))][0]
            segmap[segmap == lab] = 0

    if seg_base is not None:
        segmap2 = seg_base

        if sn_thre is not None:
            # Combine Two mask
            segmap[segmap2 > n_bright] = max_lab + segmap2[segmap2 > n_bright]
            segm_deb = SegmentationImage(segmap)
        else:
            # Only use seg_base, bright stars are aggressively masked
            segm_deb = SegmentationImage(segmap2)

        max_lab = segm_deb.max_label

    if by == 'aper':
        # mask core for bright stars out to given radii
        print("Mask core regions: r < %d (%d) pix " % (r_core_A, r_core_B))
        core_region = np.logical_or.reduce([
            np.sqrt((xx - pos[0])**2 + (yy - pos[1])**2) < r
            for (pos, r) in zip(star_pos, r_core_s)
        ])
        mask_star = core_region.copy()

        if r_out is not None:
            # mask outer region for bright stars out to given radii
            outskirt = np.logical_and.reduce([
                np.sqrt((xx - pos[0])**2 + (yy - pos[1])**2) > r
                for (pos, r) in zip(star_pos, r_out_s)
            ])
            mask_star = (mask_star) | (outskirt)

    elif by == 'brightness':
        # If count is not given, use 5 sigma above background.
        if count is None:
            count = np.mean(back + (5 * back_rms))
        # mask core for bright stars below given ADU count
        print("Mask core regions: Count > %.2f ADU " % count)
        mask_star = image >= count

    segmap[mask_star] = max_lab + 1

    # set dilation border a different label (for visual)
    segmap[(segmap != 0) & (segm_deb.data == 0)] = max_lab + 2

    # set mask map
    mask_deep = (segmap != 0)

    return mask_deep, segmap
Beispiel #23
0
# print(f'total number of sources in original map: {np.max(segm.data)}') # also works

# The segmentation image has the same dimensions as the input image. Each pixel in the segmentation image has an integer value. If $p_{i,j}=0$ this means that pixel isn't associated with a source. If $p_{i,j}>0$ that pixel is part of an object. Using imshow on the segmentation map will automatically colour each image by a different colour.

import matplotlib.pyplot as plt


fig = plt.figure(figsize = (1, 1), dpi = segm.data.shape[0])
ax = fig.add_axes((0.0, 0.0, 1.0, 1.0)) # define axes to cover entire field
ax.axis('off') # turn off axes frame, ticks, and labels
ax.imshow(segm, cmap = 'rainbow')
plt.show()
fig.savefig('segm.png')


# If two sources overlap simple segmentation can merge them together. This can be over-come using de-blending

from photutils import deblend_sources

segm_deblend = deblend_sources(sig, segm, npixels=npixels, nlevels=32, contrast=0.001)

print(f'total number of sources in debelended map: {segm_deblend.max_label}')


fig = plt.figure(figsize = (1, 1), dpi = segm_deblend.data.shape[0])
ax = fig.add_axes((0.0, 0.0, 1.0, 1.0)) # define axes to cover entire field
ax.axis('off') # turn off axes frame, ticks, and labels
ax.imshow(segm_deblend, cmap = 'rainbow')
plt.show()
fig.savefig('segm_deblend.png')
Beispiel #24
0
    def make_segmentation(self,
                          npixels=None,
                          nlevels=None,
                          contrast=None,
                          do_mask=None,
                          do_kernel=None,
                          do_deblend=None):
        """
        make a segmentation map given a threshold array (from Segmentation.make_thr), a kernel (from Segmentation.make_kernel), and a mask (from DQMask.make_mask).
        deblending of sources is implemented.
        ----------
        Ref:
            - Image Segmentation: https://photutils.readthedocs.io/en/stable/segmentation.html
        """
        from photutils import detect_sources, deblend_sources
        if npixels:
            self.segmentation.params.npixels = npixels
        if not self.segmentation.params.npixels:
            self.segmentation.params.npixels = 5
        if nlevels:
            self.segmentation.params.nlevels = nlevels
        if not self.segmentation.params.nlevels:
            self.segmentation.params.nlevels = 32
        if contrast:
            self.segmentation.params.contrast = contrast
        if not self.segmentation.params.contrast:
            self.segmentation.params.contrast = 1e-3
        if do_mask:
            self.segmentation.params.do_mask = do_mask
        if not self.segmentation.params.do_mask:
            self.segmentation.params.do_mask = False
        if do_kernel:
            self.segmentation.params.do_kernel = do_kernel
        if not self.segmentation.params.do_kernel:
            self.segmentation.params.do_kernel = False
        if do_deblend:
            self.segmentation.params.do_deblend = do_deblend
        if not self.segmentation.params.do_deblend:
            self.segmentation.params.do_deblend = False

        data = self.data
        thr = self.detect_thr.value
        npixels = self.segmentation.params.npixels
        nlevels = self.segmentation.params.nlevels
        contrast = self.segmentation.params.contrast
        mask = None
        if self.segmentation.params.do_mask:
            mask = ~self.mask.mask
        kernel = None
        if self.segmentation.params.do_kernel:
            kernel = self.kernel.value

        self.segmentation.value = detect_sources(data,
                                                 thr,
                                                 npixels=npixels,
                                                 filter_kernel=kernel,
                                                 mask=mask)
        if self.segmentation.params.do_deblend:
            self.segmentation.value = deblend_sources(data,
                                                      self.segmentation.value,
                                                      npixels=npixels,
                                                      filter_kernel=kernel,
                                                      nlevels=nlevels,
                                                      contrast=contrast)
Beispiel #25
0
def mask_cutout(cutout, nsigma=1., gauss_width=2.0, npixels=5):
    """ Masks a cutout using segmentation and deblending using watershed"""
    mask_data = {}

    # Generate a copy of the cutout just to prevent any weirdness with numpy pointers
    cutout_copy = copy(cutout)

    sigma = gauss_width * gaussian_fwhm_to_sigma
    kernel = Gaussian2DKernel(sigma)
    kernel.normalize()

    # Find threshold for cutout, and make segmentation map
    threshold = detect_threshold(cutout, snr=nsigma)
    segments = detect_sources(cutout,
                              threshold,
                              npixels=npixels,
                              filter_kernel=kernel)

    # Attempt to deblend. Return original segments upon failure.
    try:
        deb_segments = deblend_sources(cutout,
                                       segments,
                                       npixels=5,
                                       filter_kernel=kernel)
    except ImportError:
        print("Skimage not working!")
        deb_segments = segments
    except:
        # Don't do anything if it doesn't work
        deb_segments = segments

    segment_array = deb_segments.data

    # Center pixel values. (Assume that the central segment is the image, which is should be)
    c_x, c_y = floor(segment_array.shape[0] / 2), floor(
        segment_array.shape[1] / 2)
    central = segment_array[int(c_x)][int(c_y)]

    # Estimate background with severe cutout
    bg_method = 1
    bg_est, bg_rms = estimate_background(cutout_copy)

    mask_data["BG_EST"] = bg_est
    mask_data["BG_RMS"] = bg_rms
    mask_data["N_OBJS"] = segments.nlabels
    mask_data["BGMETHOD"] = bg_method

    # Use alternative method to try and get a estimate or rms value if first method fails
    if isnan(bg_est) or isnan(bg_rms):
        bg_pixel_array = []
        for x in range(0, segment_array.shape[0]):
            for y in range(0, segment_array.shape[1]):
                if segment_array[x][y] == 0:
                    bg_pixel_array.append(cutout_copy[x][y])

        bg_est = median(bg_pixel_array)
        bg_rms = sqrt((mean(bg_pixel_array) - bg_est)**2)

        bg_method = 2

    # Return input image if no need to mask
    if segments.nlabels == 1:
        mask_data["N_MASKED"] = 0
        return cutout_copy, mask_data

    num_masked = 0
    # Mask pixels
    for x in range(0, segment_array.shape[0]):
        for y in range(0, segment_array.shape[1]):
            if segment_array[x][y] not in (0, central):
                cutout_copy[x][y] = bg_est
                num_masked += 1

    mask_data["N_MASKED"] = num_masked

    return cutout_copy, mask_data
Beispiel #26
0
def circlesym(datadir,
              filname,
              output,
              method='median',
              box=451,
              data=None,
              save=True,
              **kwargs):
    '''
    finds center of circular symmetry of median combinations of registered images, and shifts this center to the center of the image cube

    INPUTS:
    datadir:str -  directory to location of file
    filname:str - name of FITS file
    output:str - location and name to save outputted aligned FITS image
    method:str - (median/individual) method to determine circular symmetry.  Use either a median image of cube or find circular symmetry for each individual frame in the cube / optional
    **kwargs:
        center_only:boolean (T/F) - use center 1.3" to find circular symmetry
        mask:boolean (T/F) - create mask and use it to cover oversaturated pixels
        rmax:float - maximum radius to search for circular symmetry

    OUTPUT:
    Data_cent:ndarray - aligned image cube

    '''
    print('running circular symmetry on: ', filname)
    if data is not None:
        Data = data
        hdr = fits.getheader(datadir + filname)
    else:
        Data = fits.getdata(datadir + filname)
        hdr = fits.getheader(datadir + filname)

    if len(Data.shape) > 2:
        segm = detect_sources(Data[3, :, :], 100, npixels=35)
        cat = source_properties(Data[3, :, :], segm)
    else:
        segm = detect_sources(Data, 10, npixels=35)
        cat = source_properties(Data, segm)

    xind_cen, yind_cen = int(cat[0].xcentroid.value), int(
        cat[0].ycentroid.value)

    box_size = box
    radius_size = int(box_size // 2)

    if xind_cen < radius_size:
        xind_cen = radius_size + 1
    if yind_cen < radius_size:
        yind_cen = radius_size + 1

    print(xind_cen, yind_cen, radius_size)

    Data = Data[:, yind_cen - radius_size:yind_cen + radius_size,
                xind_cen - radius_size:xind_cen + radius_size]
    print('dimensions of Data:', Data.shape)

    if len(Data.shape) > 2:
        Datamed = np.nanmedian(Data[3:-1], axis=0)
    else:
        Datamed = Data

    if 'center_only' in kwargs:
        centerrad = kwargs['center_only']
        box_size = int(centerrad)
        radius_size = box_size // 2
        cenx, ceny = int(Datamed.shape[1] / 2), int(Datamed.shape[0] / 2)
        Data_circsym = Data[:, ceny - radius_size:ceny + radius_size,
                            cenx - radius_size:cenx + radius_size]

        Datamed_circsym = Datamed[ceny - radius_size:ceny + radius_size,
                                  cenx - radius_size:cenx + radius_size]
    else:
        Data_circsym = Data
        Datamed_circsym = Datamed

    dimy = Datamed_circsym.shape[0]  ## y
    dimx = Datamed_circsym.shape[1]  ## x

    xr = np.arange(dimx / 2 + 1.0) - dimx / 4
    yr = np.arange(dimy / 2 + 1.0) - dimy / 4

    if 'rmax' in kwargs:
        lim = kwargs['rmax']
    else:
        lim = dimx / 2.  # xx limit

    Data_xc = np.array([])
    Data_yc = np.array([])
    if kwargs.get('mask'):
        mask_choice = 'True'
        if method == 'individual':
            for j in np.arange(len(Data_circsym)):
                print('constructing mask for oversaturated pixels for image',
                      j + 1, '/', len(Data_circsym))
                xs, ys = np.shape(Data_circsym[j, :, :])[1], np.shape(
                    Data_circsym[j, :, :])[0]
                segm = detect_sources(Data_circsym[j, :, :], 12, npixels=10)
                segm_deblend = deblend_sources(Datamed_circsym,
                                               segm,
                                               npixels=10,
                                               nlevels=30,
                                               contrast=0.001)
                cat = source_properties(Data_circsym[j, :, :], segm)

                cenx, ceny = cat.xcentroid, cat.ycentroid
                radius = cat.equivalent_radius.value
                mask = makeMask(xs, ys, radius, cenx, ceny)
                print(
                    'calculating center of circular symmetry for median line image with mask'
                )
                Dxc, Dyc = center_circlesym(Data_circsym[j, :, :], xr, yr, lim,
                                            mask)
                Data_xc = np.append(Data_xc, Dxc)
                Data_yc = np.append(Data_yc, Dyc)

        elif method == 'median':
            print('constructing mask for oversaturated pixels for image')
            xs, ys = np.shape(Datamed_circsym)[1], np.shape(Datamed_circsym)[0]
            segm = detect_sources(Datamed_circsym, 5, npixels=5)
            segm_deblend = segm  #deblend_sources(Datamed_circsym, segm, npixels=10, nlevels=15, contrast=0.001)
            cat = source_properties(Datamed_circsym, segm_deblend)
            cenx, ceny = cat.xcentroid, cat.ycentroid
            radius = cat.equivalent_radius.value
            mask = makeMask(xs, ys, radius, cenx, ceny)

            print(
                'calculating center of circular symmetry for median line image with mask'
            )
            Dxc, Dyc = center_circlesym(Datamed_circsym, xr, yr, lim, mask)
            Data_xc = np.append(Data_xc, Dxc)
            Data_yc = np.append(Data_yc, Dyc)
        else:
            return ValueError('chose method: individual images or median')

    else:
        mask_choice = 'False'
        if method == 'individual':
            for j in np.arange(len(Data_circsym)):
                print(
                    'calculating center of circular symmetry for median line image'
                )
                Dxc, Dyc = center_circlesym(Data_circsym[j, :, :], xr, yr, lim)
                Data_xc = np.append(Data_xc, Dxc)
                Data_yc = np.append(Data_yc, Dyc)
        elif method == 'median':
            Dxc, Dyc = center_circlesym(Datamed_circsym, xr, yr, lim)
            Data_xc = np.append(Data_xc, Dxc)
            Data_yc = np.append(Data_yc, Dyc)

        else:
            return ValueError('chose method: individual images or median')

    print()
    Data_xc_shift = ((dimx - 1) / 2.) - Data_xc
    Data_yc_shift = ((dimy - 1) / 2.) - Data_yc

    print('median center of circular symmetry is: ', np.median(Data_xc),
          np.median(Data_yc))
    print('median shift all images by: ', np.median(Data_xc_shift),
          np.median(Data_yc_shift))

    Data_cent = np.zeros(Data.shape)
    if len(Data.shape) > 2:
        nims = Data.shape[0]
        for i in np.arange(nims):
            if len(Data_yc_shift) == nims:
                temp = sci.shift(Data[i, :, :],
                                 (Data_yc_shift[i], Data_xc_shift[i]))
            else:
                temp = sci.shift(Data[i, :, :],
                                 (Data_yc_shift[0], Data_xc_shift[0]))
            Data_cent[i, :, :] = temp

    else:
        Data_cent = sci.shift(Data, (Data_yc_shift[0], Data_xc_shift[0]))

    print()
    print('----------- o -------------')
    print()

    if save is True:
        print(f'writing centered image cube: {output}')
        hdr.append(('COMMENT',
                    f'aligned with method: {method} using mask:{mask_choice}'),
                   end=True)
        fits.writeto(output, Data_cent, header=hdr, overwrite=True)
        return
    else:
        return Data_cent
Beispiel #27
0
def make_segment_img(data,
                     threshold,
                     npixels=5.0,
                     kernel=None,
                     mask=None,
                     deblend=False):
    """
    Detect sources in an image, including deblending.

    Parameters
    ----------
    data : 2D `~numpy.ndarray`
        The input 2D array.

    threshold : float
        The data value or pixel-wise data values to be used for the
        detection threshold. A 2D threshold must have the same shape as
        ``data``.

    npixels : int
        The number of connected pixels, each greater than ``threshold``
        that an object must have to be detected.  ``npixels`` must be a
        positive integer.

    kernel : `astropy.convolution.Kernel2D`
        The filtering kernel.  Filtering the image will smooth the noise
        and maximize detectability of objects with a shape similar to
        the kernel.

    mask : array_like of bool, optional
        A boolean mask, with the same shape as the input ``data``, where
        `True` values indicate masked pixels.  Masked pixels will not be
        included in any source.

    deblend : bool, optional
        Whether to deblend overlapping sources.  Source deblending
        requires scikit-image.

    Returns
    -------
    segment_image : `~photutils.segmentation.SegmentationImage` or `None`
        A 2D segmentation image, with the same shape as the input data,
        where sources are marked by different positive integer values.
        A value of zero is reserved for the background.  If no sources
        are found then `None` is returned.
    """
    connectivity = 8
    segm = detect_sources(data,
                          threshold,
                          npixels,
                          filter_kernel=kernel,
                          mask=mask,
                          connectivity=connectivity)

    # segm=None for photutils >= 0.7
    # segm.nlabels=0 for photutils < 0.7
    if segm is None or segm.nlabels == 0:
        return None

    # source deblending requires scikit-image
    if deblend:
        nlevels = 32
        contrast = 0.001
        mode = 'exponential'
        segm = deblend_sources(data,
                               segm,
                               npixels=npixels,
                               filter_kernel=kernel,
                               nlevels=nlevels,
                               contrast=contrast,
                               mode=mode,
                               connectivity=connectivity,
                               relabel=True)

    return segm
def extract_sources(img,
                    dqmask=None,
                    fwhm=3.0,
                    threshold=None,
                    source_box=7,
                    classify=True,
                    centering_mode="starfind",
                    nlargest=None,
                    outroot=None,
                    plot=False,
                    vmax=None,
                    deblend=False):
    """Use photutils to find sources in image based on segmentation.

    Parameters
    ----------
    img : ndarray
        Numpy array of the science extension from the observations FITS file.
    dqmask : ndarray
        Bitmask which identifies whether a pixel should be used (1) in source
        identification or not(0). If provided, this mask will be applied to the
        input array prior to source identification.
    fwhm : float
        Full-width half-maximum (fwhm) of the PSF in pixels.
    threshold : float or None
        Value from the image which serves as the limit for determining sources.
        If None, compute a default value of (background+5*rms(background)).
        If threshold < 0.0, use absolute value as scaling factor for default value.
    source_box : int
        Size of box (in pixels) which defines the minimum size of a valid source.
    classify : bool
        Specify whether or not to apply classification based on invarient moments
        of each source to determine whether or not a source is likely to be a
        cosmic-ray, and not include those sources in the final catalog.
    centering_mode : str
        "segmentaton" or "starfind"
        Algorithm to use when computing the positions of the detected sources.
        Centering will only take place after `threshold` has been determined, and
        sources are identified using segmentation.  Centering using `segmentation`
        will rely on `photutils.segmentation.source_properties` to generate the
        properties for the source catalog.  Centering using `starfind` will use
        `photutils.IRAFStarFinder` to characterize each source in the catalog.
    nlargest : int, None
        Number of largest (brightest) sources in each chip/array to measure
        when using 'starfind' mode.
    outroot : str, optional
        If specified, write out the catalog of sources to the file with this name rootname.
    plot : bool, optional
        Specify whether or not to create a plot of the sources on a view of the image.
    vmax : float, optional
        If plotting the sources, scale the image to this maximum value.
    deblend : bool, optional
        Specify whether or not to apply photutils deblending algorithm when
        evaluating each of the identified segments (sources) from the chip.
    """
    # apply any provided dqmask for segmentation only
    if dqmask is not None:
        imgarr = img.copy()
        imgarr[dqmask] = 0
    else:
        imgarr = img

    bkg_estimator = MedianBackground()
    bkg = None

    exclude_percentiles = [10, 25, 50, 75]
    for percentile in exclude_percentiles:
        try:
            bkg = Background2D(imgarr, (50, 50),
                               filter_size=(3, 3),
                               bkg_estimator=bkg_estimator,
                               exclude_percentile=percentile)
        except Exception:
            bkg = None
            continue

        if bkg is not None:
            # If it succeeds, stop and use that value
            bkg_rms = (5. * bkg.background_rms)
            bkg_rms_mean = bkg.background.mean() + 5. * bkg_rms.std()
            default_threshold = bkg.background + bkg_rms
            if threshold is None:
                threshold = default_threshold
            elif threshold < 0:
                threshold = -1 * threshold * default_threshold
                log.info("{} based on {}".format(threshold.max(),
                                                 default_threshold.max()))
                bkg_rms_mean = threshold.max()
            else:
                bkg_rms_mean = 3. * threshold

            if bkg_rms_mean < 0:
                bkg_rms_mean = 0.
            break

    # If Background2D does not work at all, define default scalar values for
    # the background to be used in source identification
    if bkg is None:
        bkg_rms_mean = max(0.01, imgarr.min())
        bkg_rms = bkg_rms_mean * 5

    sigma = fwhm * gaussian_fwhm_to_sigma
    kernel = Gaussian2DKernel(sigma, x_size=source_box, y_size=source_box)
    kernel.normalize()
    segm = detect_sources(imgarr,
                          threshold,
                          npixels=source_box,
                          filter_kernel=kernel)
    # photutils >= 0.7: segm=None; photutils < 0.7: segm.nlabels=0
    if segm is None or segm.nlabels == 0:
        log.info("No detected sources!")
        return None, None

    if deblend:
        segm = deblend_sources(imgarr,
                               segm,
                               npixels=5,
                               filter_kernel=kernel,
                               nlevels=16,
                               contrast=0.01)
    # If classify is turned on, it should modify the segmentation map
    if classify:
        cat = source_properties(imgarr, segm)
        # Remove likely cosmic-rays based on central_moments classification
        bad_srcs = np.where(classify_sources(cat) == 0)[0] + 1

        if LooseVersion(photutils.__version__) >= '0.7':
            segm.remove_labels(bad_srcs)
        else:
            # this is the photutils >= 0.7 fast code for removing labels
            segm.check_labels(bad_srcs)
            bad_srcs = np.atleast_1d(bad_srcs)
            if len(bad_srcs) != 0:
                idx = np.zeros(segm.max_label + 1, dtype=int)
                idx[segm.labels] = segm.labels
                idx[bad_srcs] = 0
                segm.data = idx[segm.data]

    # convert segm to mask for daofind
    if centering_mode == 'starfind':
        src_table = None
        # daofind = IRAFStarFinder(fwhm=fwhm, threshold=5.*bkg.background_rms_median)
        log.info("Setting up DAOStarFinder with: \n    fwhm={}  threshold={}".
                 format(fwhm, bkg_rms_mean))
        daofind = DAOStarFinder(fwhm=fwhm, threshold=bkg_rms_mean)

        # Identify nbrightest/largest sources
        if nlargest is not None:
            nlargest = min(nlargest, len(segm.labels))
            if LooseVersion(photutils.__version__) >= '0.7':
                large_labels = segm.labels[np.flip(np.argsort(
                    segm.areas))[:nlargest]]
            else:
                # for photutils < 0.7
                areas = np.array([
                    area for area in np.bincount(segm.data.ravel())[1:]
                    if area != 0
                ])
                large_labels = segm.labels[np.flip(
                    np.argsort(areas))[:nlargest]]

        log.info("Looking for sources in {} segments".format(len(segm.labels)))

        for segment in segm.segments:
            # check needed for photutils <= 0.6; it can be removed when
            # the drizzlepac depends on photutils >= 0.7
            if segment is None:
                continue
            if nlargest is not None and segment.label not in large_labels:
                continue  # Move on to the next segment
            # Get slice definition for the segment with this label
            seg_slice = segment.slices
            seg_yoffset = seg_slice[0].start
            seg_xoffset = seg_slice[1].start

            # Define raw data from this slice
            detection_img = img[seg_slice]
            # zero out any pixels which do not have this segments label
            detection_img[segm.data[seg_slice] == 0] = 0

            # Detect sources in this specific segment
            seg_table = daofind.find_stars(detection_img)

            # Pick out brightest source only
            if src_table is None and seg_table:
                # Initialize final master source list catalog
                src_table = Table(
                    names=seg_table.colnames,
                    dtype=[dt[1] for dt in seg_table.dtype.descr])

            if seg_table and seg_table['peak'].max() == detection_img.max():
                max_row = np.where(
                    seg_table['peak'] == seg_table['peak'].max())[0][0]
                # Add row for detected source to master catalog
                # apply offset to slice to convert positions into full-frame coordinates
                seg_table['xcentroid'] += seg_xoffset
                seg_table['ycentroid'] += seg_yoffset
                src_table.add_row(seg_table[max_row])

    else:
        cat = source_properties(img, segm)
        src_table = cat.to_table()
        # Make column names consistent with IRAFStarFinder column names
        src_table.rename_column('source_sum', 'flux')
        src_table.rename_column('source_sum_err', 'flux_err')

    if src_table is not None:
        log.info("Total Number of detected sources: {}".format(len(src_table)))
    else:
        log.info("No detected sources!")
        return None, None

    # Move 'id' column from first to last position
    # Makes it consistent for remainder of code
    cnames = src_table.colnames
    cnames.append(cnames[0])
    del cnames[0]
    tbl = src_table[cnames]

    if outroot:
        tbl['xcentroid'].info.format = '.10f'  # optional format
        tbl['ycentroid'].info.format = '.10f'
        tbl['flux'].info.format = '.10f'
        if not outroot.endswith('.cat'):
            outroot += '.cat'
        tbl.write(outroot, format='ascii.commented_header')
        log.info("Wrote source catalog: {}".format(outroot))

    if plot and plt is not None:
        norm = len(segm.labels)
        if vmax is None:
            norm = ImageNormalize(stretch=SqrtStretch())
        fig, ax = plt.subplots(2, 2, figsize=(8, 8))
        ax[0][0].imshow(imgarr,
                        origin='lower',
                        cmap='Greys_r',
                        norm=norm,
                        vmax=vmax)
        ax[0][1].imshow(segm,
                        origin='lower',
                        cmap=segm.cmap(random_state=12345))
        ax[0][1].set_title('Segmentation Map')
        ax[1][0].imshow(bkg.background, origin='lower')
        if not isinstance(threshold, float):
            ax[1][1].imshow(threshold, origin='lower')
    return tbl, segm