def find_sources_via_segmentation(data, sigma=3, fwhm=2.0, min_pix=5, make_plot=False): """ """ yd, xd = data.shape # Let's define the background using boxes of ~50x50 pixels nboxx = int(xd / 150) nboxy = int(yd / 150) bkg_estimator = MedianBackground() bkg = Background2D(data, (nboxy, nboxx), filter_size=(3, 3), bkg_estimator=bkg_estimator) data -= bkg.background # subtract the background threshold = sigma * bkg.background_rms #threshold = detect_threshold(data, nsigma=sigma) gaussian_sigma = fwhm * gaussian_fwhm_to_sigma kernel = Gaussian2DKernel(gaussian_sigma, x_size=3, y_size=3) kernel.normalize(mode='integral') segm = detect_sources(data, threshold, npixels=min_pix, filter_kernel=kernel) segm_deblend = deblend_sources(data, segm, npixels=min_pix, filter_kernel=kernel, nlevels=32, contrast=0.001) if make_plot: norm = ImageNormalize(stretch=SqrtStretch()) fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 12.5)) ax1.imshow(data, origin='lower', cmap='Greys_r', norm=norm) ax1.set_title('Data') cmap = segm.make_cmap(seed=123) ax2.imshow(segm, origin='lower', cmap=cmap, interpolation='nearest') ax2.set_title('Segmentation Image') ax3.imshow(segm_deblend, origin='lower', cmap=cmap, interpolation='nearest') ax3.set_title('Deblended Segmentation Image') plt.show() #plt.save('testing.jpg') return data, segm_deblend, bkg
def search_input_position(cutout): sigma = 3.0 * gaussian_fwhm_to_sigma kernel = Gaussian2DKernel(sigma, x_size=3, y_size=3) kernel.normalize() threshold = detect_threshold(cutout.data, nsigma=2.) segm = detect_sources(cutout.data, threshold, npixels=5, filter_kernel=kernel) segm_deblend = deblend_sources(cutout.data, segm, npixels=5, filter_kernel=kernel, nlevels=5, contrast=0.001) idx = np.random.choice( np.arange(len((np.where(segm_deblend.data == 0)[0]))), 1) x = np.where(segm_deblend.data == 0)[0][idx][0] y = np.where(segm_deblend.data == 0)[0][idx][0] return cutout.wcs.pixel_to_world(x, y)
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) if segm is None: 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 find_saturation(self, frame, config, default_fwhm, star_mask=None): """ This function ... :param frame: :param config: :param default_fwhm: :param star_mask: :return: """ # Convert FWHM to sigma default_sigma = default_fwhm * statistics.fwhm_to_sigma # Determine the radius for the saturation detection model = self.psf_model radius = fitting.sigma( model ) * config.sigmas if model is not None else default_sigma * config.sigmas # Make sure the radius is never smaller than 4 pixels radius = max(radius, 4.) # Add a new stage to the track record #if self.has_track_record: self.track_record.set_stage("saturation") # Look for a center segment corresponding to a 'saturation' source radius_ellipse = PixelStretch(radius, radius) ellipse = PixelEllipseRegion(self.pixel_position(frame.wcs), radius_ellipse) # frame_star_erased = frame.copy() # frame_star_erased[self.source.y_slice, self.source.x_slice][self.source.mask] = 0.0 # saturation_source = sources.find_source_segmentation(frame, ellipse, config, track_record=self.track_record, special=self.special) # saturation_source = sources.find_source_segmentation(frame_star_erased, ellipse, config, track_record=self.track_record, special=self.special) mask_cutout = CutoutMask(self.detection.mask, self.detection.x_min, self.detection.x_max, self.detection.y_min, self.detection.y_max) track_record = None saturation_source = sources.find_source_segmentation( frame, ellipse, config, track_record=track_record, special=self.special) # Check if the found source segment is larger than the PSF source if saturation_source is not None: mask_saturation = CutoutMask(saturation_source.mask, saturation_source.x_min, saturation_source.x_max, saturation_source.y_min, saturation_source.y_max) mask_saturation_as_cutout = mask_saturation.as_cutout(mask_cutout) if self.detection.mask.covers(mask_saturation_as_cutout): saturation_source = None # If a 'saturation' source was found if saturation_source is not None: if self.special: log.debug("Initial saturation source found") x_min = saturation_source.x_min x_max = saturation_source.x_max y_min = saturation_source.y_min y_max = saturation_source.y_max # DEBLEND FIRST if config.deblend: import numpy as np from photutils.segmentation import deblend_sources # from astropy.convolution import Kernel2D # Kernel2D._model = self.psf_model # if self.psf_model is not None: # kernelsize = 2 * int(round(fitting.sigma(self.psf_model) * 3.)) # print("kernelsize", kernelsize) # kernel = Kernel2D(x_size=kernelsize) # else: kernel = None kernel = None segments = deblend_sources( saturation_source.cutout, saturation_source.mask.astype(int), npixels=config.deblending.min_npixels, contrast=config.deblending.contrast, mode=config.deblending.mode, nlevels=config.deblending.nlevels, filter_kernel=kernel) plotting.plot_box(segments) smallest_distance = None smallest_distance_mask = None for index in np.unique(segments)[1:]: where = segments == index fake_box = Cutout(where.astype(int), x_min, x_max, y_min, y_max) contour = sources.find_contour(fake_box, where, sigma_level=1) difference = contour.center - self.pixel_position( frame.wcs) distance = difference.norm if smallest_distance is None or distance < smallest_distance: smallest_distance = distance smallest_distance_mask = where # print(index, difference.norm) # SET NEW MASK saturation_source.mask = smallest_distance_mask # AFTER DEBLENDING, CALCULATE CONTOUR # Calculate the elliptical contour # contour = sources.find_contour(saturation_source.cutout, saturation_source.mask, config.apertures.sigma_level) contour = sources.find_contour( Cutout(saturation_source.mask.astype(int), x_min, x_max, y_min, y_max), saturation_source.mask, config.apertures.sigma_level ) # determine the segment properties of the actual mask segment # Check whether the source centroid matches the star position if config.check_centroid: if self.special: log.debug("Checking contour parameters ...") # Calculate the offset difference = contour.center - self.pixel_position(frame.wcs) star_mask_cutout = star_mask[saturation_source.cutout.y_slice, saturation_source.cutout.x_slice] # Remove the mask of this star from the star_mask_cutout x_min_cutout = saturation_source.cutout.x_min x_max_cutout = saturation_source.cutout.x_max y_min_cutout = saturation_source.cutout.y_min y_max_cutout = saturation_source.cutout.y_max x_min_source = self.detection.cutout.x_min x_max_source = self.detection.cutout.x_max y_min_source = self.detection.cutout.y_min y_max_source = self.detection.cutout.y_max try: # plotting.plot_box(star_mask_cutout, title="before removing central source") star_mask_cutout[y_min_source - y_min_cutout:y_max_source - y_min_cutout, x_min_source - x_min_cutout:x_max_source - x_min_cutout][self.detection.mask] = False # plotting.plot_box(star_mask_cutout, title="after removing central source") except IndexError: pass # plotting.plot_box(frame[saturation_source.y_slice, saturation_source.x_slice]) # plotting.plot_box(saturation_source.mask) # plotting.plot_box(star_mask_cutout) # print(star_mask_cutout.shape) # plotting.plot_box(saturation_source.cutout) # print(saturation_source.cutout.shape) # plotting.plot_box(self.source.mask) # print(self.source.mask.shape) # print(y_min_source, y_min_cutout, y_max_source, y_min_cutout, x_min_source, x_min_cutout, x_max_source, x_min_cutout) # print(y_min_source-y_min_cutout) # becomes negative! # print(y_max_source-y_min_cutout) # print(x_min_source-x_min_cutout) # becomes negative ! # print(x_max_source-x_min_cutout) # print(star_mask_cutout[y_min_source-y_min_cutout:y_max_source-y_min_cutout, x_min_source-x_min_cutout:x_max_source-x_min_cutout].shape) # source_mask_smaller = self.source.mask[y_min_cutout-y_min_source:,x_min_cutout-x_min_source] # star_mask_cutout[0:y_max_source-y_min_cutout][0:x_max_source-x_min_cutout][source_mask_smaller] = False # TODO: fix this problem ! (how can it be that the source box is not inside the saturation box??) # saturation sources are created by expanding the initial source box ?? # Discard this saturation source if the centroid offset or the ellipticity is too large if not masks.overlap(saturation_source.mask, star_mask_cutout): if self.special: log.debug("Checking offset and ellipticity") if difference.norm > config.max_centroid_offset or contour.ellipticity > config.max_centroid_ellipticity: if self.special: log.debug( "Found to large offset or ellipticity: not a saturation source" ) return else: if self.special: log.debug( "Saturation mask overlaps other stars, so contour parameters will not be checked" ) if config.second_segmentation: # Find all of the saturation light in a second segmentation step track_record = None saturation_source = sources.find_source_segmentation( frame, ellipse, config, track_record=track_record, special=self.special, sigma_level=config.second_sigma_level) # contour = sources.find_contour(saturation_source.cutout, saturation_source.mask, config.apertures.sigma_level) contour = sources.find_contour( saturation_source.mask.astype(int), saturation_source.mask, config.apertures.sigma_level ) # determine the segment properties of the actual mask segment # Check whether the source centroid matches the star position if config.check_centroid: # Calculate the offset difference = contour.center - self.pixel_position( frame.wcs) star_mask_cutout = star_mask[ saturation_source.cutout.y_slice, saturation_source.cutout.x_slice] # Remove the mask of this star from the star_mask_cutout x_min_cutout = saturation_source.cutout.x_min x_max_cutout = saturation_source.cutout.x_max y_min_cutout = saturation_source.cutout.y_min y_max_cutout = saturation_source.cutout.y_max x_min_source = self.detection.cutout.x_min x_max_source = self.detection.cutout.x_max y_min_source = self.detection.cutout.y_min y_max_source = self.detection.cutout.y_max # plotting.plot_box(star_mask_cutout, title="before removing central source") star_mask_cutout[y_min_source - y_min_cutout:y_max_source - y_min_cutout, x_min_source - x_min_cutout:x_max_source - x_min_cutout][self.detection.mask] = False # plotting.plot_box(star_mask_cutout, title="after removing central source") # Discard this saturation source if the centroid offset or the ellipticity is too large if not masks.overlap(saturation_source.mask, star_mask_cutout): if difference.norm > config.max_centroid_offset or contour.ellipticity > config.max_centroid_ellipticity: return # Replace the pixels of the cutout box by the pixels of the original frame (because the star itself is already removed) # saturation_source.cutout = frame.box_like(saturation_source.cutout) # TODO: check with classifier to verify this is actually a saturation source! if self.special: saturation_source.plot(title="Final saturation source") # Replace the source by a source that covers the saturation self.saturation = saturation_source self.contour = contour
def deblend_stars(image, ew_map=None, sigma_thresh=3., fwhm_pix=3., npixels=15, nlevels=32, contrast=0.01, size_thresh=2., dist_to_center_thresh=10., area_thresh=200., ew_thresh=20., output_dir='', save_fits=False): """ This method estimates foreground stars over a given image. Additional constrains can be applied by providing an EW(Ha) map, preventing giant HII regions to be classified as stars. --------------------------------------------------------------------------- input params: - image: (2D array) Flux image used to deblend the sources - ew_map (optional): (2D array) EW(Ha) map used to further constrain the sources - sigma_thresh (float, default=3.0): number of stddev for estimating the signal threshold - fwhm_pix (float, default=3.0): FWHM for Gaussian smoothing prior to source detection - npixels (int, default=15): The number of connected pixels, each greater than threshold, that an object must have to be detected. - nlevels (int, default=15): The number of multi-thresholding levels to use - contrast (float, default=0.01) - size_thres (float, defualt=2.) - - - output_dir (optional, defatult=''): (str) directory where the results will be saved - save_filts (optional, default=False): Set True for saving the stellar masks """ if ew_map is None: ew = np.zeros_like(image) rows, columns = image.shape centerx, centery = rows // 2, columns // 2 XX, YY = np.meshgrid(np.arange(columns), np.arange(rows)) # Estimating the image signal threshold threshold = detect_threshold(image, nsigma=sigma_thresh) # Kernel smoothing # sigma_pixels = FWHM_pixels / conversion_factor kernel_sigma = fwhm_pix / (2.0 * np.sqrt(2.0 * np.log(2.0))) kernel = Gaussian2DKernel(kernel_sigma, x_size=int(fwhm_pix), y_size=int(fwhm_pix)) kernel.normalize() # Source detection segm = detect_sources(image, threshold, npixels, filter_kernel=kernel) # Source deblending segm_deblend = deblend_sources(image, segm, npixels=npixels, filter_kernel=kernel, nlevels=nlevels, contrast=0.01) deblended_areas = segm_deblend.areas stellar_masks = [] if deblended_areas.size > 1: mask = segm_deblend.data cmap = segm_deblend.make_cmap() plt.figure() plt.imshow(segm_deblend, origin='lower', cmap=cmap) plt.colorbar() plt.contour(image, levels=20, colors='k', origin='lower') for ii in range(deblended_areas.size): plt.annotate(r'Pixel area: {}'.format(deblended_areas[ii]), xy=(.02, .95 - ii * 0.05), xycoords='axes fraction', ha='left', va='top', color=cmap(ii + 1), fontsize=9) plt.savefig(output_dir + 'deblending_map.png') plt.close() for ii in range(deblended_areas.size): source = mask == ii + 1 source_area = deblended_areas[ii] source_image = np.copy(image) source_image[~source] = 0 pos_x, pos_y = centroid_2dg(source_image) dist_to_center = np.sqrt((pos_x - centerx)**2 + (pos_y - centery)**2) amplitude_guess = source_image.max() initial_guess = [amplitude_guess, pos_x, pos_y, 3, 3, 0] try: popt, pcov = curve_fit( gaussian2d, [XX, YY], source_image.ravel(), p0=initial_guess, # bounds=(0, [amplitude_guess*2, 80, 80, 5, 5]) ) except: print('Error fitting gaussian to data') continue popt = np.abs(popt) sigmax, sigmay = popt[3], popt[4] gaussian = gaussian2d([XX, YY], *popt).reshape(image.shape) level = gaussian2d([ popt[1] + 3 * max([popt[3], 2]), popt[2] + 3 * max([popt[4], 2]) ], *popt) central_pixels = gaussian2d([popt[1] + sigmax, popt[2] + sigmay], *popt) star_mask = gaussian > level central_star_mask = gaussian > central_pixels ew_source = ew[central_star_mask] median_ew = np.nanmedian(ew_source) if np.isnan(median_ew): median_ew = 0. ellipticity = sigmax / sigmay # chi2 = np.sum(chi2[star_mask]) if (sigmax < size_thresh) & (sigmay < size_thresh): # &(ellipticity<1.5)&(ellipticity>0.5) if (dist_to_center < dist_to_center_thresh) & ( source_area < area_thresh) & (median_ew <= ew_thresh): is_star = True stellar_masks.append(star_mask) elif (dist_to_center > dist_to_center_thresh) & (median_ew <= ew_thresh): is_star = True stellar_masks.append(star_mask) else: is_star = False else: is_star = False plt.figure(figsize=(4, 4)) plt.subplot(221) plt.plot(XX[0, :], np.sum(source_image, axis=0), 'k') plt.plot(XX[0, :], np.sum(gaussian, axis=0), 'r') plt.annotate(r'$\sigma_x$={:5.3}'.format(sigmax), xy=(.1, .9), xycoords='axes fraction', ha='left', va='top') plt.subplot(224) plt.plot(np.sum(source_image, axis=1), YY[:, 0], 'k') plt.plot(np.sum(gaussian, axis=1), YY[:, 0], 'r') plt.annotate(r'$\sigma_y$={:5.3}'.format(sigmay), xy=(.9, .9), xycoords='axes fraction', ha='right', va='top') plt.subplot(223) plt.imshow(image, cmap='gist_earth', aspect='auto', origin='lower') if is_star: plt.plot(pos_x, pos_y, '*', c='lime', markersize=10) else: plt.plot(pos_x, pos_y, '+', c='lime', markersize=10) plt.contour(gaussian, colors='r', levels=level, linewidths=2) plt.annotate(r'$\sigma_x/\sigma_y$={:5.3}'.format(ellipticity), xy=(.05, .99), xycoords='axes fraction', ha='left', va='top', fontsize=8) plt.annotate(r'$EW(H\alpha)_{50}$' + '={:5.3}'.format(median_ew), xy=(.05, .90), xycoords='axes fraction', ha='left', va='top', fontsize=8) plt.savefig(output_dir + 'detection_' + str(ii) + '.png') plt.close() # plt.xlim(pos_x-15,pos_x+15) # plt.ylim(pos_y-15,pos_y+15) stellar_masks = np.array(stellar_masks) if stellar_masks.size > 0: print('-->', stellar_masks.shape[0], ' stars detected') total_mask = np.zeros_like(image, dtype=bool) for i in range(stellar_masks.shape[0]): total_mask = total_mask | np.array(stellar_masks[i, :, :], dtype=bool) if save_fits: fits_path = output_dir + 'stellar_mask.fits' hdr = fits.Header() hdr['COMMENT'] = "Stellar masks" image_list = [] image_list.append(fits.PrimaryHDU(header=hdr)) for ii in range(stellar_masks.shape[0]): image_list.append( fits.ImageHDU(np.array(stellar_masks[ii, :, :], dtype=int))) image_list.append(np.array(total_mask, dtype=int)) hdu = fits.HDUList(image_list) hdu.writeto(fits_path, overwrite=True) hdu.close() print('File saved as: ' + fits_path) return total_mask, stellar_masks.shape[0] else: return np.zeros_like(image, dtype=bool), 0