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)
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
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
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
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
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'))
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
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
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
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
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
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
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
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
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
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))
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
# 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)):
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
# 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')
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)
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
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
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