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')
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
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
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
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
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
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
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