def convolve_projection(proj, newbeam, res_tol=0.0, min_coverage=0.8, append_raw=False, verbose=False, suppress_error=False): """ Convolve a 2D image to a specified beam. Very similar to `convolve_cube()`, but this function deals with 2D images (i.e., projections) rather than 3D cubes. Parameters ---------- proj : ~spectral_cube.Projection object Input 2D image newbeam : radio_beam.Beam object Target beam to convolve to res_tol : float, optional Tolerance on the difference between input/output resolution By default, a convolution is performed on the input image whenever its native resolution is different from (sharper than) the target resolution. Use this keyword to specify a tolerance on resolution, within which no convolution will be performed. For example, res_tol=0.1 will allow a 10% tolerance. min_coverage : float or None, optional When the convolution meets NaN values or edges, the output is calculated based on beam-weighted average. This keyword specifies the minimum beam covering fraction of valid (np.finite) values. All pixels with less beam covering fraction will be assigned NaNs. Default is 80% beam covering fraction (min_coverage=0.8). If the user would rather use the interpolation strategy in `astropy.convolution.convolve_fft`, set this keyword to None. Note that the NaN pixels will be kept as NaN. append_raw : bool, optional Whether to append the raw convolved image and weight image Default is not to append. verbose : bool, optional Whether to print the detailed processing information in terminal Default is to not print. suppress_error : bool, optional Whether to suppress the error when convolution is unsuccessful Default is to not suppress. Returns ------- outproj : Projection object or tuple Convolved 2D image (when append_raw=False), or a tuple comprising 3 images (when append_raw=True) """ from functools import partial from astropy.convolution import convolve_fft if min_coverage is None: # Skip coverage check and preserve NaN values. # This uses the default interpolation strategy # implemented in 'astropy.convolution.convolve_fft' convolve_func = partial(convolve_fft, preserve_nan=True, allow_huge=True, quiet=~verbose) else: # Do coverage check to determine the mask on the output convolve_func = partial(convolve_fft, nan_treatment='fill', boundary='fill', fill_value=0., allow_huge=True, quiet=~verbose) if (res_tol > 0) and (newbeam.major != newbeam.minor): raise ValueError("You cannot specify a non-zero resolution " "torelance if the target beam is not round") tol = newbeam.major * np.array([1-res_tol, 1+res_tol]) if ((tol[0] < proj.beam.major < tol[1]) and (tol[0] < proj.beam.minor < tol[1])): if verbose: print("Native resolution within tolerance - " "Copying original image...") my_append_raw = False newproj = proj.copy() else: if verbose: print("Convolving image...") try: convproj = proj.convolve_to(newbeam, convolve=convolve_func) if min_coverage is not None: # divide the raw convolved image by the weight image my_append_raw = True wtproj = Projection( np.isfinite(proj.data).astype('float'), wcs=proj.wcs, beam=proj.beam) wtproj = wtproj.convolve_to(newbeam, convolve=convolve_func) newproj = convproj / wtproj.hdu.data # mask all pixels w/ weight smaller than min_coverage threshold = min_coverage * u.dimensionless_unscaled newproj[wtproj < threshold] = np.nan else: my_append_raw = False newproj = convproj except ValueError as err: if suppress_error: return else: raise ValueError( "Unsuccessful convolution: {}\nOld: {}\nNew: {}" "".format(err, proj.beam, newbeam)) if append_raw and my_append_raw: return newproj, convproj, wtproj else: return newproj
def convolve_image(inimage, newbeam, mode='dataimage', res_tol=0.0, min_coverage=0.8, nan_treatment='fill', boundary='fill', fill_value=0., append_raw=False, verbose=False, suppress_error=False): """ Convolve a 2D image or an rms noise image to a specified beam. This function is similar to `convolve_cube()`, but it deals with 2D images (i.e., projections) rather than 3D cubes. Parameters ---------- inimage : FITS HDU object or ~spectral_cube.Projection object Input 2D image newbeam : radio_beam.Beam object Target beam to convolve to mode : {'dataimage', 'noiseimage'}, optional Whether the input image is a data image or an rms noise image. In the former case, a direct convolution is performed; in the latter case, the convolution attempts to mimic the error propagation process to the specified lower resolution. (Default: 'dataimage') res_tol : float, optional Tolerance on the difference between input/output resolution By default, a convolution is performed on the input image when its native resolution is different from (sharper than) the target resolution. Use this keyword to specify a tolerance on resolution, within which no convolution will be performed. For example, res_tol=0.1 will allow a 10% tolerance. min_coverage : float or None, optional This keyword specifies a minimum beam covering fraction of valid pixels for convolution (Default: 0.8). Locations with a beam covering fraction less than this value will be overwritten to "NaN" in the convolved cube. If the user would rather use the ``preserve_nan`` mode in `astropy.convolution.convolve_fft`, set this keyword to None. nan_treatment: {'interpolate', 'fill'}, optional To be passed to `astropy.convolution.convolve_fft`. (Default: 'fill') boundary: {'fill', 'wrap'}, optional To be passed to `astropy.convolution.convolve_fft`. (Default: 'fill') fill_value : float, optional To be passed to `astropy.convolution.convolve_fft`. (Default: 0) append_raw : bool, optional Whether to append the raw convolved image and weight image. Default is not to append. verbose : bool, optional Whether to print the detailed processing log in terminal. Default is to not print. suppress_error : bool, optional Whether to suppress the error message when convolution is unsuccessful. Default is to not suppress. Returns ------- outimage : FITS HDU objects or Projection objects Convolved 2D images (when append_raw=False), or a 3-tuple including a masked verson, an unmaked version, and a coverage fraction map (when append_raw=True). The output will be the same type of objects as the input. """ if isinstance(inimage, Projection): proj = inimage elif isinstance(inimage, (fits.PrimaryHDU, fits.ImageHDU)): proj = Projection.from_hdu(inimage) else: raise ValueError("`inimage` needs to be either a FITS HDU object " "or a spectral_cube.Projection object") if (res_tol > 0) and (newbeam.major != newbeam.minor): raise ValueError("Cannot handle a non-zero resolution torelance " "when the target beam is not round") if min_coverage is None: # Skip coverage check and preserve NaN values. # This uses the default 'preserve_nan' scheme # implemented in 'astropy.convolution.convolve_fft' convolve_func = partial(convolve_fft, fill_value=fill_value, nan_treatment=nan_treatment, boundary=boundary, preserve_nan=True, allow_huge=True) else: # Do coverage check to determine the mask on the output convolve_func = partial(convolve_fft, fill_value=fill_value, nan_treatment=nan_treatment, boundary=boundary, allow_huge=True) convolve_func_w = partial(convolve_fft, fill_value=0., boundary='fill', allow_huge=True) tol = newbeam.major * np.array([1 - res_tol, 1 + res_tol]) if ((tol[0] < proj.beam.major < tol[1]) and (tol[0] < proj.beam.minor < tol[1])): if verbose: print("Native resolution within tolerance - " "Copying original image...") my_append_raw = False convproj = wtproj = None newproj = proj.copy() else: if verbose: print("Deconvolving beam...") try: beamdiff = newbeam.deconvolve(proj.beam) except ValueError as err: if suppress_error: if verbose: print("Unsuccessful beam deconvolution: " "{}\nOld: {}\nNew: {}" "".format(err, proj.beam, newbeam)) print("Exiting...") return else: raise ValueError("Unsuccessful beam deconvolution: " "{}\nOld: {}\nNew: {}" "".format(err, proj.beam, newbeam)) if verbose: print("Convolving image...") if mode == 'dataimage': # do convolution convproj = proj.convolve_to(newbeam, convolve=convolve_func) if min_coverage is not None: my_append_raw = True wtproj = Projection(np.isfinite(proj.data).astype('float'), wcs=proj.wcs, beam=proj.beam) wtproj = wtproj.convolve_to(newbeam, convolve=convolve_func_w) # divide the raw convolved image by the weight image # to correct for filling fraction newproj = convproj / wtproj.hdu.data # mask all pixels w/ weight smaller than min_coverage threshold = min_coverage * u.dimensionless_unscaled newproj[wtproj < threshold] = np.nan else: my_append_raw = False newproj = convproj wtproj = None elif mode == 'noiseimage': # Empirically derive a noise image at the lower resolution # Step 1: square the high resolution noise image projsq = proj**2 # Step 2: convolve the squared noise image with a kernel # that is sqrt(2) times narrower than the one # used for data image convolution (this is because # the Gaussian weight needs to be squared in # error propagation) beamdiff_small = Beam(major=beamdiff.major / np.sqrt(2), minor=beamdiff.minor / np.sqrt(2), pa=beamdiff.pa) newbeam_small = proj.beam.convolve(beamdiff_small) convprojsq = projsq.convolve_to(newbeam_small, convolve=convolve_func) if min_coverage is not None: my_append_raw = True wtproj = Projection(np.isfinite(proj.data).astype('float'), wcs=proj.wcs, beam=proj.beam) # divide the raw convolved image by the weight image # to correct for filling fraction wtproj_d = wtproj.convolve_to(newbeam_small, convolve=convolve_func_w) newprojsq = convprojsq / wtproj_d.hdu.data # mask all pixels w/ weight smaller than min_coverage # (here I force the masking of the noise image to be # consistent with that of the data image) wtproj = wtproj.convolve_to(newbeam, convolve=convolve_func_w) threshold = min_coverage * u.dimensionless_unscaled newprojsq[wtproj < threshold] = np.nan else: my_append_raw = False newprojsq = convprojsq wtproj = None # Step 3: find the sqrt of the convolved noise image convproj = np.sqrt(convprojsq) newproj = np.sqrt(newprojsq) # Step 4: apply a multiplicative factor, which accounts # for the decrease in rms noise due to averaging convproj = (convproj * np.sqrt(proj.beam.sr / newbeam.sr).to('').value) newproj = (newproj * np.sqrt(proj.beam.sr / newbeam.sr).to('').value) else: raise ValueError("Invalid `mode` value: {}".format(mode)) if isinstance(inimage, Projection): if append_raw and my_append_raw: return newproj, convproj, wtproj else: return newproj elif isinstance(inimage, (fits.PrimaryHDU, fits.ImageHDU)): if append_raw and my_append_raw: return newproj.hdu, convproj.hdu, wtproj.hdu else: return newproj.hdu