SourceXtractorPlusPlus 1.0.3
SourceXtractor++, the next generation SExtractor
Loading...
Searching...
No Matches
measurement_config.py
Go to the documentation of this file.
1# -*- coding: utf-8 -*-
2
3# Copyright © 2019-2022 Université de Genève, LMU Munich - Faculty of Physics, IAP-CNRS/Sorbonne Université
4#
5# This library is free software; you can redistribute it and/or modify it under
6# the terms of the GNU Lesser General Public License as published by the Free
7# Software Foundation; either version 3.0 of the License, or (at your option)
8# any later version.
9#
10# This library is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
13# details.
14#
15# You should have received a copy of the GNU Lesser General Public License
16# along with this library; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18from __future__ import division, print_function
19
20import sys
21
22import _SourceXtractorPy as cpp
23
24from .measurement_images import DataCubeSlice, FitsFile, ImageGroup, MeasurementGroup, \
25 MeasurementImage
26from .model_fitting import ModelFitting, ParameterBase
27
28
30 def __init__(self):
33 self._used_names = set()
36 self._column_map = {
37 ParameterBase: self._model_fitting_parameter_columns,
38 cpp.Aperture: self._aperture_columns
39 }
41
42 @property
44 return self._apertures_for_image
45
46 @property
47 def measurement_images(self):
48 return self._measurement_images
49
50 @property
54 @property
56 return self._aperture_columns
57
58 @property
59 def model_fitting(self):
60 return self._model_fitting
61
62 def add_measurement_image(self, image):
63 if isinstance(image, MeasurementImage):
64 if image.id not in self._measurement_images:
65 self._measurement_images[image.id] = image
66 elif image.is_leaf():
67 for member in image:
68 self.add_measurement_image(member)
69 else:
70 for _, subgroup in image:
71 self.add_measurement_image(subgroup)
72
73 def load_fits_image(self, image, psf=None, weight=None, **kwargs):
74 """
75 Creates an image group with the images of a (possibly multi-HDU) single FITS file.
76
77 If image is multi-hdu, psf and weight can either be multi hdu or lists of individual files.
78
79 In any case, they are matched in order and HDUs not containing images (two dimensional arrays) are ignored.
80
81 :param image: The filename of the FITS file containing the image(s)
82 :param psf: psf file or list of psf files
83 :param weight: FITS file for the weight image or a list of such files
84
85 :return: A ImageGroup representing the images
86 """
87
88 image_file = FitsFile(image)
89 if "image_hdu" in kwargs.keys():
90 image_hdu_list = [kwargs.pop("image_hdu")]
91 else:
92 image_hdu_list = image_file.hdu_list
93
94 # handles the PSFs
95 if isinstance(psf, list):
96 if len(psf) != len(image_hdu_list):
97 raise ValueError("The number of psf files must match the number of images!")
98 psf_file_list = psf
99 psf_hdu_list = [0] * len(psf_file_list)
100 else:
101 if "psf_hdu" in kwargs.keys():
102 psf_hdu_list = [kwargs.pop("psf_hdu")] * len(image_hdu_list)
103 else:
104 psf_hdu_list = range(len(image_hdu_list))
105 psf_file_list = [psf] * len(image_hdu_list)
106
107 # handles the weight maps
108 if isinstance(weight, list):
109 if len(weight) != len(image_hdu_list):
110 raise ValueError("The number of weight files must match the number of images!")
111 weight_file_list = weight
112 weight_hdu_list = [0] * len(weight_file_list)
113 elif weight is None:
114 weight_file_list = [None] * len(image_hdu_list)
115 weight_hdu_list = [0] * len(weight_file_list)
116 else:
117 weight_file = FitsFile(weight)
118 if "weight_hdu" in kwargs.keys():
119 weight_hdu_list = [kwargs.pop("weight_hdu")] * len(image_hdu_list)
120 else:
121 weight_hdu_list = weight_file.hdu_list
122 weight_file_list = [weight_file] * len(image_hdu_list)
123
124 image_list = []
125 for hdu, psf_file, psf_hdu, weight_file, weight_hdu in zip(
126 image_hdu_list, psf_file_list, psf_hdu_list, weight_file_list, weight_hdu_list):
127 image_list.append(MeasurementImage(image_file, psf_file, weight_file,
128 image_hdu=hdu, psf_hdu=psf_hdu,
129 weight_hdu=weight_hdu,
130 **kwargs))
131 for img in image_list:
132 self._measurement_images[img.id] = img
133 return ImageGroup(images=image_list)
134
135 def load_fits_images(self, images, psfs=None, weights=None, **kwargs):
136 """Creates an image group for the given images.
137
138 Parameters
139 ----------
140 images : list of str
141 A list of relative paths to the images FITS files. Can also be single string in which case,
142 this function acts like load_fits_image
143 psfs : list of str
144 A list of relative paths to the PSF FITS files (optional). It must match the length of image_list or be None.
145 weights : list of str
146 A list of relative paths to the weight files (optional). It must match the length of image_list or be None.
147
148 Returns
149 -------
150 ImageGroup
151 A ImageGroup representing the images
152
153 Raises
154 ------
155 ValueError
156 In case of mismatched list of files
157 """
158
159 if isinstance(images, list):
160 if len(images) == 0:
161 raise ValueError("An empty list passed to load_fits_images")
162
163 psfs = psfs or [None] * len(images)
164 weights = weights or [None] * len(images)
165
166 if not isinstance(psfs, list) or len(psfs) != len(images):
167 raise ValueError("The number of image files and psf files must match!")
168
169 if not isinstance(weights, list) or len(weights) != len(images):
170 raise ValueError("The number of image files and weight files must match!")
171
172 groups = []
173 for f, p, w in zip(images, psfs, weights):
174 groups.append(self.load_fits_image(f, p, w, **kwargs))
175
176 image_list = []
177 for g in groups:
178 image_list += g
179 for img in image_list:
180 self._measurement_images[img.id] = img
181 return ImageGroup(images=image_list)
182 else:
183 return self.load_fits_image(images, psfs, weights, **kwargs)
184
185 def load_fits_data_cube(self, image, psf=None, weight=None, image_cube_hdu=-1,
186 weight_cube_hdu=-1, **kwargs):
187 """
188 Creates an image group with the images of a data cube.
189
190 weight can be a matching datacube, multi-hdu or list of individual images
191 psf can be a multi-hdu or list of individual images to be matched
192
193 In any case, they are matched in order and HDUs not containing images are ignored.
194
195 :param image: The filename of the FITS file containing the image datacube
196 :param psf: psf file or list of psf files
197 :param weight: FITS file contianing a weight datacube, a MEF contianing the weights
198 or a list of such files
199 :param image_cube_hdu: hdu containing the image datacube, default = first HDU containing image data
200 :param weight_cube_hdu: hdu containing the weight datacube, default = first HDU containing image data
201
202 :return: A ImageGroup representing the images
203 """
204
205 image_cube_file = FitsFile(image)
206
207 if image_cube_hdu < 0:
208 cube_hdu = image_cube_file.hdu_list[0]
209 else:
210 cube_hdu = image_cube_hdu
211
212 dims = image_cube_file.get_dimensions(cube_hdu)
213 if len(dims) != 3:
214 raise ValueError("Not a data cube!")
215 cube_size = dims[2]
216 image_layer_list = range(cube_size)
217
218 # handles the PSFs
219 if isinstance(psf, list):
220 if len(psf) != cube_size:
221 raise ValueError("The number of psf files must match the number of images!")
222 psf_file_list = psf
223 psf_hdu_list = [0] * cube_size
224 else:
225 psf_file_list = [psf] * cube_size
226 psf_hdu_list = range(cube_size)
227
228 # handles the weight maps
229 if isinstance(weight, list):
230 if len(weight) != cube_size:
231 raise ValueError("The number of weight files must match the number of images!")
232 weight_file_list = weight
233 weight_hdu_list = [0] * cube_size
234 weight_layer_list = [0] * cube_size
235 elif weight is None:
236 weight_file_list = [None] * cube_size
237 weight_hdu_list = [0] * cube_size
238 weight_layer_list = [0] * cube_size
239 else:
240 weight_fits_file = FitsFile(weight)
241 if weight_cube_hdu < 0:
242 weight_hdu = weight_fits_file.hdu_list[0]
243 else:
244 weight_hdu = weight_cube_hdu
245
246 weight_dims = weight_fits_file.get_dimensions(weight_hdu)
247 if len(weight_dims) == 3:
248 # handle weight as data cube
249 if dims[2] != cube_size:
250 raise ValueError("The weight map cube doesn't match the image cube")
251
252 weight_file_list = [weight_fits_file] * cube_size
253 weight_hdu_list = [weight_hdu] * cube_size
254 weight_layer_list = range(cube_size)
255 else:
256 # weight is a FITS without a datacube, assume MEF
257 weight_file_list = [weight_fits_file] * cube_size
258 weight_hdu_list = weight_fits_file.hdu_list
259 weight_layer_list = [0] * cube_size
260
261 image_list = []
262 for psf_file, psf_hdu, weight_file, weight_hdu, image_layer, weight_layer in zip(
263 psf_file_list, psf_hdu_list, weight_file_list, weight_hdu_list, image_layer_list,
264 weight_layer_list):
265 image_list.append(DataCubeSlice(image_cube_file, psf_file, weight_file,
266 image_hdu=cube_hdu, psf_hdu=psf_hdu,
267 weight_hdu=weight_hdu,
268 image_layer=image_layer, weight_layer=weight_layer,
269 **kwargs))
270
271 for img in image_list:
272 self._measurement_images[img.id] = img
273 return ImageGroup(images=image_list)
274
275 def print_measurement_images(self, file=sys.stderr):
276 """
277 Print a human-readable representation of the configured measurement images.
278
279 Parameters
280 ----------
281 file : file object
282 Where to print the representation. Defaults to sys.stderr
283 """
284 print('Measurement images:', file=file)
285 for i in self._measurement_images:
286 im = self._measurement_images[i]
287 print('Image {}'.format(i), file=file)
288 print(' File: {}'.format(im.file), file=file)
289 print(' PSF: {}'.format(im.psf_file), file=file)
290 print(' Weight: {}'.format(im.weight_file), file=file)
291
292 def add_aperture_photometry(self, target, apertures):
293 """
294 Flux measurement from the image above the background inside a circular aperture.
295
296 Parameters
297 ----------
298 target : MeasurementImage object, or leaf MeasurementGroup object with a single image, or a list of either
299 Target images on which to measure the aperture photometry. Leaf MeasurementGroup with a single image
300 are accepted as a convenience.
301
302 apertures : float, or list of float
303 Diameter of the aperture. As different MeasurementImage may not be aligned, nor have equivalent pixel size,
304 the aperture is interpreted as diameter in pixels of a circle on the detection image.
305 A transformation will be applied for each frame, so the covered area is equivalent.
306
307 Returns
308 -------
309 list of Aperture objects
310 An Aperture object is an internal representation of a property on the measurement frame that contains the
311 apertures. To actually get the measurements on the output catalog, you need to add explicitly them to the
312 output.
313
314 See Also
315 --------
316 add_output_column
317
318 Notes
319 -----
320 This property will generate five columns with the prefix specified by `add_output_column`:
321 - ``_flux`` and ``_flux_err``, for the flux and its associated error
322 - ``_mag`` and ``_mag_err``, for the magnitude and its associated error
323 - ``_flags``, to mark, for instance, saturation, boundary conditions, etc.
324
325 For M apertures and N images, the cells on the output column will be an array of MxN fluxes.
326
327 Examples
328 --------
329 >>> context = MeasurementConfig()
330 >>> measurement_group = MeasurementGroup(load_fits_images(frames, psfs))
331 >>> all_apertures = []
332 >>> for img in measurement_group:
333 >>> all_apertures.extend(context.add_aperture_photometry(img, [5, 10, 20]))
334 >>> context.add_output_column('aperture', all_apertures)
335 """
336 if not isinstance(target, list):
337 target = [target]
338 if not isinstance(apertures, list):
339 apertures = [apertures]
340
341 apertures = [float(a) for a in apertures]
342 outputs = []
343
344 for t in target:
345 if isinstance(t, MeasurementGroup):
346 if not t.is_leaf():
347 raise Exception('The MeasurementGroup is not a leaf')
348 elif len(t) != 1:
349 raise Exception('The MeasurementGroup contains {} images'.format(len(t)))
350 t = [i for i in t][0]
351
352 if not isinstance(t, MeasurementImage):
353 raise Exception(
354 'Only MeasurementImage supported as targets, got {}'.format(type(t)))
355 else:
356 if t.id in self._apertures_for_image:
357 raise Exception('Apertures already set for the image {}'.format(t.id))
358 self._apertures_for_image[t.id] = cpp.Aperture(apertures)
359 outputs.append(self._apertures_for_image[t.id])
360
361 return outputs
362
363 def print_output_columns(self, file=sys.stderr):
364 """
365 Print a human-readable representation of the configured output columns.
366
367 Parameters
368 ----------
369 file : file object
370 Where to print the representation. Defaults to sys.stderr
371 """
373 print('Model fitting parameter outputs:', file=file)
374 for n, ids in self._model_fitting_parameter_columns:
375 print(' {} : {}'.format(n, ids), file=file)
376 if self._aperture_columns:
377 print('Aperture outputs:', file=file)
378 for n, ids in self._aperture_columns:
379 print(' {} : {}'.format(n, ids), file=file)
380
381 def add_output_column(self, name, params):
382 """
383 Add a new set of columns to the output catalog.
384
385 Parameters
386 ----------
387 name : str
388 Name/prefix of the new set of columns
389 params : list of columns
390 List of properties to add to the output with the given name/prefix. They must be subtype
391 of one of the known ones: ParameterBase for model fitting, or Aperture for aperture photometry.
392
393 Raises
394 ------
395 ValueError
396 If the name has already been used
397 TypeError
398 If any of the parameters are not of a known type (see params)
399
400 See Also
401 --------
402 aperture.add_aperture_photometry
403 model_fitting.ParameterBase
404 """
405 if name in self._used_names:
406 raise ValueError('Column {} is already set'.format(name))
407 self._used_names.add(name)
408
409 if not isinstance(params, list):
410 params = [params]
411 param_type = type(params[0])
412
413 known_subclass = False
414 for base in self._column_map:
415 if issubclass(param_type, base):
416 self._column_map[base].append((name, params))
417 known_subclass = True
418 if issubclass(param_type, ParameterBase):
419 self.model_fitting._register_parameter(params[0])
420
421 if not known_subclass:
422 raise TypeError('{} is not a known column type'.format(str(param_type)))
423
424
425global_measurement_config = MeasurementConfig()
load_fits_data_cube(self, image, psf=None, weight=None, image_cube_hdu=-1, weight_cube_hdu=-1, **kwargs)
load_fits_images(self, images, psfs=None, weights=None, **kwargs)
load_fits_image(self, image, psf=None, weight=None, **kwargs)