SourceXtractorPlusPlus  0.19
SourceXtractor++, the next generation SExtractor
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
measurement_images.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 os
21 import re
22 import sys
23 
24 import _SourceXtractorPy as cpp
25 
26 if sys.version_info.major < 3:
27  from StringIO import StringIO
28 else:
29  from io import StringIO
30 
31 
32 class FitsFile(cpp.FitsFile):
33  def __init__(self, filename):
34  super(FitsFile, self).__init__(str(filename))
35  self.hdu_list = [i for i in self.image_hdus]
36 
37  def __iter__(self):
38  return iter(self.hdu_list)
39 
40  def get_headers(self, hdu):
41  d = {}
42  headers = super(FitsFile, self).get_headers(hdu)
43 
44  try:
45  it = iter(headers)
46  while it:
47  a = next(it)
48  d[a.key()] = headers[a.key()]
49  except StopIteration:
50  pass
51 
52  return d
53 
54 
55 class MeasurementImage(cpp.MeasurementImage):
56  """
57  A MeasurementImage is the processing unit for SourceXtractor++. Measurements and model fitting can be done
58  over one, or many, of them. It models the image, plus its associated weight file, PSF, etc.
59 
60  Parameters
61  ----------
62  fits_file : str or FitsFile object
63  The path to a FITS image, or an instance of FitsFile
64  psf_file : str
65  The path to a PSF. It can be either a FITS image, or a PSFEx model.
66  weight_file : str or FitsFile
67  The path to a FITS image with the pixel weights, or an instance of FitsFile
68  gain : float
69  Image gain. If None, `gain_keyword` will be used instead.
70  gain_keyword : str
71  Keyword for the header containing the gain.
72  saturation : float
73  Saturation value. If None, `saturation_keyword` will be used instead.
74  saturation_keyword : str
75  Keyword for the header containing the saturation value.
76  flux_scale : float
77  Flux scaling. Each pixel value will be multiplied by this. If None, `flux_scale_keyword` will be used
78  instead.
79  flux_scale_keyword : str
80  Keyword for the header containing the flux scaling.
81  weight_type : str
82  The type of the weight image. It must be one of:
83 
84  - none
85  The image itself is used to compute internally a constant variance (default)
86  - background
87  The image itself is used to compute internally a variance map
88  - rms
89  The weight image must contain a weight-map in units of absolute standard deviations
90  (in ADUs per pixel).
91  - variance
92  The weight image must contain a weight-map in units of relative variance.
93  - weight
94  The weight image must contain a weight-map in units of relative weights. The data are converted
95  to variance units.
96  weight_absolute : bool
97  If False, the weight map will be scaled according to an absolute variance map built from the image itself.
98  weight_scaling : float
99  Apply an scaling to the weight map.
100  weight_threshold : float
101  Pixels with weights beyond this value are treated just like pixels discarded by the masking process.
102  constant_background : float
103  If set a constant background of that value is assumed for the image instead of using automatic detection
104  image_hdu : int
105  For multi-extension FITS file specifies the HDU number for the image. Default 0 (primary HDU)
106  psf_hdu : int
107  For multi-extension FITS file specifies the HDU number for the psf. Defaults to the same value as image_hdu
108  weight_hdu : int
109  For multi-extension FITS file specifies the HDU number for the weight. Defaults to the same value as image_hdu
110  """
111 
112  def _set_checked(self, attr_name, value):
113  try:
114  setattr(self, attr_name, value)
115  except Exception:
116  expected_type = type(getattr(self, attr_name))
117  raise TypeError('Expecting {} for {}, got {}'.format(expected_type.__name__, attr_name,
118  type(value).__name__))
119 
120  def __init__(self, fits_file, psf_file=None, weight_file=None, gain=None,
121  gain_keyword='GAIN', saturation=None, saturation_keyword='SATURATE',
122  flux_scale=None, flux_scale_keyword='FLXSCALE',
123  weight_type='none', weight_absolute=False, weight_scaling=1.,
124  weight_threshold=None, constant_background=None,
125  image_hdu=0, psf_hdu=None, weight_hdu=None
126  ):
127  """
128  Constructor.
129  """
130  if isinstance(fits_file, FitsFile):
131  hdu_list = fits_file
132  file_path = fits_file.filename
133  else:
134  hdu_list = FitsFile(fits_file)
135  file_path = fits_file
136 
137  if isinstance(weight_file, FitsFile):
138  weight_file = weight_file.filename
139 
140  super(MeasurementImage, self).__init__(os.path.abspath(file_path),
141  os.path.abspath(psf_file) if psf_file else '',
142  os.path.abspath(weight_file) if weight_file else '')
143 
144  if image_hdu < 0 or (weight_hdu is not None and weight_hdu < 0) or (
145  psf_hdu is not None and psf_hdu < 0):
146  raise ValueError('HDU indices start at 0')
147 
148  self.meta = {
149  'IMAGE_FILENAME': self.file,
150  'PSF_FILENAME': self.psf_file,
151  'WEIGHT_FILENAME': self.weight_file
152  }
153 
154  self.meta.update(hdu_list.get_headers(image_hdu))
155 
156  if gain is not None:
157  self._set_checked('gain', gain)
158  elif gain_keyword in self.meta:
159  self.gain = float(self.meta[gain_keyword])
160  else:
161  self.gain = 0.
162 
163  if saturation is not None:
164  self._set_checked('saturation', saturation)
165  elif saturation_keyword in self.meta:
166  self.saturation = float(self.meta[saturation_keyword])
167  else:
168  self.saturation = 0.
169 
170  if flux_scale is not None:
171  self._set_checked('flux_scale', flux_scale)
172  elif flux_scale_keyword in self.meta:
173  self.flux_scale = float(self.meta[flux_scale_keyword])
174  else:
175  self.flux_scale = 1.
176 
177  self._set_checked('weight_type', weight_type)
178  self._set_checked('weight_absolute', weight_absolute)
179  self._set_checked('weight_scaling', weight_scaling)
180  if weight_threshold is None:
181  self.has_weight_threshold = False
182  else:
183  self.has_weight_threshold = True
184  self._set_checked('weight_threshold', weight_threshold)
185 
186  if constant_background is not None:
188  self._set_checked('constant_background_value', constant_background)
189  else:
190  self.is_background_constant = False
192 
193  self._set_checked('image_hdu', image_hdu)
194 
195  if psf_hdu is None:
196  self._set_checked('psf_hdu', image_hdu)
197  else:
198  self._set_checked('psf_hdu', psf_hdu)
199 
200  if weight_hdu is None:
201  self._set_checked('weight_hdu', image_hdu)
202  else:
203  self._set_checked('weight_hdu', weight_hdu)
204 
205  def __str__(self):
206  """
207  Returns
208  -------
209  str
210  Human readable representation for the object
211  """
212  return 'Image {}: {} / {}, PSF: {} / {}, Weight: {} / {}'.format(
213  self.id, self.meta['IMAGE_FILENAME'], self.image_hdu, self.meta['PSF_FILENAME'],
214  self.psf_hdu,
215  self.meta['WEIGHT_FILENAME'], self.weight_hdu)
216 
217 
219  def __init__(self, fits_file, psf_file=None, weight_file=None, gain=None,
220  gain_keyword='GAIN', saturation=None, saturation_keyword='SATURATE',
221  flux_scale=None, flux_scale_keyword='FLXSCALE',
222  weight_type='none', weight_absolute=False, weight_scaling=1.,
223  weight_threshold=None, constant_background=None,
224  image_hdu=0, psf_hdu=None, weight_hdu=None,
225  image_layer=0, weight_layer=0):
226  super(DataCubeSlice, self).__init__(fits_file, psf_file, weight_file, gain,
227  gain_keyword, saturation, saturation_keyword,
228  flux_scale, flux_scale_keyword,
229  weight_type, weight_absolute, weight_scaling,
230  weight_threshold, constant_background,
231  image_hdu, psf_hdu, weight_hdu)
232 
233  self.is_data_cube = True
234  self.image_layer = image_layer
235  self.weight_layer = weight_layer
236 
237  def __str__(self):
238  """
239  Returns
240  -------
241  str
242  Human readable representation for the object
243  """
244  return 'DataCubeSlice {}: {} / {} / {}, PSF: {} / {}, Weight: {} / {} / {}'.format(
245  self.id, self.meta['IMAGE_FILENAME'], self.image_hdu, self.image_layer,
246  self.meta['PSF_FILENAME'], self.psf_hdu,
247  self.meta['WEIGHT_FILENAME'], self.weight_hdu, self.weight_layer)
248 
249 
250 class ImageGroup(object):
251  """
252  Models the grouping of images. Measurement can *not* be made directly on instances of this type.
253  The configuration must be "frozen" before creating a MeasurementGroup
254 
255  See Also
256  --------
257  MeasurementGroup
258  """
259 
260  def __init__(self, **kwargs):
261  """
262  Constructor. It is not recommended to be used directly. Use instead load_fits_image or load_fits_images.
263  """
264  self.__images = []
265  self.__subgroups = None
266  self.__subgroup_names = set()
267  if len(kwargs) != 1 or ('images' not in kwargs and 'subgroups' not in kwargs):
268  raise ValueError('ImageGroup only takes as parameter one of "images" or "subgroups"')
269  key = list(kwargs.keys())[0]
270  if key == 'images':
271  if isinstance(kwargs[key], list):
272  self.__images = kwargs[key]
273  else:
274  self.__images = [kwargs[key]]
275  if key == 'subgroups':
276  self.__subgroups = kwargs[key]
277  for name, _ in self.__subgroups:
278  self.__subgroup_names.add(name)
279 
280  def __len__(self):
281  """
282  See Also
283  --------
284  is_leaf
285 
286  Returns
287  -------
288  int
289  How may subgroups or images are there in this group
290  """
291  if self.__subgroups:
292  return len(self.__subgroups)
293  else:
294  return len(self.__images)
295 
296  def __iter__(self):
297  """
298  Allows to iterate on the contained subgroups or images
299 
300  See Also
301  --------
302  is_leaf
303 
304  Returns
305  -------
306  iterator
307  """
308  if self.__subgroups:
309  return self.__subgroups.__iter__()
310  else:
311  return self.__images.__iter__()
312 
313  def split(self, grouping_method):
314  """
315  Splits the group in various subgroups, applying a filter on the contained images. If the group has
316  already been split, applies the split to each subgroup.
317 
318  Parameters
319  ----------
320  grouping_method : callable
321  A callable that receives as a parameter the list of contained images, and returns
322  a list of tuples, with the grouping key value, and the list of grouped images belonging to the given key.
323 
324  See Also
325  --------
326  ByKeyword
327  ByPattern
328 
329  Raises
330  -------
331  ValueError
332  If some images have not been grouped by the callable.
333  """
334  if self.__subgroups:
335  # if we are already subgrouped, apply the split to the subgroups
336  for _, sub_group in self.__subgroups:
337  sub_group.split(grouping_method)
338  else:
339  subgrouped_images = grouping_method(self.__images)
340  if sum(len(p[1]) for p in subgrouped_images) != len(self.__images):
341  self.__subgroups = None
342  raise ValueError('Some images were not grouped')
343  self.__subgroups = []
344  for k, im_list in subgrouped_images:
345  assert k not in self.__subgroup_names
346  self.__subgroup_names.add(k)
347  self.__subgroups.append((k, ImageGroup(images=im_list)))
348  self.__images = []
349 
350  def add_images(self, images):
351  """
352  Add new images to the group.
353 
354  Parameters
355  ----------
356  images : list of, or a single, MeasurementImage
357 
358  Raises
359  ------
360  ValueError
361  If the group has been split, no new images can be added.
362  """
363  if self.__subgroups is not None:
364  raise ValueError('ImageGroup is already subgrouped')
365  if isinstance(images, MeasurementImage):
366  self.__images.append(images)
367  else:
368  self.__images.extend(images)
369 
370  def add_subgroup(self, name, group):
371  """
372  Add a subgroup to a group.
373 
374  Parameters
375  ----------
376  name : str
377  The new of the new group
378 
379  group : ImageGroup
380  """
381  if self.__subgroups is None:
382  raise Exception('ImageGroup is not subgrouped yet')
383  if name in self.__subgroup_names:
384  raise Exception('Subgroup {} alread exists'.format(name))
385  self.__subgroup_names.add(name)
386  self.__subgroups.append((name, group))
387 
388  def is_leaf(self):
389  """
390  Returns
391  -------
392  bool
393  True if the group is a leaf group
394  """
395  return self.__subgroups is None
396 
397  def __getitem__(self, name):
398  """
399  Get a subgroup.
400 
401  Parameters
402  ----------
403  name : str
404  The name of the subgroup.
405 
406  Returns
407  -------
408  ImageGroup
409  The matching group.
410 
411  Raises
412  ------
413  ValueError
414  If the group has not been split.
415  KeyError
416  If the group has not been found.
417  """
418  if self.__subgroups is None:
419  raise ValueError('ImageGroup is not subgrouped yet')
420  try:
421  return next(x for x in self.__subgroups if x[0] == name)[1]
422  except StopIteration:
423  raise KeyError('Group {} not found'.format(name))
424 
425  def print(self, prefix='', show_images=False, file=sys.stderr):
426  """
427  Print a human-readable representation of the group.
428 
429  Parameters
430  ----------
431  prefix : str
432  Print each line with this prefix. Used internally for indentation.
433  show_images : bool
434  Show the images belonging to a leaf group.
435  file : file object
436  Where to print the representation. Defaults to sys.stderr
437  """
438  if self.__subgroups is None:
439  print('{}Image List ({})'.format(prefix, len(self.__images)), file=file)
440  if show_images:
441  for im in self.__images:
442  print('{} {}'.format(prefix, im), file=file)
443  else:
444  print('{}Image sub-groups: {}'.format(prefix,
445  ','.join(str(x) for x, _ in self.__subgroups)),
446  file=file)
447  for name, group in self.__subgroups:
448  print('{} {}:'.format(prefix, name), file=file)
449  group.print(prefix + ' ', show_images, file)
450 
451  def __str__(self):
452  """
453  Returns
454  -------
455  str
456  A human-readable representation of the group
457  """
458  string = StringIO()
459  self.print(show_images=True, file=string)
460  return string.getvalue()
461 
462 
463 class ByKeyword(object):
464  """
465  Callable that can be used to split an ImageGroup by a keyword value (i.e. FILTER).
466 
467  Parameters
468  ----------
469  key : str
470  FITS header keyword (i.e. FILTER)
471 
472  See Also
473  --------
474  ImageGroup.split
475  """
476 
477  def __init__(self, key):
478  """
479  Constructor.
480  """
481  self.__key = key
482 
483  def __call__(self, images):
484  """
485  Parameters
486  ----------
487  images : list of MeasurementImage
488  List of images to group
489 
490  Returns
491  -------
492  list of tuples of str and list of MeasurementImage
493  i.e. [
494  (R, [frame_r_01.fits, frame_r_02.fits]),
495  (G, [frame_g_01.fits, frame_g_02.fits])
496  ]
497  """
498  result = {}
499  for im in images:
500  if self.__key not in im.meta:
501  raise KeyError('The image {}[{}] does not contain the key {}'.format(
502  im.meta['IMAGE_FILENAME'], im.image_hdu, self.__key
503  ))
504  if im.meta[self.__key] not in result:
505  result[im.meta[self.__key]] = []
506  result[im.meta[self.__key]].append(im)
507  return [(k, result[k]) for k in result]
508 
509 
510 class ByPattern(object):
511  """
512  Callable that can be used to split an ImageGroup by a keyword value (i.e. FILTER), applying a regular
513  expression and using the first matching group as key.
514 
515  Parameters
516  ----------
517  key : str
518  FITS header keyword
519  pattern : str
520  Regular expression. The first matching group will be used as grouping key.
521 
522  See Also
523  --------
524  ImageGroup.split
525  """
526 
527  def __init__(self, key, pattern):
528  """
529  Constructor.
530  """
531  self.__key = key
532  self.__pattern = pattern
533 
534  def __call__(self, images):
535  """
536  Parameters
537  ----------
538  images : list of MeasurementImage
539  List of images to group
540 
541  Returns
542  -------
543  list of tuples of str and list of MeasurementImage
544  """
545  result = {}
546  for im in images:
547  if self.__key not in im.meta:
548  raise KeyError('The image {}[{}] does not contain the key {}'.format(
549  im.meta['IMAGE_FILENAME'], im.image_hdu, self.__key
550  ))
551  group = re.match(self.__pattern, im.meta[self.__key]).group(1)
552  if group not in result:
553  result[group] = []
554  result[group].append(im)
555  return [(k, result[k]) for k in result]
556 
557 
558 class MeasurementGroup(object):
559  """
560  Once an instance of this class is created from an ImageGroup, its configuration is "frozen". i.e.
561  no new images can be added, or no new grouping applied.
562 
563  Parameters
564  ----------
565  image_group : ImageGroup
566  """
567 
568  def __init__(self, image_group, is_subgroup=False):
569  """
570  Constructor.
571  """
572  self.__images = None
573  self.__subgroups = None
574  if image_group.is_leaf():
575  self.__images = [im for im in image_group]
576  else:
577  self.__subgroups = [(n, MeasurementGroup(g, is_subgroup=True)) for n, g in image_group]
578 
579  def __iter__(self):
580  """
581  Returns
582  -------
583  iterator
584  """
585  if self.__subgroups:
586  return self.__subgroups.__iter__()
587  else:
588  return self.__images.__iter__()
589 
590  def __getitem__(self, index):
591  """
592  The subgroup with the given name or image with the given index depending on whether this is a leaf group.
593 
594  Parameters
595  ----------
596  index : str or int
597  Subgroup name or image index
598 
599  Returns
600  -------
601  MeasurementGroup or MeasurementImage
602 
603  Raises
604  ------
605  KeyError
606  If we can't find what we want
607  """
608 
609  if self.__subgroups:
610  try:
611  return next(x for x in self.__subgroups if x[0] == index)[1]
612  except StopIteration:
613  raise KeyError('Group {} not found'.format(index))
614  else:
615  try:
616  return self.__images[index]
617  except:
618  raise KeyError('Image #{} not found'.format(index))
619 
620  def __len__(self):
621  """
622  Returns
623  -------
624  int
625  Number of subgroups, or images contained within the group
626  """
627  if self.__subgroups:
628  return len(self.__subgroups)
629  else:
630  return len(self.__images)
631 
632  def is_leaf(self):
633  """
634  Returns
635  -------
636  bool
637  True if the group is a leaf group
638  """
639  return self.__subgroups is None
640 
641  def print(self, prefix='', show_images=False, file=sys.stderr):
642  """
643  Print a human-readable representation of the group.
644 
645  Parameters
646  ----------
647  prefix : str
648  Print each line with this prefix. Used internally for indentation.
649  show_images : bool
650  Show the images belonging to a leaf group.
651  file : file object
652  Where to print the representation. Defaults to sys.stderr
653  """
654  if self.__images:
655  print('{}Image List ({})'.format(prefix, len(self.__images)), file=file)
656  if show_images:
657  for im in self.__images:
658  print('{} {}'.format(prefix, im), file=file)
659  if self.__subgroups:
660  print('{}Measurement sub-groups: {}'.format(prefix, ','.join(
661  x for x, _ in self.__subgroups)), file=file)
662  for name, group in self.__subgroups:
663  print('{} {}:'.format(prefix, name), file=file)
664  group.print(prefix + ' ', show_images, file=file)
665 
666  def __str__(self):
667  """
668  Returns
669  -------
670  str
671  A human-readable representation of the group
672  """
673  string = StringIO()
674  self.print(show_images=True, file=string)
675  return string.getvalue()
ELEMENTS_API auto join(Args &&...args) -> decltype(joinPath(std::forward< Args >(args)...))