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