├── .gitignore ├── LICENSE ├── PyZOGY ├── __init__.py ├── __main__.py ├── image_class.py ├── subtract.py ├── test │ ├── __init__.py │ ├── mock_image_class.py │ ├── test_subtract.py │ └── test_util.py └── util.py ├── README.md └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # PyCharm Idea 92 | .idea 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 dguevel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PyZOGY/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dguevel/PyZOGY/0f13f98b742b1a4070761804a4cd9545ed03dbda/PyZOGY/__init__.py -------------------------------------------------------------------------------- /PyZOGY/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import PyZOGY.subtract 4 | import numpy as np 5 | import logging 6 | 7 | 8 | def main(): 9 | parser = argparse.ArgumentParser(description="Subtract images using ZOGY algorithm.") 10 | 11 | parser.add_argument('--science-image', dest='science_image', help='Science image to subtract') 12 | parser.add_argument('--science-psf', dest='science_psf', help='PSF for the science image') 13 | parser.add_argument('--output', dest='output', default='output.fits', help='Output file name') 14 | parser.add_argument('--science-mask', dest='science_mask', help='Mask for the science image', default=None) 15 | parser.add_argument('--reference-image', dest='reference_image', help='Reference image to subtract') 16 | parser.add_argument('--reference-psf', dest='reference_psf', help='PSF for the reference image') 17 | parser.add_argument('--reference-mask', dest='reference_mask', help='Mask for the reference image', default=None) 18 | 19 | parser.add_argument('--science-saturation', dest='science_saturation', help='Science image saturation value', 20 | default=np.inf, type=float) 21 | parser.add_argument('--reference-saturation', dest='reference_saturation', help='Reference image saturation value', 22 | default=np.inf, type=float) 23 | parser.add_argument('--science-variance', dest='science_variance', help='Science variance image', 24 | default=None) 25 | parser.add_argument('--reference-variance', dest='reference_variance', help='Reference variance image', 26 | default=None) 27 | 28 | parser.add_argument('--n-stamps', dest='n_stamps', help='Number of stamps to use when fitting the sky level', 29 | default=1, type=int) 30 | parser.add_argument('--normalization', dest='normalization', help='Which image to normalize the difference to', 31 | default='reference') 32 | parser.add_argument('--gain-ratio', help='Ratio of the science zero point to the reference zero point', 33 | default=np.inf, type=float) 34 | parser.add_argument('--gain-mask', help='Additional mask for pixels not to be used in gain matching') 35 | parser.add_argument('--use-pixels', action='store_true', help='Use pixels for gain matching instead of stars') 36 | parser.add_argument('--show', action='store_true', help='Show plots during for gain matching') 37 | parser.add_argument('--sigma-cut', help='Threshold (in standard deviations) to extract a star from the image', 38 | default=5., type=float) 39 | parser.add_argument('--no-mask-for-gain', help='Ignore the input masks when calculating the gain ratio', 40 | action='store_false', dest='use_mask_for_gain') 41 | parser.add_argument('--max-iterations', default=5, type=int, 42 | help='Maximum number of iterations to reconvolve the images for gain matching') 43 | parser.add_argument('--size-cut', action='store_true', 44 | help='Ignore unusually large/small sources for gain matching (assumes most sources are real)') 45 | 46 | parser.add_argument('--matched-filter', help='Output filename for matched filter image') 47 | parser.add_argument('--correct', action='store_true', help='Correct matched filter image with noise') 48 | parser.add_argument('--photometry', action='store_true', help='Correct matched filter image to do photometry') 49 | 50 | parser.add_argument('--log', dest='log', help='Log output file', default='pyzogy.log') 51 | parser.add_argument('--percent', dest='percent', help='Pixel percentile for gain matching on pixels', default=99) 52 | parser.add_argument('--pixstack-limit', type=int, help='Modify set_extract_pixstack in Sep') 53 | 54 | args = parser.parse_args() 55 | 56 | logging.basicConfig(filename=args.log, level=logging.DEBUG, filemode='w') 57 | 58 | PyZOGY.subtract.run_subtraction(args.science_image, 59 | args.reference_image, 60 | args.science_psf, 61 | args.reference_psf, 62 | output=args.output, 63 | science_mask=args.science_mask, 64 | reference_mask=args.reference_mask, 65 | n_stamps=args.n_stamps, 66 | normalization=args.normalization, 67 | science_saturation=args.science_saturation, 68 | reference_saturation=args.reference_saturation, 69 | science_variance=args.science_variance, 70 | reference_variance=args.reference_variance, 71 | gain_ratio=args.gain_ratio, 72 | gain_mask=args.gain_mask, 73 | use_pixels=args.use_pixels, 74 | show=args.show, 75 | matched_filter=args.matched_filter, 76 | percent=args.percent, 77 | corrected=args.correct, 78 | photometry=args.photometry, 79 | sigma_cut=args.sigma_cut, 80 | use_mask_for_gain=args.use_mask_for_gain, 81 | max_iterations=args.max_iterations, 82 | size_cut=args.size_cut, 83 | pixstack_limit=args.pixstack_limit) 84 | 85 | if __name__ == '__main__': 86 | main() 87 | -------------------------------------------------------------------------------- /PyZOGY/image_class.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import numpy as np 4 | from astropy.io import fits 5 | from PyZOGY import util 6 | 7 | 8 | class ImageClass(np.ndarray): 9 | """ 10 | Handles images used by subtraction functions. 11 | 12 | This class handles image data, PSF data, mask data, and other parameters needed for 13 | subtraction. It is a subclass of numpy ndarray, so the image data can be accessed 14 | like a normal numpy array. 15 | 16 | Args: 17 | image_filename (str): Name of the FITS file containing the image. 18 | psf_filename (str, optional): Name of the FITS file containing the PSF. 19 | mask_filename (str, optional): Name of the FITS file containing the bad pixel 20 | mask array with 1 indicating masking and 0 indicating no masking. 21 | n_stamps (int, optional): Number of stamps to use for background estimation. 22 | saturation (float, optional): Maximum usable value in the FITS image. 23 | read_noise (float, optional): Read noise of the FITs image. 24 | 25 | Attributes: 26 | header (astropy.io.fits.Header): Header from the image FITS file. 27 | raw_image (numpy.ndarray): Unaltered data from the image FITS file. 28 | raw_psf (numpy.ndarray): Unaltered data frin tge PSF FITS file. 29 | background_std (float): Standard deviation of the image. 30 | image_filename (str): Filename of the image FITS. 31 | psf_filename (str): Filename of the PSF FITS. 32 | saturation (float): Maximum usable value in the FITS image. 33 | mask (numpy.ndarray): Bad pixel mask for the image. 34 | psf (numpy.ndarray): PSF after shifting, resizing, and normalization. 35 | zero_point (float): Flux based zero point of the image 36 | """ 37 | 38 | def __new__(cls, image_filename, psf_filename, mask_filename=None, n_stamps=1, 39 | saturation=np.inf, variance=None, read_noise=0, registration_noise=(0, 0)): 40 | if isinstance(image_filename, str): # filename 41 | raw_image, header = fits.getdata(image_filename, header=True) 42 | elif hasattr(image_filename, 'header'): # FITS HDU or astropy CCDData 43 | raw_image = image_filename.data 44 | header = image_filename.header 45 | image_filename = 'IMAGE_IN_MEMORY' 46 | else: # numpy array 47 | raw_image = image_filename 48 | header = None 49 | image_filename = 'IMAGE_IN_MEMORY' 50 | if isinstance(psf_filename, str): 51 | raw_psf = fits.getdata(psf_filename) 52 | else: 53 | raw_psf = psf_filename 54 | psf_filename = 'PSF_IN_MEMORY' 55 | psf = util.center_psf(util.resize_psf(raw_psf, raw_image.shape), fname=image_filename) / np.sum(raw_psf) 56 | if isinstance(mask_filename, str): 57 | mask = fits.getdata(mask_filename) 58 | else: 59 | mask = mask_filename 60 | mask = util.mask_saturated_pix(raw_image, saturation, mask, image_filename) 61 | masked_image = np.ma.array(raw_image, mask=mask) 62 | background_std, background_counts = util.fit_noise(masked_image, n_stamps=n_stamps, fname=image_filename) 63 | image_data = util.interpolate_bad_pixels(masked_image, fname=image_filename) - background_counts 64 | 65 | obj = np.asarray(image_data).view(cls) 66 | obj.header = header 67 | obj.raw_image = raw_image 68 | obj.raw_psf = raw_psf 69 | obj.background_std = background_std 70 | obj.background_counts = background_counts 71 | obj.image_filename = image_filename 72 | obj.psf_filename = psf_filename 73 | obj.saturation = saturation 74 | obj.mask = mask 75 | obj.psf = psf 76 | obj.zero_point = 1. 77 | obj.variance = variance 78 | obj.read_noise = read_noise 79 | obj.registration_noise = registration_noise 80 | 81 | return obj 82 | 83 | def __array_finalize__(self, obj): 84 | if obj is None: 85 | return 86 | self.raw_image = getattr(obj, 'raw_image', None) 87 | self.header = getattr(obj, 'header', None) 88 | self.raw_psf = getattr(obj, 'raw_psf', None) 89 | self.background_std = getattr(obj, 'background_std', None) 90 | self.background_counts = getattr(obj, 'background_counts', None) 91 | self.image_filename = getattr(obj, 'image_filename', None) 92 | self.psf_filename = getattr(obj, 'psf_filename', None) 93 | self.saturation = getattr(obj, 'saturation', None) 94 | self.mask = getattr(obj, 'mask', None) 95 | self.psf = getattr(obj, 'psf', None) 96 | self.zero_point = getattr(obj, 'zero_point', None) 97 | self.variance = getattr(obj, 'variance', None) 98 | self.read_noise = getattr(obj, 'read_noise', None) 99 | self.registration_noise = getattr(obj, 'registration_noise', None) 100 | -------------------------------------------------------------------------------- /PyZOGY/subtract.py: -------------------------------------------------------------------------------- 1 | from . import util 2 | from .image_class import ImageClass 3 | from astropy.io import fits 4 | import numpy as np 5 | import logging 6 | 7 | # clobber keyword is deprecated in astropy 1.3 8 | from astropy import __version__ 9 | if __version__ < '1.3': 10 | overwrite = {'clobber': True} 11 | else: 12 | overwrite = {'overwrite': True} 13 | 14 | 15 | def calculate_difference_image(science, reference, gain_ratio=np.inf, gain_mask=None, sigma_cut=5., use_pixels=False, 16 | show=False, percent=99, use_mask_for_gain=True, max_iterations=5, size_cut=True, 17 | pixstack_limit=None): 18 | """ 19 | Calculate the difference image using the Zackay algorithm. 20 | 21 | This is the main function that calculates the difference image using the 22 | Zackay, Ofek, Gal-Yam 2016. It operates on ImageClass objects defined in 23 | image_class.py. The function will fit the gain ratio if not provided. 24 | Ultimately this calculates equation 13 in Zackay, Ofek, Gal-Yam 2016. 25 | 26 | Parameters 27 | ---------- 28 | science : PyZOGY.ImageClass 29 | ImageClass instance created from the science image. 30 | reference : PyZOGY.ImageClass 31 | ImageClass instance created from the reference image. 32 | gain_ratio : float, optional 33 | Ration of the gains or flux based zero points of the two images. 34 | gain_mask : str or numpy.ndarray, optional 35 | Array or FITS file holding an array of pixels to use when fitting 36 | the gain ratio. 37 | sigma_cut : float, optional 38 | Threshold (in standard deviations) to extract a star from the image (`thresh` in `sep.extract`). 39 | use_pixels : bool, optional 40 | Fit the gain ratio using pixels (True) or stars (False) in image. 41 | show : bool, optional 42 | Display debuggin plots during fitting. 43 | percent : float, optional 44 | Percentile cutoff to use for fitting the gain ratio. 45 | use_mask_for_gain : bool, optional 46 | Set to False in order to ignore the input masks when calculating the gain ratio. 47 | max_iterations : int, optional 48 | Maximum number of iterations to reconvolve the images for gain matching. 49 | size_cut : bool, optinal 50 | Ignore unusually large/small sources for gain matching (assumes most sources are real). 51 | pixstack_limit : int, optional 52 | Number of active object pixels in Sep, set with sep.set_extract_pixstack 53 | 54 | Returns 55 | ------- 56 | difference_image : numpy.ndarray 57 | The difference between science and reference images. 58 | """ 59 | 60 | # match the gains 61 | if gain_ratio == np.inf: 62 | if gain_mask is not None: 63 | if type(gain_mask) == str: 64 | gain_mask_data = fits.getdata(gain_mask) 65 | else: 66 | gain_mask_data = gain_mask 67 | science.mask[gain_mask_data == 1] = 1 68 | reference.mask[gain_mask_data == 1] = 1 69 | science.zero_point = util.solve_iteratively(science, reference, sigma_cut=sigma_cut, use_pixels=use_pixels, 70 | show=show, percent=percent, use_mask=use_mask_for_gain, 71 | max_iterations=max_iterations, size_cut=size_cut, 72 | pixstack_limit=pixstack_limit) 73 | else: 74 | science.zero_point = gain_ratio 75 | 76 | # create required arrays 77 | science_image = science 78 | reference_image = reference 79 | science_psf = science.psf 80 | reference_psf = reference.psf 81 | 82 | # do fourier transforms (fft) 83 | science_image_fft = np.fft.fft2(science_image) 84 | reference_image_fft = np.fft.fft2(reference_image) 85 | science_psf_fft = np.fft.fft2(science_psf) 86 | reference_psf_fft = np.fft.fft2(reference_psf) 87 | 88 | # calculate difference image 89 | denominator = science.background_std ** 2 * reference.zero_point ** 2 * abs(reference_psf_fft) ** 2 90 | denominator += reference.background_std ** 2 * science.zero_point ** 2 * abs(science_psf_fft) ** 2 91 | difference_image_fft = science_image_fft * reference_psf_fft * reference.zero_point 92 | difference_image_fft -= reference_image_fft * science_psf_fft * science.zero_point 93 | difference_image_fft /= np.sqrt(denominator) 94 | difference_image = np.fft.ifft2(difference_image_fft) 95 | difference_image = np.real(difference_image) 96 | 97 | return difference_image 98 | 99 | 100 | def calculate_difference_image_zero_point(science, reference): 101 | """ 102 | Calculate the flux based zero point of the difference image. 103 | 104 | Calculate the difference image flux based zero point using equation 15 of 105 | Zackay, Ofek, Gal-Yam 2016. 106 | 107 | Parameters 108 | ---------- 109 | science : PyZOGY.ImageClass 110 | ImageClass instance created from the science image. 111 | reference : PyZOGY.ImageClass 112 | ImageClass instance created from the reference image. 113 | 114 | Returns 115 | ------- 116 | difference_image_zero_point : float 117 | Flux based zero point of the difference image. 118 | """ 119 | 120 | denominator = science.background_std ** 2 * reference.zero_point ** 2 121 | denominator += reference.background_std ** 2 * science.zero_point ** 2 122 | difference_image_zero_point = science.zero_point * reference.zero_point / np.sqrt(denominator) 123 | 124 | logging.info('Global difference image zero point is {}'.format(np.mean(difference_image_zero_point))) 125 | return difference_image_zero_point 126 | 127 | 128 | def calculate_difference_psf(science, reference, difference_image_zero_point): 129 | """ 130 | Calculate the PSF of the difference image. 131 | 132 | Calculactes the PSF of the difference image using equation 17 of Zackay, 133 | Ofek, Gal-Yam 2016. 134 | 135 | Parameters 136 | ---------- 137 | science : PyZOGY.ImageClass 138 | ImageClass instance created from the science image. 139 | reference : PyZOGY.ImageClass 140 | ImageClass instance created from the reference image. 141 | difference_image_zero_point : float 142 | Flux based zero point of the difference image. 143 | 144 | Returns 145 | ------- 146 | difference_psf : numpy.ndarray 147 | PSF of the difference image. 148 | """ 149 | 150 | science_psf_fft = np.fft.fft2(science.psf) 151 | reference_psf_fft = np.fft.fft2(reference.psf) 152 | denominator = science.background_std ** 2 * reference.zero_point ** 2 * abs(reference_psf_fft) ** 2 153 | denominator += reference.background_std ** 2 * science.zero_point ** 2 * abs(science_psf_fft) ** 2 154 | 155 | difference_psf_fft = science.zero_point * science_psf_fft * reference_psf_fft 156 | difference_psf_fft /= difference_image_zero_point * np.sqrt(denominator) 157 | difference_psf = np.fft.ifft2(difference_psf_fft) 158 | 159 | return difference_psf 160 | 161 | 162 | def calculate_matched_filter_image(difference_image, difference_psf, difference_zero_point): 163 | """ 164 | Calculate the matched filter difference image. 165 | 166 | Calculates the matched filter difference image described in Zackay, Ofek, 167 | Gal-Yam 2016 defined in equation 16. 168 | 169 | Parameters 170 | ---------- 171 | difference_image : numpy.ndarray 172 | A difference image as calculated using calculate_difference_image. 173 | difference_psf : numpy.ndarray 174 | PSF for the difference image above. 175 | difference_zero_point 176 | Flux based zero point for the image above. 177 | 178 | Returns 179 | ------- 180 | matched_filter : numpy.ndarray 181 | Matched filter image. 182 | """ 183 | 184 | matched_filter_fft = difference_zero_point * np.fft.fft2(difference_image) * np.conj(np.fft.fft2(difference_psf)) 185 | matched_filter = np.fft.ifft2(matched_filter_fft) 186 | 187 | return matched_filter 188 | 189 | 190 | def source_noise(image, kernel): 191 | """ 192 | Calculate source noise correction for matched filter image 193 | 194 | Calculate the noise due to the sources in an image. The output is used by 195 | noise corrected matched filter image. This is equation 26 in Zackay, Ofek, 196 | Gal-Yam 2016. 197 | 198 | Parameters 199 | ---------- 200 | image : PyZOGY.ImageClass 201 | ImageClass instance with read_noise attribute defined. 202 | kernel : numpy.ndarray 203 | Convolution kernel for the noise image. This comes from the function 204 | called noise_kernels. 205 | 206 | Returns 207 | ------- 208 | image_variance_corr : numpy.ndarray 209 | Variance of the image due to source noise. 210 | """ 211 | 212 | if image.variance is None: 213 | image.variance = np.copy(image.raw_image) + image.read_noise 214 | 215 | image_variance_corr = np.fft.ifft2(np.fft.fft2(image.variance) * np.fft.fft2(kernel ** 2)) 216 | 217 | return image_variance_corr 218 | 219 | 220 | def noise_kernels(science, reference): 221 | """ 222 | Calculate the convolution kernels used in the noise correction 223 | 224 | The kernels calculated here are used in the convolution of the noise images 225 | that are used in the noise corrected matched filter images. They are 226 | defined in equation 28 and 29 of Zackay, Ofek, Gal-Yam 2016. 227 | 228 | Parameters 229 | science : PyZOGY.ImageClass 230 | ImageClass instance created from the science image. 231 | reference : PyZOGY.ImageClass 232 | ImageClass instance created from the reference image. 233 | 234 | Returns 235 | ------- 236 | science_kernel : numpy.ndarray 237 | Kernel for the convolution of arrays derived from the science image. 238 | reference_kernel : numpy.ndarray 239 | Kernel for the convolution of arrays derived from the reference image. 240 | """ 241 | 242 | science_psf_fft = np.fft.fft2(science.psf) 243 | reference_psf_fft = np.fft.fft2(reference.psf) 244 | denominator = reference.background_std ** 2 * science.zero_point ** 2 * abs(science_psf_fft) ** 2 245 | denominator += science.background_std ** 2 * reference.zero_point ** 2 * abs(reference_psf_fft) ** 2 246 | 247 | science_kernel_fft = science.zero_point * reference.zero_point ** 2 248 | science_kernel_fft *= np.conj(reference_psf_fft) * abs(science_psf_fft) ** 2 249 | science_kernel_fft /= denominator 250 | science_kernel = np.fft.ifft2(science_kernel_fft) 251 | 252 | reference_kernel_fft = reference.zero_point * science.zero_point ** 2 253 | reference_kernel_fft *= np.conj(science_psf_fft) * abs(reference_psf_fft) ** 2 254 | reference_kernel_fft /= denominator 255 | reference_kernel = np.fft.ifft2(reference_kernel_fft) 256 | 257 | return science_kernel, reference_kernel 258 | 259 | 260 | def registration_noise(image, kernel): 261 | """ 262 | Calculate the registration noise for the noise correction 263 | 264 | Calculates the astrometric registration noise image. This noise image is 265 | used in the calculation of the noise corrected matched filter image. 266 | 267 | Parameters 268 | ---------- 269 | image : PyZOGY.ImageClass 270 | ImageClass instance with registration_noise attribute defined. 271 | kernel : numpy.ndarray 272 | Convolution kernel for the noise image. This comes from the function 273 | called noise_kernels. 274 | 275 | Returns 276 | ------- 277 | reg_variance : numpy.ndarray 278 | Noise image due to uncertainty in the image registration. 279 | """ 280 | 281 | matched_part = np.fft.ifft2(np.fft.fft2(image) * np.fft.fft2(kernel)) 282 | gradient = np.gradient(matched_part) 283 | # registration_noise is (x, y), gradient is (row, col) 284 | reg_variance = image.registration_noise[1] ** 2 * gradient[0] ** 2 285 | reg_variance += image.registration_noise[0] ** 2 * gradient[1] ** 2 286 | 287 | return reg_variance 288 | 289 | 290 | def correct_matched_filter_image(science, reference): 291 | """ 292 | Calculate the noise corrected matched filter image 293 | 294 | Computes the total noise used for the noise corrected matched filter image 295 | as defined in equation 25 of Zackay, Ofek, Gal-Yam 2016. This will work 296 | with the default read_noise and registration_noise, but it may not give 297 | a meaningful result. 298 | 299 | Parameters 300 | ---------- 301 | science : PyZOGY.ImageClass 302 | ImageClass instance created from the science image. 303 | reference : PyZOGY.ImageClass 304 | ImageClass instance created from the reference image. 305 | 306 | Returns 307 | ------- 308 | noise : numpy.ndarray 309 | The total noise in the matched filter image. 310 | """ 311 | 312 | science_kernel, reference_kernel = noise_kernels(science, reference) 313 | science_source_noise = source_noise(science, science_kernel) 314 | reference_source_noise = source_noise(reference, reference_kernel) 315 | science_registration_noise = registration_noise(science, science_kernel) 316 | reference_registration_noise = registration_noise(reference, reference_kernel) 317 | noise = science_source_noise + reference_source_noise + science_registration_noise + reference_registration_noise 318 | return noise 319 | 320 | 321 | def photometric_matched_filter_image(science, reference, matched_filter): 322 | """ 323 | Calculate the photometry on the matched filter image 324 | """ 325 | # note this may do exactly what another function above does 326 | # check this out later. 327 | 328 | science_psf_fft = np.fft.fft2(science.psf) 329 | reference_psf_fft = np.fft.fft2(reference.psf) 330 | zero_point = science.zero_point ** 2 * reference.zero_point ** 2 331 | zero_point *= abs(science_psf_fft) ** 2 * abs(reference_psf_fft) ** 2 332 | denominator = reference.background_std ** 2 * science.zero_point ** 2 * abs(science_psf_fft) ** 2 333 | denominator += science.background_std ** 2 * reference.zero_point ** 2 * abs(reference_psf_fft) ** 2 334 | zero_point /= denominator 335 | photometric_matched_filter = matched_filter / np.sum(zero_point) 336 | 337 | return photometric_matched_filter 338 | 339 | 340 | def normalize_difference_image(difference, difference_image_zero_point, science, reference, normalization='reference'): 341 | """ 342 | Normalize to user's choice of image 343 | 344 | Normalizes the difference image into the photometric system of the science 345 | image, reference image, or leave un-normalized. 346 | 347 | Parameters 348 | ---------- 349 | difference : numpy.ndarray 350 | Difference image as calculated by calculate_difference_image. 351 | difference_image_zero_point : float 352 | Flux based zero point of the difference image above. 353 | science : PyZOGY.ImageClass 354 | ImageClass instance created from the science image. 355 | reference : PyZOGY.ImageClass 356 | ImageClass instance created from the reference image. 357 | normalization : str, optional 358 | Normalization choice. Options are 'reference', 'science', or 'none'. 359 | 360 | Returns 361 | ------- 362 | difference_image : numpy.ndarray 363 | Normalized difference image. 364 | """ 365 | 366 | if normalization == 'reference' or normalization == 't': 367 | difference_image = difference * reference.zero_point / difference_image_zero_point 368 | elif normalization == 'science' or normalization == 'i': 369 | difference_image = difference * science.zero_point / difference_image_zero_point 370 | else: 371 | difference_image = difference 372 | 373 | logging.info('Difference normalized to {}'.format(normalization)) 374 | return difference_image 375 | 376 | 377 | def run_subtraction(science_image, reference_image, science_psf, reference_psf, output=None, 378 | science_mask=None, reference_mask=None, n_stamps=1, normalization='reference', 379 | science_saturation=np.inf, reference_saturation=np.inf, science_variance=None, 380 | reference_variance=None, matched_filter=False, photometry=False, 381 | gain_ratio=np.inf, gain_mask=None, use_pixels=False, sigma_cut=5., show=False, percent=99, 382 | corrected=False, use_mask_for_gain=True, max_iterations=5, size_cut=False, pixstack_limit=None): 383 | """ 384 | Run full subtraction given filenames and parameters 385 | 386 | Main function for users who don't want to use the ImageClass. This function 387 | lets the user put in all the arguments by hand and then creates the 388 | ImageClass instances. 389 | 390 | Parameters 391 | ---------- 392 | science_image : numpy.ndarray 393 | Science image to compare to reference image. 394 | reference_image : numpy.ndarray 395 | Reference image to subtract from science. 396 | science_psf : numpy.ndarray 397 | PSF of the science image. 398 | reference_psf : numpy.ndarray 399 | PSF of the reference image. 400 | output : str, optional 401 | If provided, save the difference image to a FITS file with this file name (and its PSF to *.psf.fits). 402 | science_mask : str, optional 403 | Name of the FITS file holding the science image mask. 404 | reference_mask : str, optional 405 | Name of the FITS file holding the reference image mask. 406 | n_stamps : int, optional 407 | Number of stamps to use while fitting background. 408 | normalization : str, optional 409 | Normalize difference image to 'reference', 'science', or 'none'. 410 | science_saturation : float, optional 411 | Maximum usable pixel value in science image. 412 | reference_saturation : float, optional 413 | Maximum usable pixel value in reference image. 414 | science_variance : numpy.ndarray or float, optional 415 | Variance of the science image 416 | reference_variance : numpy.ndarray or float, optional 417 | Variance of the reference image. 418 | matched_filter : bool, optional 419 | Calculate the matched filter image. 420 | photometry : bool, optional 421 | Photometrically normalize the matched filter image. 422 | gain_ratio : float, optional 423 | Ratio between the flux based zero points of the images. 424 | gain_mask : numpy.ndarray or str, optional 425 | Array or FITS image of pixels to use in gain matching. 426 | use_pixels : bool, optional 427 | Use pixels (True) or stars (False) to match gains. 428 | sigma_cut : float, optional 429 | Threshold (in standard deviations) to extract a star from the image (`thresh` in `sep.extract`). 430 | show : bool, optional 431 | Show debugging plots. 432 | percent : float, optional 433 | Percentile cutoff for gain matching. 434 | corrected : bool, optional 435 | Noise correct matched filter image. 436 | use_mask_for_gain : bool, optional 437 | Set to False in order to ignore the input masks when calculating the gain ratio. 438 | max_iterations : int, optional 439 | Maximum number of iterations to reconvolve the images for gain matching. 440 | size_cut : bool, optinal 441 | Ignores unusually large/small sources for gain matching (assumes most sources are real). 442 | pixstack_limit : int 443 | Number of active object pixels in Sep, set with sep.set_extract_pixstack 444 | 445 | Returns 446 | ------- 447 | normalized_difference : numpy.ndarray 448 | The normalized difference between science and reference images. 449 | difference_psf : numpy.ndarray 450 | The difference image PSF. 451 | """ 452 | 453 | science = ImageClass(science_image, science_psf, science_mask, n_stamps, science_saturation, science_variance) 454 | reference = ImageClass(reference_image, reference_psf, reference_mask, n_stamps, reference_saturation, 455 | reference_variance) 456 | difference = calculate_difference_image(science, reference, gain_ratio, gain_mask, sigma_cut, use_pixels, show, 457 | percent, use_mask_for_gain, max_iterations, size_cut, pixstack_limit) 458 | difference_zero_point = calculate_difference_image_zero_point(science, reference) 459 | difference_psf = calculate_difference_psf(science, reference, difference_zero_point) 460 | normalized_difference = normalize_difference_image(difference, difference_zero_point, science, reference, 461 | normalization) 462 | 463 | if output: 464 | save_difference_image_to_file(normalized_difference, science, normalization, output) 465 | save_difference_psf_to_file(difference_psf, output.replace('.fits', '.psf.fits')) 466 | 467 | if matched_filter: 468 | matched_filter_image = calculate_matched_filter_image(difference, difference_psf, difference_zero_point) 469 | if photometry and corrected: 470 | logging.error('Photometric matched filter and noise corrected matched filter are incompatible') 471 | if photometry: 472 | matched_filter_image = photometric_matched_filter_image(science, reference, matched_filter_image) 473 | elif corrected: 474 | matched_filter_image /= np.sqrt(correct_matched_filter_image(science, reference)) 475 | fits.writeto(matched_filter, np.real(matched_filter_image), science.header, output_verify='warn', **overwrite) 476 | logging.info('Wrote matched filter image to {}'.format(matched_filter)) 477 | 478 | return normalized_difference, difference_psf 479 | 480 | 481 | def save_difference_image_to_file(difference_image, science, normalization, output): 482 | """ 483 | Save difference image to file. 484 | 485 | Normalize and save difference image to file. This also copies over the 486 | FITS header of the science image. 487 | 488 | Parameters 489 | ---------- 490 | difference_image : numpy.ndarray 491 | Difference image 492 | science : PyZOGY.ImageClass 493 | ImageClass instance created from the science image. 494 | normalization : str 495 | Normalize to 'reference', 'science', or 'none'. 496 | output : str 497 | File to save FITS image to. 498 | """ 499 | 500 | hdu = fits.PrimaryHDU(difference_image) 501 | hdu.header = science.header.copy() 502 | hdu.header['PHOTNORM'] = normalization 503 | hdu.writeto(output, output_verify='warn', **overwrite) 504 | logging.info('Wrote difference image to {}'.format(output)) 505 | 506 | 507 | def save_difference_psf_to_file(difference_psf, output): 508 | """ 509 | Save difference image psf to file. 510 | 511 | Save the PSF of the difference image to a FITS file. 512 | 513 | Parameters 514 | ---------- 515 | difference_psf : numpy.ndarray 516 | PSF of the difference image. 517 | output : str 518 | File to save FITS image to. 519 | """ 520 | real_part = np.real(difference_psf) 521 | center = np.array(real_part.shape) / 2 522 | centered_psf = np.roll(real_part, center.astype(int), (0, 1)) 523 | fits.writeto(output, centered_psf, output_verify='warn', **overwrite) 524 | logging.info('Wrote difference psf to {}'.format(output)) 525 | -------------------------------------------------------------------------------- /PyZOGY/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dguevel/PyZOGY/0f13f98b742b1a4070761804a4cd9545ed03dbda/PyZOGY/test/__init__.py -------------------------------------------------------------------------------- /PyZOGY/test/mock_image_class.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from astropy.io import fits 3 | 4 | class MockImageClass(np.ndarray): 5 | """Creates a mock version of ImageClass for testing""" 6 | 7 | def __new__(cls, image_filename='', psf_filename='', mask_filename=None, n_stamps=1, saturation=np.inf, variance=np.inf, shape=(50,50)): 8 | raw_image, header = np.ones(shape), fits.Header()#fits.getdata(image_filename, header=True) 9 | raw_psf = np.ones(shape) 10 | mask = np.zeros(shape) 11 | background_std, background_counts = np.ones(shape), np.zeros(shape) 12 | image_data = np.ones(shape) 13 | 14 | obj = np.asarray(image_data).view(cls) 15 | obj.header = header 16 | obj.raw_image = raw_image 17 | obj.raw_psf = raw_psf 18 | obj.background_std = background_std 19 | obj.background_counts = background_counts 20 | obj.image_filename = image_filename 21 | obj.psf_filename = psf_filename 22 | obj.saturation = saturation 23 | obj.mask = mask 24 | obj.psf = raw_psf 25 | obj.zero_point = 1. 26 | obj.variance = variance 27 | 28 | return obj 29 | 30 | def __array_finalize__(self, obj): 31 | if obj is None: 32 | return 33 | self.raw_image = getattr(obj, 'raw_image', None) 34 | self.header = getattr(obj, 'header', None) 35 | self.raw_psf = getattr(obj, 'raw_psf', None) 36 | self.background_std = getattr(obj, 'background_std', None) 37 | self.background_counts = getattr(obj, 'background_counts', None) 38 | self.image_filename = getattr(obj, 'image_filename', None) 39 | self.psf_filename = getattr(obj, 'psf_filename', None) 40 | self.saturation = getattr(obj, 'saturation', None) 41 | self.mask = getattr(obj, 'mask', None) 42 | self.psf = getattr(obj, 'psf', None) 43 | self.zero_point = getattr(obj, 'zero_point', None) 44 | self.variance = getattr(obj, 'variance', None) 45 | -------------------------------------------------------------------------------- /PyZOGY/test/test_subtract.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from astropy.io import fits 3 | 4 | -------------------------------------------------------------------------------- /PyZOGY/test/test_util.py: -------------------------------------------------------------------------------- 1 | import unittest as ut 2 | import PyZOGY.util as util 3 | from . import mock_image_class 4 | import numpy as np 5 | 6 | 7 | class test_util(ut.TestCase): 8 | 9 | def test_center_psf(self): 10 | psf = np.zeros((50, 51)) 11 | psf[25, 25] = 1 12 | assert util.center_psf(psf)[0, 0] == 1 13 | 14 | def test_make_mask(self): 15 | saturation = 1. 16 | image = mock_image_class.MockImageClass(shape=(50, 50), saturation=saturation) 17 | image[25, 25] = 2. 18 | image[30, 30] = 1. 19 | 20 | mask = util.mask_saturated_pix(image, image.saturation, np.zeros(image.shape)) 21 | assert mask[image >= saturation].all() 22 | 23 | mask = util.mask_saturated_pix(image, image.saturation, None) 24 | assert mask[image >= saturation].all() 25 | 26 | def test_interpolate_bad_pixels(self): 27 | image = mock_image_class.MockImageClass(shape=(50, 50)) 28 | image[0, 25] = np.nan 29 | image[25, 0] = np.nan 30 | image[25, 25] = np.nan 31 | image.mask = np.zeros(shape=(50, 50)) 32 | image.mask[np.isnan(image)] = 1 33 | image = util.interpolate_bad_pixels(image) 34 | assert np.isfinite(image).all() 35 | -------------------------------------------------------------------------------- /PyZOGY/util.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.ndimage 3 | from astropy.convolution import convolve, Gaussian2DKernel 4 | import statsmodels.api as stats 5 | import sep 6 | import logging 7 | 8 | 9 | def mask_saturated_pix(image, saturation=np.inf, input_mask=None, fname=''): 10 | """Make a pixel mask that marks saturated pixels; optionally join with input_mask""" 11 | 12 | if input_mask is None: 13 | input_mask = np.zeros(image.shape) 14 | 15 | input_mask[np.isnan(image) | (image >= saturation)] = 1 16 | input_mask = input_mask.astype(bool) 17 | 18 | logging.info('{0}Masked {1} saturated pixels'.format(fname + ':', np.size(np.where(input_mask)))) 19 | return input_mask 20 | 21 | 22 | def center_psf(psf, fname=''): 23 | """Center psf at (0,0) based on max value""" 24 | 25 | peak = np.array(np.unravel_index(psf.argmax(), psf.shape)) 26 | psf = np.roll(psf, -peak, (0, 1)) 27 | 28 | logging.info('{0}Shifted PSF from {1} to [0 0]'.format(fname + ':', peak)) 29 | return psf 30 | 31 | 32 | def fit_noise(data, n_stamps=1, mode='iqr', fname=''): 33 | """Find the standard deviation of the image background; returns standard deviation, median""" 34 | 35 | median_small = np.zeros([n_stamps, n_stamps]) 36 | std_small = np.zeros([n_stamps, n_stamps]) 37 | if mode == 'sep': 38 | background = sep.Background(np.ascontiguousarray(data.data).byteswap().newbyteorder()) 39 | median = background.back() 40 | std = background.rms() 41 | else: 42 | for y_stamp in range(n_stamps): 43 | for x_stamp in range(n_stamps): 44 | y_index = [y_stamp * data.shape[0] // n_stamps, (y_stamp + 1) * data.shape[0] // n_stamps] 45 | x_index = [x_stamp * data.shape[1] // n_stamps, (x_stamp + 1) * data.shape[1] // n_stamps] 46 | stamp_data = data[y_index[0]: y_index[1], x_index[0]: x_index[1]].compressed() 47 | if mode == 'iqr': 48 | quartile25, median, quartile75 = np.nanpercentile(stamp_data, (25, 50, 75)) 49 | median_small[y_stamp, x_stamp] = median 50 | # 0.741301109 is a parameter that scales iqr to std 51 | std_small[y_stamp, x_stamp] = 0.741301109 * (quartile75 - quartile25) 52 | elif mode == 'mad': 53 | median = np.median(stamp_data) 54 | absdev = np.abs(stamp_data - median) 55 | mad = np.median(absdev) 56 | median_small[y_stamp, x_stamp] = median 57 | std_small[y_stamp, x_stamp] = 1.4826 * mad 58 | 59 | median = scipy.ndimage.zoom(median_small, [data.shape[0] / float(n_stamps), data.shape[1] / float(n_stamps)]) 60 | std = scipy.ndimage.zoom(std_small, [data.shape[0] / float(n_stamps), data.shape[1] / float(n_stamps)]) 61 | 62 | logging.info('{0}Global median is {1}'.format(fname + ':', np.mean(median))) 63 | logging.info('{0}Global standard deviation is {1}'.format(fname + ':', np.mean(std))) 64 | return std, median 65 | 66 | 67 | def interpolate_bad_pixels(image, median_size=6, fname=''): 68 | """Interpolate over bad pixels using a global median; needs a mask""" 69 | 70 | interpolated_image = image.astype(float).filled(np.nan) 71 | blurred = convolve(interpolated_image, Gaussian2DKernel(median_size)) 72 | interpolated_image[image.mask] = blurred[image.mask] 73 | 74 | logging.info('{0}Interpolated {1} pixels'.format(fname + ':', np.size(np.where(image.mask)))) 75 | return interpolated_image 76 | 77 | 78 | def join_images(science_raw, science_mask, reference_raw, reference_mask, sigma_cut=5., use_pixels=False, show=False, 79 | percent=99, size_cut=True, pixstack_limit=None): 80 | """Join two images to fittable vectors""" 81 | 82 | science = np.ma.array(science_raw, mask=science_mask, copy=True) 83 | reference = np.ma.array(reference_raw, mask=reference_mask, copy=True) 84 | science_std, _ = fit_noise(science) 85 | reference_std, _ = fit_noise(reference) 86 | if use_pixels: 87 | # remove pixels less than `percent` percentile above sky level to speed fitting 88 | science.mask[science <= np.nanpercentile(science.compressed(), percent)] = True 89 | reference.mask[reference <= np.nanpercentile(reference.compressed(), percent)] = True 90 | 91 | # flatten into 1d arrays of good pixels 92 | science.mask |= reference.mask 93 | reference.mask |= science.mask 94 | science_flatten = science.compressed() 95 | reference_flatten = reference.compressed() 96 | logging.info('Found {0} usable pixels for gain matching'.format(science_flatten.size)) 97 | if science_flatten.size == 0: 98 | logging.error('No pixels in common at this percentile ({0}); lower and try again'.format(percent)) 99 | else: 100 | if pixstack_limit is None: 101 | pixstack_limit = science.size // 20 102 | if pixstack_limit > 300000: 103 | sep.set_extract_pixstack(pixstack_limit) 104 | science_sources = sep.extract(np.ascontiguousarray(science.data), thresh=sigma_cut, err=science_std, mask=np.ascontiguousarray(science.mask)) 105 | reference_sources = sep.extract(np.ascontiguousarray(reference.data), thresh=sigma_cut, err=reference_std, mask=np.ascontiguousarray(reference.mask)) 106 | science_sources = science_sources[science_sources['errx2'] != np.inf] # exclude partially masked sources 107 | reference_sources = reference_sources[reference_sources['errx2'] != np.inf] 108 | dx = science_sources['x'] - reference_sources['x'][:, np.newaxis] 109 | dy = science_sources['y'] - reference_sources['y'][:, np.newaxis] 110 | separation = np.sqrt(dx**2 + dy**2) 111 | sigma_eqv = np.sqrt((reference_sources['a']**2 + reference_sources['b']**2) / 2.) 112 | matches = (np.min(separation, axis=1) < 2. * sigma_eqv) 113 | if size_cut: 114 | # cut unusually large/small sources (assumes most sources are real) 115 | med_sigma = np.median(sigma_eqv) # median sigma if all sources were circular Gaussians 116 | absdev_sigma = np.abs(sigma_eqv - med_sigma) 117 | std_sigma = np.median(absdev_sigma) * np.sqrt(np.pi / 2) 118 | matches &= (absdev_sigma < 3 * std_sigma) 119 | inds = np.argmin(separation, axis=1) 120 | science_flatten = science_sources['flux'][inds][matches] 121 | reference_flatten = reference_sources['flux'][matches] 122 | logging.info('Found {0} stars in common for gain matching'.format(science_flatten.size)) 123 | if science_flatten.size <= 2: 124 | logging.error('Too few stars in common at {0}-sigma; lower and try again'.format(sigma_cut)) 125 | raise ValueError() 126 | 127 | if show: 128 | import matplotlib.pyplot as plt 129 | 130 | plt.ion() 131 | plt.figure(1) 132 | plt.clf() 133 | vmin, vmax = np.nanpercentile(science, (1, 99)) 134 | plt.imshow(science, vmin=vmin, vmax=vmax) 135 | plt.title('Science') 136 | if not use_pixels: 137 | plt.plot(reference_sources['x'][matches], reference_sources['y'][matches], 'o', mfc='none', mec='r') 138 | 139 | plt.figure(2) 140 | plt.clf() 141 | vmin, vmax = np.nanpercentile(reference, (1, 99)) 142 | plt.imshow(reference, vmin=vmin, vmax=vmax) 143 | plt.title('Reference') 144 | if not use_pixels: 145 | plt.plot(reference_sources['x'][matches], reference_sources['y'][matches], 'o', mfc='none', mec='r') 146 | 147 | plt.figure(3) 148 | plt.clf() 149 | plt.loglog(reference_flatten, science_flatten, '.') 150 | plt.xlabel('Reference') 151 | plt.ylabel('Science') 152 | 153 | return reference_flatten, science_flatten 154 | 155 | 156 | def resize_psf(psf, shape): 157 | """Resize centered (0,0) psf to larger shape""" 158 | 159 | psf_extended = np.pad(psf, ((0, shape[0] - psf.shape[0]), (0, shape[1] - psf.shape[1])), 160 | mode='constant', constant_values=0.) 161 | return psf_extended 162 | 163 | 164 | def pad_to_power2(data, value='median'): 165 | """Pad arrays to the nearest power of two""" 166 | 167 | if value == 'median': 168 | constant = np.median(data) 169 | elif value == 'bool': 170 | constant = False 171 | n = 0 172 | deficit = [0, 0] 173 | while (data.shape[0] > (2 ** n)) or (data.shape[1] > (2 ** n)): 174 | n += 1 175 | deficit = [(2 ** n) - data.shape[0], (2 ** n) - data.shape[1]] 176 | padded_data = np.pad(data, ((0, deficit[0]), (0, deficit[1])), mode='constant', constant_values=constant) 177 | return padded_data 178 | 179 | 180 | def solve_iteratively(science, reference, mask_tolerance=10e-5, gain_tolerance=10e-6, max_iterations=5, 181 | sigma_cut=5., use_pixels=False, show=False, percent=99, use_mask=True, size_cut=True, 182 | pixstack_limit=None): 183 | """Solve for linear fit iteratively""" 184 | 185 | gain = 1. 186 | gain0 = 10e5 187 | i = 1 188 | # pad image to power of two to speed fft 189 | old_size = science.shape 190 | science_image = pad_to_power2(science) 191 | reference_image = pad_to_power2(reference) 192 | 193 | science_psf = center_psf(resize_psf(science.raw_psf, science_image.shape)) 194 | science_psf /= np.sum(science.raw_psf) 195 | reference_psf = center_psf(resize_psf(reference.raw_psf, reference_image.shape)) 196 | reference_psf /= np.sum(reference.raw_psf) 197 | 198 | science_std = pad_to_power2(science.background_std) 199 | reference_std = pad_to_power2(reference.background_std) 200 | 201 | science_mask = pad_to_power2(science.mask, value='bool') 202 | reference_mask = pad_to_power2(reference.mask, value='bool') 203 | 204 | # fft arrays 205 | science_image_fft = np.fft.fft2(science_image) 206 | reference_image_fft = np.fft.fft2(reference_image) 207 | science_psf_fft = np.fft.fft2(science_psf) 208 | reference_psf_fft = np.fft.fft2(reference_psf) 209 | 210 | while abs(gain - gain0) > gain_tolerance: 211 | 212 | # calculate the psf in the difference image to convolve masks 213 | # not a simple convolution of the two PSF's; see the paper for details 214 | difference_zero_point = gain / np.sqrt(science_std ** 2 + reference_std ** 2 * gain ** 2) 215 | denominator = science_std ** 2 * abs(reference_psf_fft) ** 2 216 | denominator += reference_std ** 2 * gain ** 2 * abs(science_psf_fft) ** 2 217 | difference_psf_fft = gain * science_psf_fft * reference_psf_fft / (difference_zero_point * np.sqrt(denominator)) 218 | 219 | if use_mask: 220 | # convolve masks with difference psf to mask all pixels within a psf radius 221 | # this is important to prevent convolutions of saturated pixels from affecting the fit 222 | science_mask_convolved = np.fft.ifft2(difference_psf_fft * np.fft.fft2(science_mask)) 223 | science_mask_convolved[science_mask_convolved > mask_tolerance] = 1 224 | science_mask_convolved = np.real(science_mask_convolved).astype(int) 225 | reference_mask_convolved = np.fft.ifft2(difference_psf_fft * np.fft.fft2(reference_mask)) 226 | reference_mask_convolved[reference_mask_convolved > mask_tolerance] = 1 227 | reference_mask_convolved = np.real(reference_mask_convolved).astype(int) 228 | 229 | # do the convolutions on the images 230 | denominator = science_std ** 2 * abs(reference_psf_fft) ** 2 231 | denominator += gain ** 2 * reference_std ** 2 * abs(science_psf_fft) ** 2 232 | 233 | science_convolved_image_fft = reference_psf_fft * science_image_fft / np.sqrt(denominator) 234 | reference_convolved_image_fft = science_psf_fft * reference_image_fft / np.sqrt(denominator) 235 | 236 | science_convolved_image = np.real(np.fft.ifft2(science_convolved_image_fft)) 237 | reference_convolved_image = np.real(np.fft.ifft2(reference_convolved_image_fft)) 238 | 239 | # remove power of 2 padding 240 | science_convolved_image = science_convolved_image[: old_size[0], : old_size[1]] 241 | reference_convolved_image = reference_convolved_image[: old_size[0], : old_size[1]] 242 | if use_mask: 243 | science_mask_convolved = science_mask_convolved[: old_size[0], : old_size[1]] 244 | reference_mask_convolved = reference_mask_convolved[: old_size[0], : old_size[1]] 245 | else: 246 | science_mask_convolved = None 247 | reference_mask_convolved = None 248 | 249 | # do a linear robust regression between convolved image 250 | x, y = join_images(science_convolved_image, science_mask_convolved, reference_convolved_image, 251 | reference_mask_convolved, sigma_cut, use_pixels, show, percent, size_cut, pixstack_limit) 252 | robust_fit = stats.RLM(y, stats.add_constant(x), stats.robust.norms.TukeyBiweight()).fit() 253 | parameters = robust_fit.params 254 | gain0 = gain 255 | gain = parameters[-1] 256 | if show: 257 | import matplotlib.pyplot as plt 258 | xfit = np.logspace(np.log10(np.min(x)), np.log10(np.max(x))) 259 | plt.plot(xfit, robust_fit.predict(stats.add_constant(xfit))) 260 | plt.pause(0.1) 261 | 262 | logging.info('Iteration {0}: Gain = {1}'.format(i, gain)) 263 | if i == max_iterations: 264 | logging.warning('Maximum regression ({0}) iterations reached'.format(max_iterations)) 265 | break 266 | i += 1 267 | 268 | logging.info('Fit done in {0} iterations'.format(i)) 269 | 270 | return gain 271 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyZOGY 2 | PyZOGY is a Python implementation of the image subtraction algorithm published by Zackay, Ofek, and Gal-Yam. 3 | The algorithm requires two registered images and their PSF's saved as fits files. One can optionally provide 4 | masks, in fits files where every pixel is either 0 (good) or 1 (bad). Alternatively, the code will mask pixels 5 | above a user defined threshold (a number) for each image. The code fits the spatially varying background level by dividing 6 | the image into a number of stamps provided by the user; the default is 1. The image can be normalized to either 7 | the science image or the reference image. 8 | 9 | 10 | The details of the algorithm can be found at http://iopscience.iop.org/article/10.3847/0004-637X/830/1/27/meta 11 | 12 | If you use this code for a publication, please cite the above paper and our Zenodo DOI: [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.1043973.svg)](https://doi.org/10.5281/zenodo.1043973) 13 | 14 | ## Installation 15 | Clone the repository and run `python setup.py install` 16 | 17 | ## Usage 18 | The code can be run from the command line or within Python 19 | 20 | To run on the command line, type: 21 | 22 | `pyzogy --science-image "your-science-image" --reference-image "your-reference-image" --science-psf "your-science-psf" --reference-psf "your-reference-psf"` 23 | 24 | with any of the following options: 25 | 26 | ``` 27 | --science-mask "your-science-mask" 28 | --reference-mask "your-reference-mask" 29 | --science-saturation number 30 | --reference-saturation number 31 | --n-stamps "number" 32 | --normalization "science" or "reference" 33 | --gain-ratio number 34 | --gain-mask "mask-filename" 35 | --use-pixels 36 | --show 37 | --matched-filter "your-matched-filter-output" 38 | ``` 39 | To use in Python, type: 40 | ``` 41 | from PyZOGY.subtract import run_subtraction 42 | run_subtraction("your-science-image", "your-reference-image", "your-science-psf", "your-reference-psf") 43 | ``` 44 | 45 | with any of the following options: 46 | 47 | ``` 48 | science_mask = "your-science-mask" 49 | reference_mask = "youre-reference-mask" 50 | science_saturation = number 51 | reference_saturation = number 52 | n_stamps = number 53 | normalization = "science" or "reference" 54 | gain_ratio = number 55 | gain-mask = "mask-filename" 56 | use-pixels = boolean 57 | show = boolean 58 | matched-filter = "your-matched-filter-output" 59 | ``` 60 | 61 | ## Dependencies 62 | 63 | PyZOGY requires numpy, astropy, scipy, sep, matplotlib, and statsmodels 64 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from os import path 3 | from glob import glob 4 | 5 | setup( 6 | name='PyZOGY', 7 | version='0.0.1', 8 | author='David Guevel', 9 | author_email='guevel.david@gmail.com', 10 | entry_points = {'console_scripts': ['pyzogy = PyZOGY.__main__ : main']}, 11 | license='LICENSE.txt', 12 | description='PyZOGY is a Python implementation of the ZOGY algorithm.', 13 | install_requires=['numpy>=1.12', 'astropy', 'scipy', 'statsmodels', 'matplotlib', 'sep'], 14 | packages=['PyZOGY'], 15 | test_suite = 'PyZOGY.test' 16 | ) 17 | --------------------------------------------------------------------------------