SourceXtractorPlusPlus  0.19
SourceXtractor++, the next generation SExtractor
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
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
18 from __future__ import division, print_function
19 
20 import sys
21 
22 import _SourceXtractorPy as cpp
23 
24 from .measurement_images import DataCubeSlice, FitsFile, ImageGroup, MeasurementGroup, \
25  MeasurementImage
26 from .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
53 
54  @property
55  def aperture_columns(self):
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 
425 global_measurement_config = MeasurementConfig()
Image< SeFloat > MeasurementImage
Alias for the measurement image, to make easier its type modification.
Definition: Image.h:82