18 from __future__
import division, print_function
24 from astropy.io
import fits
26 import _SourceXtractorPy
as cpp
28 if sys.version_info.major < 3:
29 from StringIO
import StringIO
31 from io
import StringIO
34 measurement_images = {}
39 A MeasurementImage is the processing unit for SourceXtractor++. Measurements and model fitting can be done
40 over one, or many, of them. It models the image, plus its associated weight file, PSF, etc.
45 The path to a FITS image. Only primary HDU images are supported.
47 The path to a PSF. It can be either a FITS image, or a PSFEx model.
49 The path to a FITS image with the pixel weights.
51 Image gain. If None, `gain_keyword` will be used instead.
53 Keyword for the header containing the gain.
55 Saturation value. If None, `saturation_keyword` will be used instead.
56 saturation_keyword : str
57 Keyword for the header containing the saturation value.
59 Flux scaling. Each pixel value will be multiplied by this. If None, `flux_scale_keyword` will be used
61 flux_scale_keyword : str
62 Keyword for the header containing the flux scaling.
64 The type of the weight image. It must be one of:
67 The image itself is used to compute internally a constant variance (default)
69 The image itself is used to compute internally a variance map
71 The weight image must contain a weight-map in units of absolute standard deviations
74 The weight image must contain a weight-map in units of relative variance.
76 The weight image must contain a weight-map in units of relative weights. The data are converted
78 weight_absolute : bool
79 If False, the weight map will be scaled according to an absolute variance map built from the image itself.
80 weight_scaling : float
81 Apply an scaling to the weight map.
82 weight_threshold : float
83 Pixels with weights beyond this value are treated just like pixels discarded by the masking process.
84 constant_background : float
85 If set a constant background of that value is assumed for the image instead of using automatic detection
87 For multi-extension FITS file specifies the HDU number for the image. Default 1 (primary HDU)
89 For multi-extension FITS file specifies the HDU number for the psf. Defaults to the same value as image_hdu
91 For multi-extension FITS file specifies the HDU number for the weight. Defaults to the same value as image_hdu
94 def __init__(self, fits_file, psf_file=None, weight_file=None, gain=None,
95 gain_keyword=
'GAIN', saturation=
None, saturation_keyword=
'SATURATE',
96 flux_scale=
None, flux_scale_keyword=
'FLXSCALE',
97 weight_type=
'none', weight_absolute=
False, weight_scaling=1.,
98 weight_threshold=
None, constant_background=
None,
99 image_hdu=1, psf_hdu=
None, weight_hdu=
None
104 super(MeasurementImage, self).
__init__(os.path.abspath(fits_file),
105 os.path.abspath(psf_file)
if psf_file
else '',
106 os.path.abspath(weight_file)
if weight_file
else '')
108 if image_hdu <= 0
or (weight_hdu
is not None and weight_hdu <= 0)
or (psf_hdu
is not None and psf_hdu <= 0):
109 raise ValueError(
'HDU indexes start at 1')
112 'IMAGE_FILENAME': self.file,
113 'PSF_FILENAME': self.psf_file,
114 'WEIGHT_FILENAME': self.weight_file
116 hdu_list = fits.open(fits_file)
117 hdu_meta = hdu_list[image_hdu-1].header
119 self.
meta[key] = hdu_meta[key]
125 elif gain_keyword
in self.
meta:
126 self.
gain = float(self.
meta[gain_keyword])
130 if saturation
is not None:
132 elif saturation_keyword
in self.
meta:
137 if flux_scale
is not None:
139 elif flux_scale_keyword
in self.
meta:
147 if weight_threshold
is None:
153 if constant_background
is not None:
168 if weight_hdu
is None:
173 global measurement_images
174 measurement_images[self.id] = self
178 pre, ext = os.path.splitext(filename)
179 header_file = pre +
".head"
182 if os.path.exists(header_file):
183 print(
"processing ascii header file: " + header_file)
185 with open(header_file)
as f:
187 line = re.sub(
"\\s*#.*",
"", line)
188 line = re.sub(
"\\s*$",
"", line)
193 if line.upper() ==
"END":
195 elif current_hdu == hdu:
196 m = re.match(
"(.+)=(.+)", line)
198 self.
meta[m.group(1)] = m.group(2)
205 Human readable representation for the object
207 return 'Image {}: {} / {}, PSF: {} / {}, Weight: {} / {}'.format(
214 Print a human-readable representation of the configured measurement images.
219 Where to print the representation. Defaults to sys.stderr
221 print(
'Measurement images:', file=file)
222 for i
in measurement_images:
223 im = measurement_images[i]
224 print(
'Image {}'.format(i), file=file)
225 print(
' File: {}'.format(im.file), file=file)
226 print(
' PSF: {}'.format(im.psf_file), file=file)
227 print(
' Weight: {}'.format(im.weight_file), file=file)
232 Models the grouping of images. Measurement can *not* be made directly on instances of this type.
233 The configuration must be "frozen" before creating a MeasurementGroup
242 Constructor. It is not recommended to be used directly. Use instead load_fits_image or load_fits_images.
247 if len(kwargs) != 1
or (
'images' not in kwargs
and 'subgroups' not in kwargs):
248 raise ValueError(
'ImageGroup only takes as parameter one of "images" or "subgroups"')
249 key = list(kwargs.keys())[0]
251 if isinstance(kwargs[key], list):
255 if key ==
'subgroups':
258 self.__subgroup_names.add(name)
269 How may subgroups or images are there in this group
278 Allows to iterate on the contained subgroups or images
289 return self.__subgroups.__iter__()
291 return self.__images.__iter__()
295 Splits the group in various subgroups, applying a filter on the contained images. If the group has
296 already been split, applies the split to each subgroup.
300 grouping_method : callable
301 A callable that receives as a parameter the list of contained images, and returns
302 a list of tuples, with the grouping key value, and the list of grouped images belonging to the given key.
312 If some images have not been grouped by the callable.
317 sub_group.split(grouping_method)
319 subgrouped_images = grouping_method(self.
__images)
320 if sum(len(p[1])
for p
in subgrouped_images) != len(self.
__images):
322 raise ValueError(
'Some images were not grouped')
324 for k, im_list
in subgrouped_images:
326 self.__subgroup_names.add(k)
327 self.__subgroups.append((k,
ImageGroup(images=im_list)))
332 Add new images to the group.
336 images : list of, or a single, MeasurementImage
341 If the group has been split, no new images can be added.
344 raise ValueError(
'ImageGroup is already subgrouped')
345 if isinstance(images, MeasurementImage):
346 self.__images.append(images)
348 self.__images.extend(images)
352 Add a subgroup to a group.
357 The new of the new group
362 raise Exception(
'ImageGroup is not subgrouped yet')
364 raise Exception(
'Subgroup {} alread exists'.format(name))
365 self.__subgroup_names.add(name)
366 self.__subgroups.append((name, group))
373 True if the group is a leaf group
384 The name of the subgroup.
394 If the group has not been split.
396 If the group has not been found.
399 raise ValueError(
'ImageGroup is not subgrouped yet')
401 return next(x
for x
in self.
__subgroups if x[0] == name)[1]
402 except StopIteration:
403 raise KeyError(
'Group {} not found'.format(name))
405 def print(self, prefix='', show_images=False, file=sys.stderr):
407 Print a human-readable representation of the group.
412 Print each line with this prefix. Used internally for indentation.
414 Show the images belonging to a leaf group.
416 Where to print the representation. Defaults to sys.stderr
419 print(
'{}Image List ({})'.format(prefix, len(self.
__images)), file=file)
422 print(
'{} {}'.format(prefix, im), file=file)
424 print(
'{}Image sub-groups: {}'.format(prefix,
','.join(str(x)
for x, _
in self.
__subgroups)), file=file)
426 print(
'{} {}:'.format(prefix, name), file=file)
427 group.print(prefix +
' ', show_images, file)
434 A human-readable representation of the group
437 self.
print(show_images=
True, file=string)
438 return string.getvalue()
448 for key, value
in kwargs.items():
449 if key
not in self.
kwargs:
450 mismatch.append(
'{} {} != undefined'.format(key, value))
451 elif self.
kwargs[key] != value:
452 mismatch.append(
'{} {} != {}'.format(key, value, self.
kwargs[key]))
458 """Creates an image group with the images of a (possibly multi-HDU) single FITS file.
460 If image is multi-hdu, psf and weight can either be multi hdu or lists of individual files.
462 In any case, they are matched in order and HDUs not containing images (two dimensional arrays) are ignored.
464 :param image: The FITS file containing the image(s)
465 :param psf: psf file or list of psf files
466 :param weight: FITS file for the weight image or a list of such files
468 :return: A ImageGroup representing the images
471 hdu_list = [i
for i, hdu
in enumerate(fits.open(image))
if hdu.is_image
and hdu.header[
'NAXIS'] == 2]
474 if isinstance(psf, list):
475 if len(psf) != len(hdu_list):
476 raise ValueError(
"The number of psf files must match the number of images!")
478 psf_hdu_list = [0] * len(psf_list)
480 psf_list = [psf] * len(hdu_list)
481 psf_hdu_list = range(len(hdu_list))
484 if isinstance(weight, list):
485 if len(weight) != len(hdu_list):
486 raise ValueError(
"The number of weight files must match the number of images!")
488 weight_hdu_list = [0] * len(weight_list)
491 weight_list = [
None] * len(hdu_list)
492 weight_hdu_list = [0] * len(weight_list)
494 weight_hdu_list = [i
for i, hdu
in enumerate(fits.open(weight))
if hdu.is_image
and hdu.header[
'NAXIS'] == 2]
495 weight_list = [weight] * len(hdu_list)
498 for hdu, psf_file, psf_hdu, weight_file, weight_hdu
in zip(
499 hdu_list, psf_list, psf_hdu_list, weight_list, weight_hdu_list):
501 image_hdu=hdu+1, psf_hdu=psf_hdu+1, weight_hdu=weight_hdu+1, **kwargs))
506 """Creates an image group for the given images.
511 A list of relative paths to the images FITS files. Can also be single string in which case,
512 this function acts like load_fits_image
514 A list of relative paths to the PSF FITS files (optional). It must match the length of image_list or be None.
515 weights : list of str
516 A list of relative paths to the weight files (optional). It must match the length of image_list or be None.
521 A ImageGroup representing the images
526 In case of mismatched list of files
529 if isinstance(images, list):
531 raise ValueError(
"An empty list passed to load_fits_images")
533 psfs = psfs
or [
None] * len(images)
534 weights = weights
or [
None] * len(images)
536 if not isinstance(psfs, list)
or len(psfs) != len(images):
537 raise ValueError(
"The number of image files and psf files must match!")
539 if not isinstance(weights, list)
or len(weights) != len(images):
540 raise ValueError(
"The number of image files and weight files must match!")
543 for f, p, w
in zip(images, psfs, weights):
556 Callable that can be used to split an ImageGroup by a keyword value (i.e. FILTER).
561 FITS header keyword (i.e. FILTER)
578 images : list of MeasurementImage
579 List of images to group
583 list of tuples of str and list of MeasurementImage
585 (R, [frame_r_01.fits, frame_r_02.fits]),
586 (G, [frame_g_01.fits, frame_g_02.fits])
591 if self.
__key not in im.meta:
592 raise KeyError(
'The image {}[{}] does not contain the key {}'.format(
593 im.meta[
'IMAGE_FILENAME'], im.image_hdu, self.
__key
595 if im.meta[self.
__key]
not in result:
596 result[im.meta[self.
__key]] = []
597 result[im.meta[self.
__key]].append(im)
598 return [(k, result[k])
for k
in result]
603 Callable that can be used to split an ImageGroup by a keyword value (i.e. FILTER), applying a regular
604 expression and using the first matching group as key.
611 Regular expression. The first matching group will be used as grouping key.
629 images : list of MeasurementImage
630 List of images to group
634 list of tuples of str and list of MeasurementImage
638 if self.
__key not in im.meta:
639 raise KeyError(
'The image {}[{}] does not contain the key {}'.format(
640 im.meta[
'IMAGE_FILENAME'], im.image_hdu, self.
__key
643 if group
not in result:
645 result[group].append(im)
646 return [(k, result[k])
for k
in result]
651 Once an instance of this class is created from an ImageGroup, its configuration is "frozen". i.e.
652 no new images can be added, or no new grouping applied.
656 image_group : ImageGroup
661 def __init__(self, image_group, is_subgroup=False):
667 if image_group.is_leaf():
668 self.
__images = [im
for im
in image_group]
672 MeasurementGroup._all_groups.append(self)
681 return self.__subgroups.__iter__()
683 return self.__images.__iter__()
687 The subgroup with the given name or image with the given index depending on whether this is a leaf group.
692 Subgroup name or image index
696 MeasurementGroup or MeasurementImage
701 If we can't find what we want
706 return next(x
for x
in self.
__subgroups if x[0] == index)[1]
707 except StopIteration:
708 raise KeyError(
'Group {} not found'.format(index))
713 raise KeyError(
'Image #{} not found'.format(index))
720 Number of subgroups, or images contained within the group
732 True if the group is a leaf group
736 def print(self, prefix='', show_images=False, file=sys.stderr):
738 Print a human-readable representation of the group.
743 Print each line with this prefix. Used internally for indentation.
745 Show the images belonging to a leaf group.
747 Where to print the representation. Defaults to sys.stderr
750 print(
'{}Image List ({})'.format(prefix, len(self.
__images)), file=file)
753 print(
'{} {}'.format(prefix, im), file=file)
755 print(
'{}Measurement sub-groups: {}'.format(prefix,
','.join(
758 print(
'{} {}:'.format(prefix, name), file=file)
759 group.print(prefix +
' ', show_images, file=file)
766 A human-readable representation of the group
769 self.
print(show_images=
True, file=string)
770 return string.getvalue()