Ejemplo n.º 1
0
def merge_gsaoi_objcats(ad, cull_sources=False):
    """
    This function takes an AstroDataGsaoi object and combines the OBJCATs on
    the 4 extensions into a single Table, while also adding the static-
    distortion-corrected coordinates based on the WCS on each extension.

    Parameters
    ----------
    ad : AstroData
        input AstroDataGsaoi object with OBJCATs
    cull_sources : bool
        include only non-saturated point-like sources?

    Returns
    -------
    Table : merged OBJCATs with static-distortion-corrected coords added
    """
    # If we're not going to clip the OBJCATs we still need an iterable
    # for the main loop
    clipped_objcats = gt.clip_sources(ad) if cull_sources else ad
    objcats = []
    for index, (ext, objcat) in enumerate(zip(ad, clipped_objcats)):
        if cull_sources and not objcat:
            continue
        elif not cull_sources:
            try:
                objcat = ext.OBJCAT.copy()
            except AttributeError:
                continue
        objcat['ext_index'] = index
        static = ext.wcs.get_transform(ext.wcs.input_frame, "static")
        objcat['X_STATIC'], objcat['Y_STATIC'] = static(
            objcat['X_IMAGE'] - 1, objcat['Y_IMAGE'] - 1)
        objcats.append(objcat)
    return table.vstack(objcats, metadata_conflicts='silent')
Ejemplo n.º 2
0
 def test_clip_sources(self):
     ad = astrodata.open(os.path.join(TESTDATAPATH, 'GSAOI',
                                 'S20150110S0208_sourcesDetected.fits'))
     ret = gt.clip_sources(ad)
     # Only check the x-values, which are all unique in the table
     correct_x_values = [(1174.81,), (200.874,1616.33),
                 (915.444,1047.15,1106.54,1315.22), (136.063,957.848)]
     for rv, cv in zip([objcat['x'].data for objcat in ret], correct_x_values):
         assert len(rv) == len(cv)
         for a, b in zip(np.sort(rv), np.sort(cv)):
             assert abs(a-b) < 0.01
Ejemplo n.º 3
0
def _correlate_sources(ad1, ad2, delta=None, firstPass=10, cull_sources=False):
    """
    This function takes sources from the OBJCAT extensions in two
    images and attempts to correlate them. It returns a list of 
    reference source positions and their correlated image source 
    positions.
    
    :param ad1: reference image
    :type ad1: AstroData instance
    
    :param ad2: input image
    :type ad2: AstroData instance
    
    :param delta: maximum distance in pixels to allow a match. If
                  left as None, it will attempt to find an appropriate
                  number (recommended).
    :type delta: float
    
    :param firstPass: estimated maximum distance between correlated
                      sources. This distance represents the expected
                      mismatch between the WCSs of the input images.
    :type firstPass: float
    
    :param cull_sources: flag to indicate whether to reject sources that
                   are insufficiently star-like
    :type cull_sources: bool
    """

    log = gemLog.getGeminiLog()

    # If desired, clip out the most star-like sources in the OBJCAT
    if cull_sources:
        good_src_1 = gt.clip_sources(ad1)[("SCI", 1)]
        good_src_2 = gt.clip_sources(ad2)[("SCI", 1)]

        if len(good_src_1) < 3 or len(good_src_2) < 3:
            log.warning("Too few sources in culled list, using full set " "of sources")
            x1 = ad1["OBJCAT"].data.field("X_IMAGE")
            y1 = ad1["OBJCAT"].data.field("Y_IMAGE")
            x2 = ad2["OBJCAT"].data.field("X_IMAGE")
            y2 = ad2["OBJCAT"].data.field("Y_IMAGE")
        else:
            x1 = good_src_1["x"]
            y1 = good_src_1["y"]
            x2 = good_src_2["x"]
            y2 = good_src_2["y"]
    else:
        # Otherwise, just get all sources
        x1 = ad1["OBJCAT"].data.field("X_IMAGE")
        y1 = ad1["OBJCAT"].data.field("Y_IMAGE")
        x2 = ad2["OBJCAT"].data.field("X_IMAGE")
        y2 = ad2["OBJCAT"].data.field("Y_IMAGE")

    # get WCS from both images
    wcs1 = pywcs.WCS(ad1["SCI"].header)
    wcs2 = pywcs.WCS(ad2["SCI"].header)

    # convert image 2 data to sky coordinates
    ra2, dec2 = wcs2.wcs_pix2sky(x2, y2, 1)

    # convert image 2 sky data to image 1 pixel coordinates
    conv_x2, conv_y2 = wcs1.wcs_sky2pix(ra2, dec2, 1)

    # find matches
    ind1, ind2 = at.match_cxy(x1, conv_x2, y1, conv_y2, delta=delta, firstPass=firstPass, log=log)

    if len(ind1) != len(ind2):
        raise Errors.ScienceError("Mismatched arrays returned from match_cxy")

    if len(ind1) < 1 or len(ind2) < 1:
        return [[], []]
    else:
        obj_list = [zip(x1[ind1], y1[ind1]), zip(x2[ind2], y2[ind2])]
        return obj_list
Ejemplo n.º 4
0
    def adjustWCSToReference(self, adinputs=None, **params):
        """
        This primitive registers images to a reference image by correcting
        the relative error in their world coordinate systems. This is
        preferably done via alignment of sources common to the reference
        and input images but a fallback method that uses the header offsets
        is also available, if there are too few sources to provide a robust
        alignment, or if the initial WCSs are incorrect.

        The alignment of sources is done via the KDTreeFitter, which does
        not require a direct one-to-one mapping of sources between the
        images. The alignment is performed in the pixel frame of each input
        image, with sources in the reference image being transformed into
        that frame via the existing WCS transforms. Therefore, whatever
        transformation is required can simply be placed at the start of each
        input image's WCS pipeline.

        In order to use the direct mapping method, sources must have been
        detected in the frame and attached to the AstroData instance in an
        OBJCAT extension. This can be accomplished via the detectSources
        primitive.

        It is expected that the relative difference between the WCSs of
        images to be combined should be quite small, so it may not be necessary
        to allow rotation and scaling degrees of freedom when fitting the image
        WCS to the reference WCS. However, if it is desired, the options
        rotate and scale can be used to allow these degrees of freedom. Note
        that these options refer to rotation/scaling of the WCS itself, not the
        images. Significant rotation and scaling of the images themselves
        will generally already be encoded in the WCS, and will be corrected for
        when the images are aligned.

        Parameters
        ----------
        suffix: str
            suffix to be added to output files
        method: str ['sources' | 'offsets']
            method to use to generate reference points. 'sources' uses sources
            to align images, 'offsets' uses POFFSET and QOFFSET keywords
        fallback: 'header' or None
            backup method, if the primary one fails
        first_pass: float
            search radius (arcsec) for the initial alignment matching
        min_sources: int
            minimum number of matched sources required to apply a WCS shift
        cull_sources: bool
            remove sub-optimal (saturated and/or non-stellar) sources before
            alignment?
        rotate: bool
            allow image rotation to align to reference image?
        scale: bool
            allow image scaling to align to reference image?
        """
        warnings = {
            "offsets": "Using header offsets for alignment",
            None: "No WCS correction being performed"
        }

        log = self.log
        log.debug(gt.log_message("primitive", self.myself(), "starting"))
        timestamp_key = self.timestamp_keys[self.myself()]

        if len(adinputs) <= 1:
            log.warning("No correction will be performed, since at least two "
                        "input images are required for adjustWCSToReference")
            return adinputs

        if not all(len(ad) == 1 for ad in adinputs):
            raise OSError("All input images must have only one extension.")

        method = params["method"]
        fallback = params["fallback"]
        first_pass = params["first_pass"]
        min_sources = params["min_sources"]
        cull_sources = params["cull_sources"]
        rotate = params["rotate"]
        scale = params["scale"]

        # Use first image in list as reference
        adref = adinputs[0]
        log.stdinfo(f"Reference image: {adref.filename}")
        # Create a dummy WCS to facilitate future operations
        if adref[0].wcs is None:
            adref[0].wcs = gWCS([(cf.Frame2D(name="pixels"),
                                  models.Identity(len(adref[0].shape))),
                                 (cf.Frame2D(name="world"), None)])

        try:
            ref_objcat = adref[0].OBJCAT
        except AttributeError:
            log.warning(f"Reference image has no OBJCAT. {warnings[fallback]}")
            method = fallback
        else:
            if len(ref_objcat) < min_sources and method == "sources":
                log.warning(
                    f"Too few objects found in reference image {warnings[fallback]}"
                )
                method = fallback
        if method is None:
            return adinputs

        adoutputs = [adref]

        for ad in adinputs[1:]:
            msg = ""
            if method == "sources":
                try:
                    objcat = ad[0].OBJCAT
                except AttributeError:
                    msg = f"{ad.filename} image has no OBJCAT. "
                else:
                    if len(objcat) < min_sources:
                        msg = f"{ad.filename} has too few sources. "

            if msg or method == "offsets":
                msg += warnings[fallback]
                log.stdinfo(msg)
                _create_wcs_from_offsets(ad, adref)
                continue

            # GNIRS WCS is dubious, so update WCS by using the ref
            # image's WCS and the telescope offsets before alignment
            #if ad.instrument() == 'GNIRS':
            #    log.stdinfo("Recomputing WCS for GNIRS from offsets")
            #    ad = _create_wcs_from_offsets(ad, adref)

            log.fullinfo(f"Number of objects in {ad.filename}: {len(objcat)}")
            log.stdinfo(
                f"Cross-correlating sources in {adref.filename}, {ad.filename}"
            )

            pixscale = ad.pixel_scale()
            if pixscale is None:
                log.warning(f'Cannot determine pixel scale for {ad.filename}. '
                            f'Using a search radius of {first_pass} pixels.')
                firstpasspix = first_pass
            else:
                firstpasspix = first_pass / pixscale

            incoords = (objcat['X_IMAGE'].data - 1, objcat['Y_IMAGE'].data - 1)
            refcoords = (ref_objcat['X_IMAGE'].data - 1,
                         ref_objcat['Y_IMAGE'].data - 1)
            if cull_sources:
                good_src1 = gt.clip_sources(ad)[0]
                good_src2 = gt.clip_sources(adref)[0]
                if len(good_src1) < min_sources or len(
                        good_src2) < min_sources:
                    log.warning(
                        "Too few sources in culled list, using full set "
                        "of sources")
                else:
                    incoords = (good_src1["x"] - 1, good_src1["y"] - 1)
                    refcoords = (good_src2["x"] - 1, good_src2["y"] - 1)

            try:
                t_init = adref[0].wcs.forward_transform | ad[
                    0].wcs.backward_transform
            except AttributeError:  # for cases with no defined WCS
                pass
            else:
                refcoords = t_init(*refcoords)

            # The code used to start with a translation-only model, but this
            # isn't helpful if there's a sizeable rotation or scaling, so
            # let's just try to do the whole thing (if the user asks) and
            # see what happens.
            transform, obj_list = find_alignment_transform(
                incoords,
                refcoords,
                transform=None,
                shape=ad[0].shape,
                search_radius=firstpasspix,
                rotate=rotate,
                scale=scale,
                return_matches=True)

            n_corr = len(obj_list[0])
            if n_corr < min_sources + rotate + scale:
                log.warning(f"Too few correlated objects ({n_corr}). "
                            "Setting rotate=False, scale=False")
                transform, obj_list = find_alignment_transform(
                    incoords,
                    refcoords,
                    transform=None,
                    shape=ad[0].shape,
                    search_radius=firstpasspix,
                    rotate=False,
                    scale=False,
                    return_matches=True)
                n_corr = len(obj_list[0])

            log.stdinfo(f"Number of correlated sources: {n_corr}")
            log.fullinfo("\nMatched sources:")
            log.fullinfo("   Ref. x Ref. y  Img. x  Img. y\n  {}".format("-" *
                                                                         31))
            for img, ref in zip(*obj_list):
                log.fullinfo("  {:7.2f} {:7.2f} {:7.2f} {:7.2f}".format(
                    *ref, *img))
            log.stdinfo("")

            if n_corr < min_sources:
                log.warning("Too few correlated sources found. "
                            "{}".format(warnings[fallback]))
                if fallback == 'offsets':
                    _create_wcs_from_offsets(ad, adref)
                adoutputs.append(ad)
                continue

            try:
                ad[0].wcs.insert_transform(ad[0].wcs.input_frame,
                                           transform,
                                           after=True)
            except AttributeError:  # no WCS
                ad[0].wcs = gWCS([(cf.Frame2D(name="pixels"), transform),
                                  (cf.Frame2D(name="world"), None)])
            adoutputs.append(ad)

        # Timestamp and update filenames
        for ad in adoutputs:
            gt.mark_history(ad, primname=self.myself(), keyword=timestamp_key)
            ad.update_filename(suffix=params["suffix"], strip=True)
        return adoutputs
Ejemplo n.º 5
0
def align_images_from_wcs(adinput,
                          adref,
                          cull_sources=False,
                          transform=None,
                          min_sources=1,
                          search_radius=10,
                          match_radius=2,
                          rotate=False,
                          scale=False,
                          full_wcs=False,
                          brute=True,
                          return_matches=False):
    """
    This function takes two images (an input image, and a reference image) and
    works out the modifications needed to the WCS of the input images so that
    the world coordinates of its OBJCAT sources match the world coordinates of
    the OBJCAT sources in the reference image. This is done by modifying the
    WCS of the input image and mapping the reference image sources to pixels
    in the input image via the reference image WCS (fixed) and the input image
    WCS. As such, in the nomenclature of the fitting routines, the pixel
    positions of the input image's OBJCAT become the "reference" sources,
    while the converted positions of the reference image's OBJCAT are the
    "input" sources.

    Parameters
    ----------
    adinput: AstroData
        input AD whose pixel shift is requested
    adref: AstroData
        reference AD image
    transform: Transform/None
        existing transformation (if None, will do brute search)
    cull_sources: bool
        limit matched sources to "good" (i.e., stellar) objects
    min_sources: int
        minimum number of sources to use for cross-correlation
    search_radius: float
        size of search box (in pixels)
    match_radius: float
        matching radius for objects (in pixels)
    rotate: bool
        add a rotation to the alignment transform?
    scale: bool
        add a magnification to the alignment transform?
    full_wcs: bool
        use the two images' WCSs to reproject the reference image's coordinates
        onto the input image's pixel plane, rather than just align the OBJCAT
        coordinates?
    brute: bool
        perform brute (landscape) search first?
    return_matches: bool
        return a list of matched objects as well as the Transform?

    Returns
    -------
    matches: 2 lists
        OBJCAT sources in input and reference that are matched
    WCS: new WCS for input image
    """
    log = logutils.get_logger(__name__)
    if len(adinput) * len(adref) != 1:
        log.warning('Can only match single-extension images')
        return None

    try:
        input_objcat = adinput[0].OBJCAT
        ref_objcat = adref[0].OBJCAT
    except AttributeError:
        log.warning('Both input images must have object catalogs')
        return None

    if len(input_objcat) < min_sources or len(ref_objcat) < min_sources:
        log.warning("Too few sources in one or both images. Cannot align.")
        return None

    largest_dimension = max(*adinput[0].shape, *adref[0].shape)
    # Values larger than these result in errors of >1 pixel
    mag_threshold = 1. / largest_dimension
    rot_threshold = np.degrees(mag_threshold)

    # OK, we can proceed
    incoords = (input_objcat['X_IMAGE'].data - 1,
                input_objcat['Y_IMAGE'].data - 1)
    refcoords = (ref_objcat['X_IMAGE'].data - 1,
                 ref_objcat['Y_IMAGE'].data - 1)
    if cull_sources:
        good_src1 = gt.clip_sources(adinput)[0]
        good_src2 = gt.clip_sources(adref)[0]
        if len(good_src1) < min_sources or len(good_src2) < min_sources:
            log.warning("Too few sources in culled list, using full set "
                        "of sources")
        else:
            incoords = (good_src1["x"] - 1, good_src1["y"] - 1)
            refcoords = (good_src2["x"] - 1, good_src2["y"] - 1)

    # Set up the initial model
    magnification, rotation = 1, 0  # May be overridden later
    try:
        t = adref[0].wcs.forward_transform | adinput[0].wcs.backward_transform
    except AttributeError:  # for cases with no defined WCS
        t = None
        if full_wcs:
            log.warning(
                "Cannot determine WCS information: setting full_wcs=False")
            full_wcs = False

    if full_wcs:
        refcoords = t(*refcoords)
    elif transform is None and t is not None:
        transform = t.inverse
    if transform is None:
        transform = models.Identity(2)

    # We always refactor the transform (if provided) in a prescribed way so
    # as to ensure it's fittable and not overly weird
    affine = adwcs.calculate_affine_matrices(transform, adinput[0].shape)
    m_init = models.Shift(affine.offset[1]) & models.Shift(affine.offset[0])

    # This is approximate since the affine matrix might have differential
    # scaling and a shear
    magnification = np.sqrt(abs(np.linalg.det(affine.matrix)))
    rotation = np.degrees(
        np.arctan2(affine.matrix[1, 0] - affine.matrix[0, 1],
                   affine.matrix[0, 0] + affine.matrix[1, 1]))
    m_init.offset_0.bounds = (m_init.offset_0 - search_radius,
                              m_init.offset_0 + search_radius)
    m_init.offset_1.bounds = (m_init.offset_1 - search_radius,
                              m_init.offset_1 + search_radius)

    m_rotate = Rotate2D(rotation)
    if rotate:
        m_rotate.angle.bounds = (rotation - 5, rotation + 5)
        m_init = m_rotate | m_init
    elif abs(rotation) > rot_threshold:
        m_rotate.angle.fixed = True
        m_init = m_rotate | m_init
        log.warning("A rotation of {:.3f} degrees is expected but the "
                    "rotation is fixed".format(rotation))

    m_magnify = Scale2D(magnification)
    if scale:
        m_magnify.factor.bounds = (magnification - 0.05, magnification + 0.05)
        m_init = m_magnify | m_init
    elif abs(magnification - 1) > mag_threshold:
        m_magnify.factor.fixed = True
        m_init = m_magnify | m_init
        log.warning("A magnification of {:.4f} is expected but the "
                    "magnification is fixed".format(magnification))

    # Perform the fit
    m_final = fit_model(m_init, incoords, refcoords, sigma=10, brute=brute)
    if return_matches:
        matched = match_sources(m_final(*incoords),
                                refcoords,
                                radius=match_radius)
        ind2 = np.where(matched >= 0)
        ind1 = matched[ind2]
        obj_list = [[], []] if len(ind1) < 1 else [
            np.array(list(zip(*incoords)))[ind2],
            np.array(list(zip(*refcoords)))[ind1]
        ]
        return obj_list, m_final
    return m_final
Ejemplo n.º 6
0
def align_images_from_wcs(adinput,
                          adref,
                          transform=None,
                          cull_sources=False,
                          min_sources=1,
                          search_radius=10,
                          rotate=False,
                          scale=False,
                          full_wcs=False,
                          return_matches=False):
    """
    This function takes two images (an input image, and a reference image) and
    works out the modifications needed to the WCS of the input images so that
    the world coordinates of its OBJCAT sources match the world coordinates of
    the OBJCAT sources in the reference image. This is done by modifying the
    WCS of the input image and mapping the reference image sources to pixels
    in the input image via the reference image WCS (fixed) and the input image
    WCS. As such, in the nomenclature of the fitting routines, the pixel
    positions of the input image's OBJCAT become the "reference" sources,
    while the converted positions of the reference image's OBJCAT are the
    "input" sources.

    Parameters
    ----------
    adinput: AstroData
        input AD whose pixel shift is requested
    adref: AstroData
        reference AD image
    transform: Transform/None
        existing transformation (if None, will do brute search)
    cull_sources: bool
        limit matched sources to "good" (i.e., stellar) objects
    min_sources: int
        minimum number of sources to use for cross-correlation
    search_radius: float
        size of search box (in arcseconds)
    rotate: bool
        add a rotation to the alignment transform?
    scale: bool
        add a magnification to the alignment transform?
    full_wcs: bool
        use recomputed WCS at each iteration, rather than modify the positions
        in pixel space?
    return_matches: bool
        return a list of matched objects as well as the Transform?

    Returns
    -------
    matches: 2 lists
        OBJCAT sources in input and reference that are matched
    WCS: new WCS for input image
    """
    log = logutils.get_logger(__name__)
    if len(adinput) * len(adref) != 1:
        log.warning('Can only match single-extension images')
        return None

    try:
        input_objcat = adinput[0].OBJCAT
        ref_objcat = adref[0].OBJCAT
    except AttributeError:
        log.warning('Both input images must have object catalogs')
        return None

    if len(input_objcat) < min_sources or len(ref_objcat) < min_sources:
        log.warning("Too few sources in one or both images. Cannot align.")
        return None

    # OK, we can proceed
    incoords = (input_objcat['X_IMAGE'].data, input_objcat['Y_IMAGE'].data)
    refcoords = (ref_objcat['X_IMAGE'], ref_objcat['Y_IMAGE'])
    if cull_sources:
        good_src1 = gt.clip_sources(adinput)[0]
        good_src2 = gt.clip_sources(adref)[0]
        if len(good_src1) < min_sources or len(good_src2) < min_sources:
            log.warning("Too few sources in culled list, using full set "
                        "of sources")
        else:
            incoords = (good_src1["x"], good_src1["y"])
            refcoords = (good_src2["x"], good_src2["y"])

    # Nomenclature here is that the ref_transform transforms the reference
    # OBJCAT coords immutably, and then the fit_transform is the fittable
    # thing we're trying to get to map those to the input OBJCAT coords
    ref_transform = Transform(Pix2Sky(WCS(adref[0].hdr)))
    # pixel_range = max(adinput[0].data.shape)
    if full_wcs:
        # fit_transform = Transform(Pix2Sky(WCS(adinput[0].hdr), factor=pixel_range, factor_scale=pixel_range, angle_scale=pixel_range/57.3).inverse)
        fit_transform = Transform(
            Pix2Sky(WCS(adinput[0].hdr)).rename('WCS').inverse)
        fit_transform.angle.fixed = not rotate
        fit_transform.factor.fixed = not scale
    else:
        ref_transform.append(Pix2Sky(WCS(adinput[0].hdr)).inverse)
        fit_transform = Transform.create2d(translation=(0, 0),
                                           rotation=0 if rotate else None,
                                           magnification=1 if scale else None,
                                           shape=adinput[0].shape)

    # Copy parameters across. We don't simply start with the provided
    # transform, because we may be moving from a geometric one to WCS
    if transform is not None:
        for param in fit_transform.param_names:
            if param in transform.param_names:
                setattr(fit_transform, param, getattr(transform, param))
    fit_transform.add_bounds('x_offset', search_radius)
    fit_transform.add_bounds('y_offset', search_radius)
    if rotate:
        fit_transform.add_bounds('angle', 5.)
    if scale:
        fit_transform.add_bounds('factor', 0.05)

    # Do the fit, and update the transform with the fitted parameters
    transformed_ref_coords = ref_transform(*refcoords)
    refine = (transform is not None)
    fitted_model = fit_model(fit_transform.asModel(),
                             transformed_ref_coords,
                             incoords,
                             sigma=10,
                             tolerance=1e-6,
                             brute=not refine)
    fit_transform.replace(fitted_model)

    if return_matches:
        matched = match_sources(fitted_model(*transformed_ref_coords),
                                incoords,
                                radius=1.0)
        ind2 = np.where(matched >= 0)
        ind1 = matched[ind2]
        obj_list = [[], []] if len(ind1) < 1 else [
            np.array(list(zip(*incoords)))[ind1],
            np.array(list(zip(*refcoords)))[ind2]
        ]
        return obj_list, fit_transform

    return fit_transform
Ejemplo n.º 7
0
def align_images_from_wcs(adinput, adref, first_pass=10, cull_sources=False,
                          initial_shift = (0,0), min_sources=1, rotate=False,
                          scale=False, full_wcs=False, refine=False,
                          tolerance=0.1, return_matches=False):
    """
    This function takes two images (an input image, and a reference image) and
    works out the modifications needed to the WCS of the input images so that
    the world coordinates of its OBJCAT sources match the world coordinates of
    the OBJCAT sources in the reference image. This is done by modifying the
    WCS of the input image and mapping the reference image sources to pixels
    in the input image via the reference image WCS (fixed) and the input image
    WCS. As such, in the nomenclature of the fitting routines, the pixel
    positions of the input image's OBJCAT become the "reference" sources,
    while the converted positions of the reference image's OBJCAT are the
    "input" sources.

    Parameters
    ----------
    adinput: AstroData
        input AD whose pixel shift is requested
    adref: AstroData
        reference AD image
    first_pass: float
        size of search box (in arcseconds)
    cull_sources: bool
        limit matched sources to "good" (i.e., stellar) objects
    min_sources: int
        minimum number of sources to use for cross-correlation, depending on the
        instrument used
    rotate: bool
        add a rotation to the alignment transform?
    scale: bool
        add a magnification to the alignment transform?
    full_wcs: bool
        use recomputed WCS at each iteration, rather than modify the positions
        in pixel space?
    refine: bool
        only do a simplex fit to refine an existing transformation?
        (requires full_wcs=True). Also ignores return_matches
    tolerance: float
        matching requirement (in pixels)
    return_matches: bool
        return a list of matched objects?

    Returns
    -------
    matches: 2 lists
        OBJCAT sources in input and reference that are matched
    WCS: new WCS for input image
    """
    log = logutils.get_logger(__name__)
    if len(adinput) * len(adref) != 1:
        log.warning('Can only match single-extension images')
        return None
    if not (hasattr(adinput[0], 'OBJCAT') and hasattr(adref[0], 'OBJCAT')):
        log.warning('Both input images must have object catalogs')
        return None

    if cull_sources:
        good_src1 = gt.clip_sources(adinput)[0]
        good_src2 = gt.clip_sources(adref)[0]
        if len(good_src1) < min_sources or len(good_src2) < min_sources:
            log.warning("Too few sources in culled list, using full set "
                        "of sources")
            x1, y1 = adinput[0].OBJCAT['X_IMAGE'], adinput[0].OBJCAT['Y_IMAGE']
            x2, y2 = adref[0].OBJCAT['X_IMAGE'], adref[0].OBJCAT['Y_IMAGE']
        else:
            x1, y1 = good_src1["x"], good_src1["y"]
            x2, y2 = good_src2["x"], good_src2["y"]
    else:
        x1, y1 = adinput[0].OBJCAT['X_IMAGE'], adinput[0].OBJCAT['Y_IMAGE']
        x2, y2 = adref[0].OBJCAT['X_IMAGE'], adref[0].OBJCAT['Y_IMAGE']

    # convert reference positions to sky coordinates
    ra2, dec2 = WCS(adref[0].hdr).all_pix2world(x2, y2, 1)

    func = match_catalogs if return_matches else align_catalogs

    if full_wcs:
        # Set up the (inverse) Pix2Sky transform with appropriate scalings
        pixel_range = max(adinput[0].data.shape)
        transform = Pix2Sky(WCS(adinput[0].hdr), factor=pixel_range,
                            factor_scale=pixel_range, angle=0.0,
                            angle_scale=pixel_range/57.3, direction=-1)
        x_offset, y_offset = initial_shift
        transform.x_offset = x_offset
        transform.y_offset = y_offset
        transform.angle.fixed = not rotate
        transform.factor.fixed = not scale

        if refine:
            fit_it = KDTreeFitter()
            final_model = fit_it(transform, (ra2, dec2), (x1, y1),
                                 method='Nelder-Mead',
                                 options={'xtol': tolerance, 'ftol': 100.0})
            log.stdinfo(_show_model(final_model, 'Refined transformation'))
            return final_model
        else:
            transform.x_offset.bounds = (x_offset-first_pass, x_offset+first_pass)
            transform.y_offset.bounds = (y_offset-first_pass, y_offset+first_pass)
            if rotate:
                # 5.73 degrees
                transform.angle.bounds = (-0.1*pixel_range, 0.1*pixel_range)
            if scale:
                # 5% scaling
                transform.factor.bounds = (0.95*pixel_range, 1.05*pixel_range)

            # map input positions (in reference frame) to reference positions
            func_ret = func(ra2, dec2, x1, y1, model_guess=transform,
                            tolerance=tolerance)
    else:
        x2a, y2a = WCS(adinput[0].hdr).all_world2pix(ra2, dec2, 1)
        func_ret = func(x2a, y2a, x1, y1, model_guess=None,
                        translation=initial_shift,
                        translation_range=first_pass,
                        rotation_range=5.0 if rotate else None,
                        magnification_range=0.05 if scale else None,
                        tolerance=tolerance)

    if return_matches:
        matched, transform = func_ret
        ind2 = np.where(matched >= 0)
        ind1 = matched[ind2]
        obj_list = [[], []] if len(ind1) < 1 else [list(zip(x1[ind1], y1[ind1])),
                                                   list(zip(x2[ind2], y2[ind2]))]
        return obj_list, transform
    else:
        return func_ret
Ejemplo n.º 8
0
def align_images_from_wcs(adinput,
                          adref,
                          first_pass=10,
                          cull_sources=False,
                          initial_shift=(0, 0),
                          min_sources=1,
                          rotate=False,
                          scale=False,
                          full_wcs=False,
                          refine=False,
                          tolerance=0.1,
                          return_matches=False):
    """
    This function takes two images (an input image, and a reference image) and
    works out the modifications needed to the WCS of the input images so that
    the world coordinates of its OBJCAT sources match the world coordinates of
    the OBJCAT sources in the reference image. This is done by modifying the
    WCS of the input image and mapping the reference image sources to pixels
    in the input image via the reference image WCS (fixed) and the input image
    WCS. As such, in the nomenclature of the fitting routines, the pixel
    positions of the input image's OBJCAT become the "reference" sources,
    while the converted positions of the reference image's OBJCAT are the
    "input" sources.

    Parameters
    ----------
    adinput: AstroData
        input AD whose pixel shift is requested
    adref: AstroData
        reference AD image
    first_pass: float
        size of search box (in arcseconds)
    cull_sources: bool
        limit matched sources to "good" (i.e., stellar) objects
    min_sources: int
        minimum number of sources to use for cross-correlation, depending on the
        instrument used
    rotate: bool
        add a rotation to the alignment transform?
    scale: bool
        add a magnification to the alignment transform?
    full_wcs: bool
        use recomputed WCS at each iteration, rather than modify the positions
        in pixel space?
    refine: bool
        only do a simplex fit to refine an existing transformation?
        (requires full_wcs=True). Also ignores return_matches
    tolerance: float
        matching requirement (in pixels)
    return_matches: bool
        return a list of matched objects?

    Returns
    -------
    matches: 2 lists
        OBJCAT sources in input and reference that are matched
    WCS: new WCS for input image
    """
    log = logutils.get_logger(__name__)
    if len(adinput) * len(adref) != 1:
        log.warning('Can only match single-extension images')
        return None
    if not (hasattr(adinput[0], 'OBJCAT') and hasattr(adref[0], 'OBJCAT')):
        log.warning('Both input images must have object catalogs')
        return None

    if cull_sources:
        good_src1 = gt.clip_sources(adinput)[0]
        good_src2 = gt.clip_sources(adref)[0]
        if len(good_src1) < min_sources or len(good_src2) < min_sources:
            log.warning("Too few sources in culled list, using full set "
                        "of sources")
            x1, y1 = adinput[0].OBJCAT['X_IMAGE'], adinput[0].OBJCAT['Y_IMAGE']
            x2, y2 = adref[0].OBJCAT['X_IMAGE'], adref[0].OBJCAT['Y_IMAGE']
        else:
            x1, y1 = good_src1["x"], good_src1["y"]
            x2, y2 = good_src2["x"], good_src2["y"]
    else:
        x1, y1 = adinput[0].OBJCAT['X_IMAGE'], adinput[0].OBJCAT['Y_IMAGE']
        x2, y2 = adref[0].OBJCAT['X_IMAGE'], adref[0].OBJCAT['Y_IMAGE']

    # convert reference positions to sky coordinates
    ra2, dec2 = WCS(adref[0].hdr).all_pix2world(x2, y2, 1)

    func = match_catalogs if return_matches else align_catalogs

    if full_wcs:
        # Set up the (inverse) Pix2Sky transform with appropriate scalings
        pixel_range = max(adinput[0].data.shape)
        transform = Pix2Sky(WCS(adinput[0].hdr),
                            factor=pixel_range,
                            factor_scale=pixel_range,
                            angle=0.0,
                            angle_scale=pixel_range / 57.3,
                            direction=-1)
        x_offset, y_offset = initial_shift
        transform.x_offset = x_offset
        transform.y_offset = y_offset
        transform.angle.fixed = not rotate
        transform.factor.fixed = not scale

        if refine:
            fit_it = KDTreeFitter()
            final_model = fit_it(transform, (ra2, dec2), (x1, y1),
                                 method='Nelder-Mead',
                                 options={
                                     'xtol': tolerance,
                                     'ftol': 100.0
                                 })
            log.stdinfo(_show_model(final_model, 'Refined transformation'))
            return final_model
        else:
            transform.x_offset.bounds = (x_offset - first_pass,
                                         x_offset + first_pass)
            transform.y_offset.bounds = (y_offset - first_pass,
                                         y_offset + first_pass)
            if rotate:
                # 5.73 degrees
                transform.angle.bounds = (-0.1 * pixel_range,
                                          0.1 * pixel_range)
            if scale:
                # 5% scaling
                transform.factor.bounds = (0.95 * pixel_range,
                                           1.05 * pixel_range)

            # map input positions (in reference frame) to reference positions
            func_ret = func(ra2,
                            dec2,
                            x1,
                            y1,
                            model_guess=transform,
                            tolerance=tolerance)
    else:
        x2a, y2a = WCS(adinput[0].hdr).all_world2pix(ra2, dec2, 1)
        func_ret = func(x2a,
                        y2a,
                        x1,
                        y1,
                        model_guess=None,
                        translation=initial_shift,
                        translation_range=first_pass,
                        rotation_range=5.0 if rotate else None,
                        magnification_range=0.05 if scale else None,
                        tolerance=tolerance)

    if return_matches:
        matched, transform = func_ret
        ind2 = np.where(matched >= 0)
        ind1 = matched[ind2]
        obj_list = [[], []] if len(ind1) < 1 else [
            list(zip(x1[ind1], y1[ind1])),
            list(zip(x2[ind2], y2[ind2]))
        ]
        return obj_list, transform
    else:
        return func_ret