def test_different(): np.random.seed(0) x = np.random.random((512, 2)) y = np.random.random((512, 2)) r = stimage.xyxymatch(x, y, algorithm='tolerance', tolerance=0.01, separation=0.0) assert len(r) < 512 and len(r) > 0 for i in range(len(r)): x0, y0 = r['input_x'][i], r['input_y'][i] x1, y1 = r['ref_x'][i], r['ref_y'][i] dx = x1 - x0 dy = y1 - y0 distance = dx * dx + dy * dy assert distance < 0.01 * 0.01 assert r['input_idx'][i] < 512 assert r['ref_idx'][i] < 512
def test_same(): np.random.seed(0) x = np.random.random((512, 2)) y = x[:] r = stimage.xyxymatch(x, y, algorithm='tolerance', tolerance=0.01, separation=0.0, nmatch=0, maxratio=0, nreject=0) print(r.dtype) print(r.shape) assert len(r) == 512 for i in range(512): assert r['input_x'][i] == r['ref_x'][i] assert r['input_y'][i] == r['ref_y'][i] assert r['input_idx'][i] == r['ref_idx'][i] assert r['input_idx'][i] < 512
def run(sl_names, img_names, diagnostic_mode=False, log_level=logutil.logging.INFO): """main running subroutine Parameters ---------- sl_names : list A list containing the reference sourcelist filename and the comparison sourcelist filename, in that order. img_names : list A list containing the reference image filename and the comparison image filename, in that order. diagnostic_mode : Bool, optional If this option is set to Boolean 'True', region files will be created to test the quality of the coordinate transformation. Default value is Boolean 'False'. log_level : int, optional The desired level of verboseness in the log statements displayed on the screen and written to the .log file. Default value is 'INFO'. Returns ------- Nothing. """ log.setLevel(log_level) # 0: correct ra, dec, x, y in HLA sourcelists if sl_names[1].endswith("phot.txt"): if not os.path.exists(sl_names[1].replace("phot.txt", "phot_corrected.txt")): corrected_hla_slname = svmqa.correct_hla_classic_ra_dec(sl_names[1], img_names[0], sl_names[1].split("_")[-1][:-8], log_level=log_level) sl_names[1] = corrected_hla_slname # 1: get sourcelist data from files ref_data, comp_data = cu.slFiles2dataTables(sl_names) # 2: stack up RA and DEC columns ref_ra_dec_values = np.stack((ref_data['RA'], ref_data['DEC']), axis=1) comp_ra_dec_values = np.stack((comp_data['RA'], comp_data['DEC']), axis=1) # 3: transform comp frame RA, DEC into Ref frame X, Y values and ref RA, DEC into comp X, Y values ref_xy_in_comp_frame = hla_flag_filter.rdtoxy(ref_ra_dec_values, img_names[1], "[1]", origin=0) comp_xy_in_ref_frame = hla_flag_filter.rdtoxy(comp_ra_dec_values, img_names[0], "[1]", origin=0) # 4: (diagnostic only) write out region files to check coordinate transformation if diagnostic_mode: for data_table, reg_filename in zip([ref_data, comp_data, comp_xy_in_ref_frame], ["ref_orig.reg", "comp_orig.reg", "comp_xform.reg"]): write_region_file(data_table, reg_filename) new_comp_xy = np.stack((comp_xy_in_ref_frame[:, 0], comp_xy_in_ref_frame[:, 1]), axis=1) ref_xy = np.stack((ref_data['X'], ref_data['Y']), axis=1) comp_xy = np.stack((comp_data['X'], comp_data['Y']), axis=1) matches = xyxymatch(new_comp_xy, ref_xy, tolerance=5.0, separation=1.0) # Report number and percentage of the total number of detected ref and comp sources that were matched log.info("Sourcelist Matching Results") log.info( "Reference sourcelist: {} of {} total sources matched ({} %)".format(len(matches), len(ref_xy), 100.0 * (float(len(matches)) / float(len(ref_xy))))) log.info( "Comparison sourcelist: {} of {} total sources matched ({} %)".format(len(matches), len(new_comp_xy), 100.0 * (float(len(matches)) / float(len(new_comp_xy))))) # extract indices of the matching ref and comp lines and then use them to compile lists of matched X, Y, # RA and DEC for calculation of differences matched_lines_comp = [] matched_lines_ref = [] for item in matches: matched_lines_comp.append(item[2]) matched_lines_ref.append(item[5]) matching_values_ref_x = ref_data['X'][[matched_lines_ref]] matching_values_ref_y = ref_data['Y'][[matched_lines_ref]] matching_values_ref_ra = ref_data['RA'][[matched_lines_ref]] matching_values_ref_dec = ref_data['DEC'][[matched_lines_ref]] matching_values_comp_x = new_comp_xy[:, 0][[matched_lines_comp]] matching_values_comp_y = new_comp_xy[:, 1][[matched_lines_comp]] matching_values_comp_ra = comp_ra_dec_values[:, 0][[matched_lines_comp]] matching_values_comp_dec = comp_ra_dec_values[:, 1][[matched_lines_comp]] # get coordinate system type from fits headers ref_frame = fits.getval(img_names[0], "radesys", ext=('sci', 1)).lower() comp_frame = fits.getval(img_names[1], "radesys", ext=('sci', 1)).lower() # force RA and Dec values to be the correct type for SkyCoord() call if str(type(matching_values_ref_ra)) == "<class 'astropy.table.column.Column'>": matching_values_ref_ra = matching_values_ref_ra.tolist() if str(type(matching_values_ref_dec)) == "<class 'astropy.table.column.Column'>": matching_values_ref_dec = matching_values_ref_dec.tolist() if str(type(matching_values_comp_ra)) == "<class 'astropy.table.column.Column'>": matching_values_comp_ra = matching_values_comp_ra.tolist() if str(type(matching_values_comp_dec)) == "<class 'astropy.table.column.Column'>": matching_values_comp_dec = matching_values_comp_dec.tolist() # convert reference and comparison RA/Dec values into SkyCoord objects matching_values_ref_rd = SkyCoord(matching_values_ref_ra, matching_values_ref_dec, frame=ref_frame, unit="deg") matching_values_comp_rd = SkyCoord(matching_values_comp_ra, matching_values_comp_dec, frame=comp_frame, unit="deg") # convert to ICRS coord system if ref_frame != "icrs": matching_values_ref_rd = matching_values_ref_rd.icrs if comp_frame != "icrs": matching_values_comp_rd = matching_values_comp_rd.icrs # compute mean-subtracted differences diff_x = (matching_values_comp_x - matching_values_ref_x) diff_x -= sigma_clipped_stats(diff_x, sigma=3, maxiters=3)[0] diff_y = (matching_values_comp_y - matching_values_ref_y) diff_y -= sigma_clipped_stats(diff_y, sigma=3, maxiters=3)[0] diff_xy = np.sqrt(diff_x**2 + diff_y**2) diff_rd = matching_values_comp_rd.separation(matching_values_ref_rd).arcsec diff_list = [diff_x, diff_y, diff_xy, diff_rd] title_list = ["X-axis differences", "Y-axis differences", "Seperation", "On-sky separation"] units_list = ["HAP WFC3/UVIS pixels", "HAP WFC3/UVIS pixels", "HAP WFC3/UVIS pixels", "Arcseconds"] for diff_ra, title, units in zip(diff_list, title_list, units_list): log.info("Comparison - reference {} statistics ({})".format(title, units)) _ = compute_stats(diff_ra, title, log_level=log_level) generate_sorted_region_file(diff_xy, ref_xy_in_comp_frame[matched_lines_ref], comp_xy[matched_lines_comp], ref_data['FLAGS'][matched_lines_ref], comp_data['FLAGS'][matched_lines_comp])
def __call__(self, refcat, imcat, tp_wcs=None): r""" Performs catalog matching. Parameters ---------- refcat: astropy.table.Table A reference source catalog. When a tangent-plane ``WCS`` is provided through ``tp_wcs``, the catalog must contain ``'RA'`` and ``'DEC'`` columns which indicate reference source world coordinates (in degrees). Alternatively, when ``tp_wcs`` is `None`, reference catalog must contain ``'TPx'`` and ``'TPy'`` columns that provide undistorted (distortion-correction applied) source coordinates in some *tangent plane*. In this case, the ``'RA'`` and ``'DEC'`` columns in the ``refcat`` catalog will be ignored. imcat: astropy.table.Table Source catalog associated with an image. Must contain ``'x'`` and ``'y'`` columns which indicate source coordinates (in pixels) in the associated image. Alternatively, when ``tp_wcs`` is `None`, catalog must contain ``'TPx'`` and ``'TPy'`` columns that provide undistorted (distortion-correction applied) source coordinates in **the same**\ *tangent plane* used to define ``refcat``'s tangent plane coordinates. In this case, the ``'x'`` and ``'y'`` columns in the ``imcat`` catalog will be ignored. tp_wcs: TPWCS, None, optional A ``WCS`` that defines a tangent plane onto which both reference and image catalog sources can be projected. For this reason, ``tp_wcs`` is associated with the image in which sources from the ``imcat`` catalog were found in the sense that ``tp_wcs`` must be able to map image coordinates ``'x'`` and ``'y'`` from the ``imcat`` catalog to the tangent plane. When ``tp_wcs`` is provided, the ``'TPx'`` and ``'TPy'`` columns in both ``imcat`` and ``refcat`` catalogs will be ignored (if present). Returns ------- (refcat_idx, imcat_idx): tuple of numpy.ndarray A tuple of two 1D `numpy.ndarray` containing indices of matched sources in the ``refcat`` and ``imcat`` catalogs accordingly. """ # Check catalogs: if not isinstance(refcat, astropy.table.Table): raise TypeError("'refcat' must be an instance of " "astropy.table.Table") if not refcat: raise ValueError("Reference catalog must contain at least one " "source.") if not isinstance(imcat, astropy.table.Table): raise TypeError("'imcat' must be an instance of " "astropy.table.Table") if not imcat: raise ValueError("Image catalog must contain at least one " "source.") if tp_wcs is None: if 'TPx' not in refcat.colnames or 'TPy' not in refcat.colnames: raise KeyError("When tangent plane WCS is not provided, " "'refcat' must contain both 'TPx' and 'TPy' " "columns.") if 'TPx' not in imcat.colnames or 'TPy' not in imcat.colnames: raise KeyError("When tangent plane WCS is not provided, " "'imcat' must contain both 'TPx' and 'TPy' " "columns.") imxy = np.asarray([imcat['TPx'], imcat['TPy']]).T refxy = np.asarray([refcat['TPx'], refcat['TPy']]).T else: if 'RA' not in refcat.colnames or 'DEC' not in refcat.colnames: raise KeyError("When tangent plane WCS is provided, 'refcat' " "must contain both 'RA' and 'DEC' columns.") if 'x' not in imcat.colnames or 'y' not in imcat.colnames: raise KeyError("When tangent plane WCS is provided, 'imcat' " "must contain both 'x' and 'y' columns.") # compute x & y in the tangent plane provided by tp_wcs: imxy = np.asarray(tp_wcs.det_to_tanp(imcat['x'], imcat['y'])).T refxy = np.asarray( tp_wcs.world_to_tanp(refcat['RA'], refcat['DEC'])).T imcat_name = imcat.meta.get('name', 'Unnamed') if imcat_name is None: imcat_name = 'Unnamed' refcat_name = refcat.meta.get('name', 'Unnamed') if refcat_name is None: refcat_name = 'Unnamed' log.info("Matching sources from '{:s}' catalog with sources from the " "reference '{:s}' catalog.".format(imcat_name, refcat_name)) ps = 1.0 if tp_wcs is None else tp_wcs.tanp_center_pixel_scale if self._use2dhist: # Determine xyoff (X,Y offset) and tolerance # to be used with xyxymatch: zpxoff, zpyoff = _estimate_2dhist_shift(imxy / ps, refxy / ps, searchrad=self._searchrad) xyoff = (zpxoff * ps, zpyoff * ps) else: xyoff = (self._xoffset * ps, self._yoffset * ps) matches = xyxymatch(imxy, refxy, origin=xyoff, tolerance=ps * self._tolerance, separation=ps * self._separation) return matches['ref_idx'], matches['input_idx']
def match2ref(self, refcat, minobj=15, searchrad=1.0, searchunits='arcseconds', separation=0.5, use2dhist=True, xoffset=0.0, yoffset=0.0, tolerance=1.0): """ Uses xyxymatch to cross-match sources between this catalog and a reference catalog. Parameters ---------- refcat : RefCatalog A `RefCatalog` object that contains a catalog of reference sources as well as a valid reference WCS. minobj : int, None, optional Minimum number of identified objects from each input image to use in matching objects from other images. If the default `None` value is used then `align` will automatically deternmine the minimum number of sources from the value of the `fitgeom` parameter. searchrad : float, optional The search radius for a match. searchunits : str, optional Units for search radius. separation : float, optional The minimum separation for sources in the input and reference catalogs in order to be considered to be disctinct sources. Objects closer together than 'separation' pixels are removed from the input and reference coordinate lists prior to matching. This parameter gets passed directly to :py:func:`~stsci.stimage.xyxymatch` for use in matching the object lists from each image with the reference image's object list. use2dhist : bool, optional Use 2D histogram to find initial offset? xoffset : float, optional Initial estimate for the offset in X between the images and the reference frame. This offset will be used for all input images provided. This parameter is ignored when `use2dhist` is `True`. yoffset : float (Default = 0.0) Initial estimate for the offset in Y between the images and the reference frame. This offset will be used for all input images provided. This parameter is ignored when `use2dhist` is `True`. tolerance : float, optional The matching tolerance in pixels after applying an initial solution derived from the 'triangles' algorithm. This parameter gets passed directly to :py:func:`~stsci.stimage.xyxymatch` for use in matching the object lists from each image with the reference image's object list. """ colnames = self._catalog.colnames if 'xref' not in colnames or 'yref' not in colnames: raise RuntimeError("'calc_xyref()' should have been run prior " "to match2ref()") im_xyref = np.asanyarray([self._catalog['xref'], self._catalog['yref']]).T refxy = np.asanyarray([refcat.catalog['xref'], refcat.catalog['yref']]).T log.info("Matching sources from '{}' with sources from reference " "{:s} '{}'".format(self.name, 'image', refcat.name)) # convert tolerance from units of arcseconds to pixels, as needed if searchunits == 'arcseconds': searchrad /= refcat.pscale xyoff = (xoffset, yoffset) if use2dhist: # Determine xyoff (X,Y offset) and tolerance # to be used with xyxymatch: zpxoff, zpyoff, flux, zpqual = matchutils.build_xy_zeropoint( im_xyref, refxy, searchrad=searchrad ) if zpqual is not None: xyoff = (zpxoff, zpyoff) # set tolerance as well # This value allows initial guess to be off by 1 in both and # still pick up the identified matches tolerance = 1.5 matches = xyxymatch( im_xyref, refxy, origin=xyoff, tolerance=tolerance, separation=separation ) nmatches = len(matches) self._catalog.meta['nmatches'] = nmatches minput_idx = matches['input_idx'] catlen = len(self._catalog) # matched_ref_id: if 'matched_ref_id' not in colnames: c = table.MaskedColumn(name='matched_ref_id', dtype=int, length=catlen, mask=True) self._catalog.add_column(c) else: self._catalog['matched_ref_id'].mask = True self._catalog['matched_ref_id'][minput_idx] = \ self._catalog['id'][minput_idx] self._catalog['matched_ref_id'].mask[minput_idx] = False # this is needed to index reference catalog directly without using # astropy table indexing which at this moment is experimental: if '_raw_matched_ref_idx' not in colnames: c = table.MaskedColumn(name='_raw_matched_ref_idx', dtype=int, length=catlen, mask=True) self._catalog.add_column(c) else: self._catalog['_raw_matched_ref_idx'].mask = True self._catalog['_raw_matched_ref_idx'][minput_idx] = \ matches['ref_idx'] self._catalog['_raw_matched_ref_idx'].mask[minput_idx] = False log.info("Found {:d} matches for '{}'...".format(nmatches, self.name)) return matches