SourceXtractorPlusPlus 0.21
SourceXtractor++, the next generation SExtractor
Loading...
Searching...
No Matches
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
18from __future__ import division, print_function
19
20import os
21import re
22import sys
23
24import _SourceXtractorPy as cpp
25
26if sys.version_info.major < 3:
27 from StringIO import StringIO
28else:
29 from io import StringIO
30
31
32class 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
55class 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:
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'],
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.idid, self.meta['IMAGE_FILENAME'], self.image_hduimage_hdu, self.image_layer,
246 self.meta['PSF_FILENAME'], self.psf_hdupsf_hdu,
247 self.meta['WEIGHT_FILENAME'], self.weight_hduweight_hdu, self.weight_layer)
248
249
250class 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
463class 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
510class 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
558class 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()
__init__(self, fits_file, psf_file=None, weight_file=None, gain=None, gain_keyword='GAIN', saturation=None, saturation_keyword='SATURATE', flux_scale=None, flux_scale_keyword='FLXSCALE', weight_type='none', weight_absolute=False, weight_scaling=1., weight_threshold=None, constant_background=None, image_hdu=0, psf_hdu=None, weight_hdu=None, image_layer=0, weight_layer=0)
print(self, prefix='', show_images=False, file=sys.stderr)
print(self, prefix='', show_images=False, file=sys.stderr)
__init__(self, fits_file, psf_file=None, weight_file=None, gain=None, gain_keyword='GAIN', saturation=None, saturation_keyword='SATURATE', flux_scale=None, flux_scale_keyword='FLXSCALE', weight_type='none', weight_absolute=False, weight_scaling=1., weight_threshold=None, constant_background=None, image_hdu=0, psf_hdu=None, weight_hdu=None)