Example #1
0
def run(img,
        coordinates,
        epoch,
        aperture,
        annulus,
        dannulus,
        maximum,
        datek,
        timek,
        exptimek,
        uncimgk,
        cbox=0):
    """ Do photometry on a FITS image.

    This convenience function does photometry on a FITSImage object, applying
    proper-motion correction to a series of astronomical objects and measuring
    them. The FITS image must have been previously calibrated astrometrically,
    so that right ascensions and declinations are meaningful. Returns a QPhot
    object, using None as the magnitude of those astronomical objects that are
    INDEF (i.e., so faint that qphot could not measure anything) and positive
    infinity if they are saturated (i.e., if one or more pixels in the aperture
    are above the saturation level).

    Arguments:
    img - the fitsimage.FITSImage object on which to do photometry.
    coordinates - an iterable of astromatic.Coordinates objects, one for each
                  astronomical object to be measured.
    epoch - the epoch of the coordinates of the astronomical objects, used to
            compute the proper-motion correction. Must be an integer, such as
            2000 for J2000.
    aperture - the aperture radius, in pixels.
    annulus - the inner radius of the sky annulus, in pixels.
    dannulus - the width of the sky annulus, in pixels.
    maximum - number of ADUs at which saturation arises. If one or more pixels
              in the aperture are above this value, the magnitude of the
              astronomical object is set to positive infinity. For coadded
              observations, the effective saturation level is obtained by
              multiplying this value by the number of coadded images.
    datek - the image header keyword containing the date of the observation, in
            the format specified in the FITS Standard. The old date format was
            'yy/mm/dd' and may be used only for dates from 1900 through 1999.
            The new Y2K compliant date format is 'yyyy-mm-dd' or
            'yyyy-mm-ddTHH:MM:SS[.sss]'. This keyword is not necessary if none
            of the astromatic.Coordinates objects have a known proper motion.
            When that is the case, it is not even read from the FITS header.
    timek - the image header keyword containing the time at which the
            observation started, in the format HH:MM:SS[.sss]. This keyword is
            not necessary (and, therefore, is ignored) if the time is included
            directly as part of the 'datek' keyword value with the format
            'yyyy-mm-ddTHH:MM:SS[.sss]'. As with 'datek', if no object has a
            proper motion this keyword is ignored and not read from the header.
    exptimek - the image header keyword containing the exposure time. Needed
               by qphot in order to normalize the computed magnitudes to an
               exposure time of one time unit.
    uncimgk - the image header keyword containing the path to the image used to
              check for saturation. It is expected to be the original FITS file
              (that is, before any calibration step, since corrections such as
              flat-fielding may move a saturated pixel below the saturation
              level) of the very image on which photometry is done. If this
              argument is set to an empty string or None, saturation is checked
              for on the same FITS image used for photometry, 'img'.
    cbox - the width of the centering box, in pixels. Accurate centers for each
           astronomical object are computed using the centroid centering
           algorithm. This means that, unless this argument is zero (the
           default value), photometry is not done exactly on the specified
           coordinates, but instead where IRAF has determined that the actual,
           accurate center of each object is. This is usually a good thing, and
           helps improve the photometry.

    """

    kwargs = dict(date_keyword=datek, time_keyword=timek, exp_keyword=exptimek)

    # The date of observation is only actually needed when we need to apply
    # proper motion corrections. Therefore, don't call FITSImage.year() unless
    # one or more of the astromatic.Coordinates objects have a proper motion.
    # This avoids an unnecessary KeyError exception when we do photometry on a
    # FITS image without the 'datek' or 'timek' keywords (for example, a mosaic
    # created with IPAC's Montage): when that happens we cannot apply proper
    # motion corrections, that's right, but that's not an issue if none of our
    # objects have a known proper motion.

    for coord in coordinates:

        if coord.pm_ra or coord.pm_dec:
            try:
                year = img.year(**kwargs)
                break

            except KeyError as e:
                # Include the missing FITS keyword in the exception message
                regexp = "keyword '(?P<keyword>.*?)' not found"
                match = re.search(regexp, str(e))
                assert match is not None
                msg = ("{0}: keyword '{1}' not found. It is needed in order "
                       "to be able to apply proper-motion correction, as one "
                       "or more astronomical objects have known proper motions"
                       .format(img.path, match.group('keyword')))
                raise KeyError(msg)

        else:
            # No object has a known proper motion, so don't call
            # FITSImage.year().  Use the same value as the epoch, so that when
            # get_coords_file() below applies the proper motion correction the
            # input and output coordinates are the same.
            year = epoch

    # The proper-motion corrected objects coordinates
    coords_path = get_coords_file(coordinates, year, epoch)

    img_qphot = QPhot(img.path, coords_path)
    img_qphot.run(annulus, dannulus, aperture, exptimek, cbox=cbox)

    # How do we know whether one or more pixels in the aperture are above a
    # saturation threshold? As suggested by Frank Valdes at the IRAF.net
    # forums, we can make a mask of the saturated values, on which we can do
    # photometry using the same aperture. If we get a non-zero flux, we know it
    # has saturation: http://iraf.net/forum/viewtopic.php?showtopic=1466068

    if not uncimgk:
        orig_img_path = img.path

    else:
        orig_img_path = img.read_keyword(uncimgk)
        if not os.path.exists(orig_img_path):
            msg = "image %s (keyword '%s' of image %s) does not exist"
            args = orig_img_path, uncimgk, img.path
            raise IOError(msg % args)

    try:
        # Temporary file to which the saturation mask is saved
        basename = os.path.basename(orig_img_path)
        mkstemp_prefix = "%s_satur_mask_%d_ADUS_" % (basename, maximum)
        kwargs = dict(prefix=mkstemp_prefix, suffix='.fits', text=True)
        mask_fd, satur_mask_path = tempfile.mkstemp(**kwargs)
        os.close(mask_fd)

        # IRAF's imexpr won't overwrite the file. Instead, it will raise an
        # IrafError exception stating that "IRAF task terminated abnormally
        # ERROR (1121, "FXF: EOF encountered while reading FITS file".
        os.unlink(satur_mask_path)

        # The expression that will be given to 'imexpr'. The space after the
        # colon is needed to avoid sexigesimal interpretation. 'a' is the first
        # and only operand, linked to our image at the invokation of the task.
        expr = "a>%d ? 1 : 0" % maximum
        logging.debug("%s: imexpr = '%s'" % (img.path, expr))
        logging.debug("%s: a = %s" % (img.path, orig_img_path))
        logging.info("%s: Running IRAF's imexpr..." % img.path)
        pyraf.iraf.images.imexpr(expr,
                                 a=orig_img_path,
                                 output=satur_mask_path,
                                 verbose='yes',
                                 Stdout=methods.LoggerWriter('debug'))

        assert os.path.exists(satur_mask_path)

        msg = "%s: IRAF's imexpr OK" % img.path
        logging.info(msg)
        msg = "%s: IRAF's imexpr output = %s"
        logging.debug(msg % (img.path, satur_mask_path))

        # Now we just do photometry again, on the same pixels, but this time on
        # the saturation mask. Those objects for which we get a non-zero flux
        # will be known to be saturated and their magnitude set to infinity.

        # If 'cbox' is other than zero, the center of each object may have been
        # recentered by qphot using the centroid centering algorithm. When that
        # is the case, we need to feed run() with these more accurate centers.
        # Since the QPhotResult objects contain x- and y-coordinates (qphot
        # does not support 'world' coordinates as the output system), we need
        # to convert them back to right ascension and declination.

        if cbox:

            root, _ = os.path.splitext(os.path.basename(img.path))
            kwargs = dict(prefix=root + '_', suffix='_satur.coords', text=True)

            os.unlink(coords_path)
            fd, coords_path = tempfile.mkstemp(**kwargs)
            for object_phot in img_qphot:
                centered_x, centered_y = object_phot.x, object_phot.y
                ra, dec = img.pix2world(centered_x, centered_y)
                os.write(fd, "{0} {1}\n".format(ra, dec))
            os.close(fd)

        mask_qphot = QPhot(satur_mask_path, coords_path)
        # No centering this time: if cbox != 0 the accurate centers for each
        # astronomical object have been computed using the centroid centering
        # algorithm, so we're already feeding run() with the accurate values.
        mask_qphot.run(annulus, dannulus, aperture, exptimek, cbox=0)
        os.unlink(coords_path)

        assert len(img_qphot) == len(mask_qphot)
        for object_phot, object_mask in itertools.izip(img_qphot, mask_qphot):

            if __debug__:

                # In cbox != 0 we cannot expect the coordinates to be the exact
                # same: the previous call to run() returned x and y coordinates
                # that we converted to celestial coordinates, and now qphot is
                # giving as output image coordinates again. It is unavoidable
                # to lose some precision. Anyway, this does not affect the
                # result: photometry was still done on almost the absolute
                # exact coordinates that we wanted it to.

                if not cbox:
                    assert object_phot.x == object_mask.x
                    assert object_phot.y == object_mask.y

            if object_mask.flux > 0:
                object_phot = object_phot._replace(mag=float('infinity'))
    finally:

        # Remove saturation mask. The try-except is necessary because an
        # exception may be raised before 'satur_mask_path' is defined.

        try:
            methods.clean_tmp_files(satur_mask_path)
        except NameError:
            pass

    return img_qphot
Example #2
0
        with open(solved_file, 'rb') as fd:
            if ord(fd.read()) != 1:
                raise AstrometryNetUnsolvedField(path)

        return output_path

    except subprocess.CalledProcessError, e:
        raise AstrometryNetError(e.returncode, e.cmd)
    # If .solved file doesn't exist or contain one
    except (IOError, AstrometryNetUnsolvedField):
        raise AstrometryNetUnsolvedField(path)
    except subprocess.TimeoutExpired:
        raise AstrometryNetTimeoutExpired(path, timeout)
    finally:
        null_fd.close()
        methods.clean_tmp_files(output_dir)

@methods.print_exception_traceback
def parallel_astrometry(args):
    """ Function argument of map_async() to do astrometry in parallel.

    This will be the first argument passed to multiprocessing.Pool.map_async(),
    which chops the iterable into a number of chunks that are submitted to the
    process pool as separate tasks. 'args' must be a three-element tuple with
    (1) a string with the path to the FITS image, (2) a string with the path to
    the output directory and (3) 'options', the optparse.Values object returned
    by optparse.OptionParser.parse_args().

    This function does astrometry on each FITS image with the astrometry_net()
    function. The output FITS files, containing the WCS headers calculated by
    Astrometry.net, are written to the output directory with the same basename
Example #3
0
                        assert stdev_str == 'INDEF'
                        msg = "%s: stdev = None ('INDEF')" % self.path
                        logging.debug(msg)
                        stdev = None

                    args = xcenter, ycenter, mag, sum_, flux, stdev
                    self.append(QPhotResult(*args))

        finally:

            # Remove temporary files. The try-except is necessary because an
            # exception may be raised before 'qphot_output' and 'txdump_output'
            # have been defined.

            try:
                methods.clean_tmp_files(qphot_output)
            except NameError:
                pass

            try:
                methods.clean_tmp_files(txdump_output)
            except NameError:
                pass

        return len(self)


def get_coords_file(coordinates, year, epoch):
    """ Return a coordinates file with the exact positions of the objects.

    Loop over 'coordinates', an iterable of astromatic.Coordinates objects, and
Example #4
0
def run(img, coordinates, epoch,
        aperture, annulus, dannulus, maximum,
        datek, timek, exptimek, uncimgk):
    """ Do photometry on a FITS image.

    This convenience function does photometry on a FITSImage object, applying
    proper-motion correction to a series of astronomical objects and measuring
    them. The FITS image must have been previously calibrated astrometrically,
    so that right ascensions and declinations are meaningful. Returns a QPhot
    object, using None as the magnitude of those astronomical objects that are
    INDEF (i.e., so faint that qphot could not measure anything) and positive
    infinity if they are saturated (i.e., if one or more pixels in the aperture
    are above the saturation level).

    Arguments:
    img - the fitsimage.FITSImage object on which to do photometry.
    coordinates - an iterable of astromatic.Coordinates objects, one for each
                  astronomical object to be measured.
    epoch - the epoch of the coordinates of the astronomical objects, used to
            compute the proper-motion correction. Must be an integer, such as
            2000 for J2000.
    aperture - the aperture radius, in pixels.
    annulus - the inner radius of the sky annulus, in pixels.
    dannulus - the width of the sky annulus, in pixels.
    maximum - number of ADUs at which saturation arises. If one or more pixels
              in the aperture are above this value, the magnitude of the
              astronomical object is set to positive infinity. For coadded
              observations, the effective saturation level is obtained by
              multiplying this value by the number of coadded images.
    datek - the image header keyword containing the date of the observation, in
            the format specified in the FITS Standard. The old date format was
            'yy/mm/dd' and may be used only for dates from 1900 through 1999.
            The new Y2K compliant date format is 'yyyy-mm-dd' or
            'yyyy-mm-ddTHH:MM:SS[.sss]'.
    timek - the image header keyword containing the time at which the
            observation started, in the format HH:MM:SS[.sss]. This keyword is
            not necessary (and, therefore, is ignored) if the time is included
            directly as part of the 'datek' keyword value with the format
            'yyyy-mm-ddTHH:MM:SS[.sss]'.
    exptimek - the image header keyword containing the exposure time. Needed
               by qphot in order to normalize the computed magnitudes to an
               exposure time of one time unit.
    uncimgk - the image header keyword containing the path to the image used to
              check for saturation. It is expected to be the original FITS file
              (that is, before any calibration step, since corrections such as
              flat-fielding may move a saturated pixel below the saturation
              level) of the very image on which photometry is done. If this
              argument is set to an empty string or None, saturation is checked
              for on the same FITS image used for photometry, 'img'.

    """

    kwargs = dict(date_keyword = datek,
                  time_keyword = timek,
                  exp_keyword = exptimek)
    year = img.year(**kwargs)
    # The proper-motion corrected objects coordinates
    coords_path = get_coords_file(coordinates, year, epoch)

    img_qphot = QPhot(img.path, coords_path)
    img_qphot.run(annulus, dannulus, aperture, exptimek)

    # How do we know whether one or more pixels in the aperture are above a
    # saturation threshold? As suggested by Frank Valdes at the IRAF.net
    # forums, we can make a mask of the saturated values, on which we can do
    # photometry using the same aperture. If we get a non-zero flux, we know it
    # has saturation: http://iraf.net/forum/viewtopic.php?showtopic=1466068

    if not uncimgk:
        orig_img_path = img.path

    else:
        orig_img_path = img.read_keyword(uncimgk)
        if not os.path.exists(orig_img_path):
            msg = "image %s (keyword '%s' of image %s) does not exist"
            args = orig_img_path, uncimgk, img.path
            raise IOError(msg % args)

    try:
        # Temporary file to which the saturation mask is saved
        basename = os.path.basename(orig_img_path)
        mkstemp_prefix = "%s_satur_mask_%d_ADUS_" % (basename, maximum)
        kwargs = dict(prefix = mkstemp_prefix,
                      suffix = '.fits', text = True)
        mask_fd, satur_mask_path = tempfile.mkstemp(**kwargs)
        os.close(mask_fd)

        # IRAF's imexpr won't overwrite the file. Instead, it will raise an
        # IrafError exception stating that "IRAF task terminated abnormally
        # ERROR (1121, "FXF: EOF encountered while reading FITS file".
        os.unlink(satur_mask_path)

        # The expression that will be given to 'imexpr'. The space after the
        # colon is needed to avoid sexigesimal interpretation. 'a' is the first
        # and only operand, linked to our image at the invokation of the task.
        expr = "a>%d ? 1 : 0" % maximum
        logging.debug("%s: imexpr = '%s'" % (img.path, expr))
        logging.debug("%s: a = %s" % (img.path, orig_img_path))
        logging.info("%s: Running IRAF's imexpr..." % img.path)
        pyraf.iraf.images.imexpr(expr, a = orig_img_path,
                                 output = satur_mask_path, verbose = 'yes',
                                 Stdout = methods.LoggerWriter('debug'))

        assert os.path.exists(satur_mask_path)

        msg = "%s: IRAF's imexpr OK" % img.path
        logging.info(msg)
        msg = "%s: IRAF's imexpr output = %s"
        logging.debug(msg % (img.path, satur_mask_path))

        # Now we just do photometry again, on the same pixels, but this time on
        # the saturation mask. Those objects for which we get a non-zero flux
        # will be known to be saturated and their magnitude set to infinity.
        mask_qphot = QPhot(satur_mask_path, coords_path)
        mask_qphot.run(annulus, dannulus, aperture, exptimek)
        os.unlink(coords_path)

        assert len(img_qphot) == len(mask_qphot)
        for object_phot, object_mask in itertools.izip(img_qphot, mask_qphot):
            assert object_phot.x == object_mask.x
            assert object_phot.y == object_mask.y

            if object_mask.flux > 0:
                object_phot = object_phot._replace(mag = float('infinity'))
    finally:

        # Remove saturation mask. The try-except is necessary because an
        # exception may be raised before 'satur_mask_path' is defined.

        try:
            methods.clean_tmp_files(satur_mask_path)
        except NameError:
            pass

    return img_qphot
Example #5
0
                        assert stdev_str == 'INDEF'
                        msg = "%s: stdev = None ('INDEF')" % self.path
                        logging.debug(msg)
                        stdev = None

                    args = xcenter, ycenter, mag, sum_, flux, stdev
                    self.append(QPhotResult(*args))

        finally:

            # Remove temporary files. The try-except is necessary because an
            # exception may be raised before 'qphot_output' and 'txdump_output'
            # have been defined.

            try:
                methods.clean_tmp_files(qphot_output)
            except NameError:
                pass

            try:
                methods.clean_tmp_files(txdump_output)
            except NameError:
                pass

        return len(self)


def get_coords_file(coordinates, year, epoch):
    """ Return a coordinates file with the exact positions of the objects.

    Loop over 'coordinates', an iterable of astromatic.Coordinates objects, and
Example #6
0
        with open(solved_file, 'rb') as fd:
            if ord(fd.read()) != 1:
                raise AstrometryNetUnsolvedField(path)

        return output_path

    except subprocess.CalledProcessError, e:
        raise AstrometryNetError(e.returncode, e.cmd)
    # If .solved file doesn't exist or contain one
    except (IOError, AstrometryNetUnsolvedField):
        raise AstrometryNetUnsolvedField(path)
    except subprocess.TimeoutExpired:
        raise AstrometryNetTimeoutExpired(path, timeout)
    finally:
        null_fd.close()
        methods.clean_tmp_files(output_dir)

@methods.print_exception_traceback
def parallel_astrometry(args):
    """ Function argument of map_async() to do astrometry in parallel.

    This will be the first argument passed to multiprocessing.Pool.map_async(),
    which chops the iterable into a number of chunks that are submitted to the
    process pool as separate tasks. 'args' must be a three-element tuple with
    (1) a string with the path to the FITS image, (2) a string with the path to
    the output directory and (3) 'options', the optparse.Values object returned
    by optparse.OptionParser.parse_args().

    This function does astrometry on each FITS image with the astrometry_net()
    function. The output FITS files, containing the WCS headers calculated by
    Astrometry.net, are written to the output directory with the same basename
Example #7
0
def run(img, coordinates, epoch,
        aperture, annulus, dannulus, maximum,
        datek, timek, exptimek, uncimgk,
        cbox = 0):
    """ Do photometry on a FITS image.

    This convenience function does photometry on a FITSImage object, applying
    proper-motion correction to a series of astronomical objects and measuring
    them. The FITS image must have been previously calibrated astrometrically,
    so that right ascensions and declinations are meaningful. Returns a QPhot
    object, using None as the magnitude of those astronomical objects that are
    INDEF (i.e., so faint that qphot could not measure anything) and positive
    infinity if they are saturated (i.e., if one or more pixels in the aperture
    are above the saturation level).

    Arguments:
    img - the fitsimage.FITSImage object on which to do photometry.
    coordinates - an iterable of astromatic.Coordinates objects, one for each
                  astronomical object to be measured.
    epoch - the epoch of the coordinates of the astronomical objects, used to
            compute the proper-motion correction. Must be an integer, such as
            2000 for J2000.
    aperture - the aperture radius, in pixels.
    annulus - the inner radius of the sky annulus, in pixels.
    dannulus - the width of the sky annulus, in pixels.
    maximum - number of ADUs at which saturation arises. If one or more pixels
              in the aperture are above this value, the magnitude of the
              astronomical object is set to positive infinity. For coadded
              observations, the effective saturation level is obtained by
              multiplying this value by the number of coadded images.
    datek - the image header keyword containing the date of the observation, in
            the format specified in the FITS Standard. The old date format was
            'yy/mm/dd' and may be used only for dates from 1900 through 1999.
            The new Y2K compliant date format is 'yyyy-mm-dd' or
            'yyyy-mm-ddTHH:MM:SS[.sss]'. This keyword is not necessary if none
            of the astromatic.Coordinates objects have a known proper motion.
            When that is the case, it is not even read from the FITS header.
    timek - the image header keyword containing the time at which the
            observation started, in the format HH:MM:SS[.sss]. This keyword is
            not necessary (and, therefore, is ignored) if the time is included
            directly as part of the 'datek' keyword value with the format
            'yyyy-mm-ddTHH:MM:SS[.sss]'. As with 'datek', if no object has a
            proper motion this keyword is ignored and not read from the header.
    exptimek - the image header keyword containing the exposure time. Needed
               by qphot in order to normalize the computed magnitudes to an
               exposure time of one time unit.
    uncimgk - the image header keyword containing the path to the image used to
              check for saturation. It is expected to be the original FITS file
              (that is, before any calibration step, since corrections such as
              flat-fielding may move a saturated pixel below the saturation
              level) of the very image on which photometry is done. If this
              argument is set to an empty string or None, saturation is checked
              for on the same FITS image used for photometry, 'img'.
    cbox - the width of the centering box, in pixels. Accurate centers for each
           astronomical object are computed using the centroid centering
           algorithm. This means that, unless this argument is zero (the
           default value), photometry is not done exactly on the specified
           coordinates, but instead where IRAF has determined that the actual,
           accurate center of each object is. This is usually a good thing, and
           helps improve the photometry.

    """

    kwargs = dict(date_keyword = datek,
                  time_keyword = timek,
                  exp_keyword = exptimek)

    # The date of observation is only actually needed when we need to apply
    # proper motion corrections. Therefore, don't call FITSImage.year() unless
    # one or more of the astromatic.Coordinates objects have a proper motion.
    # This avoids an unnecessary KeyError exception when we do photometry on a
    # FITS image without the 'datek' or 'timek' keywords (for example, a mosaic
    # created with IPAC's Montage): when that happens we cannot apply proper
    # motion corrections, that's right, but that's not an issue if none of our
    # objects have a known proper motion.

    for coord in coordinates:

        if coord.pm_ra or coord.pm_dec:
            try:
                year = img.year(**kwargs)
                break

            except KeyError as e:
                # Include the missing FITS keyword in the exception message
                regexp = "keyword '(?P<keyword>.*?)' not found"
                match = re.search(regexp, str(e))
                assert match is not None
                msg = ("{0}: keyword '{1}' not found. It is needed in order "
                       "to be able to apply proper-motion correction, as one "
                       "or more astronomical objects have known proper motions"
                       .format(img.path, match.group('keyword')))
                raise KeyError(msg)

        else:
            # No object has a known proper motion, so don't call
            # FITSImage.year().  Use the same value as the epoch, so that when
            # get_coords_file() below applies the proper motion correction the
            # input and output coordinates are the same.
            year = epoch

    # The proper-motion corrected objects coordinates
    coords_path = get_coords_file(coordinates, year, epoch)

    img_qphot = QPhot(img.path, coords_path)
    img_qphot.run(annulus, dannulus, aperture, exptimek, cbox=cbox)

    # How do we know whether one or more pixels in the aperture are above a
    # saturation threshold? As suggested by Frank Valdes at the IRAF.net
    # forums, we can make a mask of the saturated values, on which we can do
    # photometry using the same aperture. If we get a non-zero flux, we know it
    # has saturation: http://iraf.net/forum/viewtopic.php?showtopic=1466068

    if not uncimgk:
        orig_img_path = img.path

    else:
        orig_img_path = img.read_keyword(uncimgk)
        if not os.path.exists(orig_img_path):
            msg = "image %s (keyword '%s' of image %s) does not exist"
            args = orig_img_path, uncimgk, img.path
            raise IOError(msg % args)

    try:
        # Temporary file to which the saturation mask is saved
        basename = os.path.basename(orig_img_path)
        mkstemp_prefix = "%s_satur_mask_%d_ADUS_" % (basename, maximum)
        kwargs = dict(prefix = mkstemp_prefix,
                      suffix = '.fits', text = True)
        mask_fd, satur_mask_path = tempfile.mkstemp(**kwargs)
        os.close(mask_fd)

        # IRAF's imexpr won't overwrite the file. Instead, it will raise an
        # IrafError exception stating that "IRAF task terminated abnormally
        # ERROR (1121, "FXF: EOF encountered while reading FITS file".
        os.unlink(satur_mask_path)

        # The expression that will be given to 'imexpr'. The space after the
        # colon is needed to avoid sexigesimal interpretation. 'a' is the first
        # and only operand, linked to our image at the invokation of the task.
        expr = "a>%d ? 1 : 0" % maximum
        logging.debug("%s: imexpr = '%s'" % (img.path, expr))
        logging.debug("%s: a = %s" % (img.path, orig_img_path))
        logging.info("%s: Running IRAF's imexpr..." % img.path)
        pyraf.iraf.images.imexpr(expr, a = orig_img_path,
                                 output = satur_mask_path, verbose = 'yes',
                                 Stdout = methods.LoggerWriter('debug'))

        assert os.path.exists(satur_mask_path)

        msg = "%s: IRAF's imexpr OK" % img.path
        logging.info(msg)
        msg = "%s: IRAF's imexpr output = %s"
        logging.debug(msg % (img.path, satur_mask_path))

        # Now we just do photometry again, on the same pixels, but this time on
        # the saturation mask. Those objects for which we get a non-zero flux
        # will be known to be saturated and their magnitude set to infinity.

        # If 'cbox' is other than zero, the center of each object may have been
        # recentered by qphot using the centroid centering algorithm. When that
        # is the case, we need to feed run() with these more accurate centers.
        # Since the QPhotResult objects contain x- and y-coordinates (qphot
        # does not support 'world' coordinates as the output system), we need
        # to convert them back to right ascension and declination.

        if cbox:

            root, _ = os.path.splitext(os.path.basename(img.path))
            kwargs = dict(prefix = root + '_',
                          suffix = '_satur.coords',
                          text = True)

            os.unlink(coords_path)
            fd, coords_path = tempfile.mkstemp(**kwargs)
            for object_phot in img_qphot:
                centered_x, centered_y = object_phot.x, object_phot.y
                ra, dec = img.pix2world(centered_x, centered_y)
                os.write(fd, "{0} {1}\n".format(ra, dec))
            os.close(fd)

        mask_qphot = QPhot(satur_mask_path, coords_path)
        # No centering this time: if cbox != 0 the accurate centers for each
        # astronomical object have been computed using the centroid centering
        # algorithm, so we're already feeding run() with the accurate values.
        mask_qphot.run(annulus, dannulus, aperture, exptimek, cbox=0)
        os.unlink(coords_path)

        assert len(img_qphot) == len(mask_qphot)
        for object_phot, object_mask in itertools.izip(img_qphot, mask_qphot):

            if __debug__:

                # In cbox != 0 we cannot expect the coordinates to be the exact
                # same: the previous call to run() returned x and y coordinates
                # that we converted to celestial coordinates, and now qphot is
                # giving as output image coordinates again. It is unavoidable
                # to lose some precision. Anyway, this does not affect the
                # result: photometry was still done on almost the absolute
                # exact coordinates that we wanted it to.

                if not cbox:
                    assert object_phot.x == object_mask.x
                    assert object_phot.y == object_mask.y

            if object_mask.flux > 0:
                object_phot = object_phot._replace(mag = float('infinity'))
    finally:

        # Remove saturation mask. The try-except is necessary because an
        # exception may be raised before 'satur_mask_path' is defined.

        try:
            methods.clean_tmp_files(satur_mask_path)
        except NameError:
            pass

    return img_qphot