def setup_class(self): self.data = np.arange(20.).reshape(5, 4) self.position = SkyCoord('13h11m29.96s -01d19m18.7s', frame='icrs') wcs = WCS(naxis=2) rho = np.pi / 3. scale = 0.05 / 3600. wcs.wcs.cd = [[scale * np.cos(rho), -scale * np.sin(rho)], [scale * np.sin(rho), scale * np.cos(rho)]] wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] wcs.wcs.crval = [ self.position.ra.to_value(u.deg), self.position.dec.to_value(u.deg) ] wcs.wcs.crpix = [3, 3] self.wcs = wcs # add SIP sipwcs = wcs.deepcopy() sipwcs.wcs.ctype = ['RA---TAN-SIP', 'DEC--TAN-SIP'] a = np.array( [[0, 0, 5.33092692e-08, 3.73753773e-11, -2.02111473e-13], [0, 2.44084308e-05, 2.81394789e-11, 5.17856895e-13, 0.0], [-2.41334657e-07, 1.29289255e-10, 2.35753629e-14, 0.0, 0.0], [-2.37162007e-10, 5.43714947e-13, 0.0, 0.0, 0.0], [-2.81029767e-13, 0.0, 0.0, 0.0, 0.0]]) b = np.array( [[0, 0, 2.99270374e-05, -2.38136074e-10, 7.23205168e-13], [0, -1.71073858e-07, 6.31243431e-11, -5.16744347e-14, 0.0], [6.95458963e-06, -3.08278961e-10, -1.75800917e-13, 0.0, 0.0], [3.51974159e-11, 5.60993016e-14, 0.0, 0.0, 0.0], [-5.92438525e-13, 0.0, 0.0, 0.0, 0.0]]) sipwcs.sip = Sip(a, b, None, None, wcs.wcs.crpix) sipwcs.wcs.set() self.sipwcs = sipwcs
def extract(self, cutout_region): data = self.data data_shape = data.shape position, shape = self._get_position_shape(data_shape, cutout_region) logger.debug('Position {} and Shape {}'.format(position, shape)) # No pixels specified, so return the entire HDU if (not position and not shape) or shape == data_shape: logger.debug('Returning entire HDU data for {}'.format( cutout_region.get_extension())) cutout_data = data else: logger.debug('Cutting out {} at {} for extension {} from \ {}.'.format(shape, position, cutout_region.get_extension(), data.shape)) cutout_data, position = extract_array(data, shape, position, mode='partial', return_position=True) if self.wcs: cutout_shape = cutout_data.shape output_wcs = deepcopy(self.wcs) wcs_crpix = output_wcs.wcs.crpix l_wcs_crpix = len(wcs_crpix) ranges = cutout_region.get_ranges() logger.debug('Adjusting WCS.') for idx, _ in enumerate(ranges): if idx < l_wcs_crpix: wcs_crpix[idx] -= (ranges[idx][0] - 1.0) output_wcs._naxis = list(cutout_shape) if self.wcs.sip: curr_sip = self.wcs.sip output_wcs.sip = Sip(curr_sip.a, curr_sip.b, curr_sip.ap, curr_sip.bp, wcs_crpix[0:2]) logger.debug('WCS adjusted.') else: logger.debug('No WCS present.') output_wcs = None return CutoutResult(data=cutout_data, wcs=output_wcs, wcs_crpix=wcs_crpix)
def set_wcs(self, x0): """ x0 = [0:crpix1, 1:crpix2, 2:cdelt1, 3:cdelt2, 4:pc, 5:pc, 6:pc, 7:pc, sip... ] """ # Referece Pixel self.w.wcs.crpix = [x0[0], x0[1]] # Set the cdelt values self.w.wcs.cdelt = [x0[2], x0[3]] # Set the pc matrix self.w.wcs.pc = x0[4:8].reshape((2, 2)) # Make a new SIP if np.size(x0) > 8: a = x0[self.a_ind].reshape((self.a_order + 1, self.a_order + 1)) b = x0[self.b_ind].reshape((self.b_order + 1, self.b_order + 1)) self.w.sip = Sip(a, b, self.sip_zeros_a, self.sip_zeros_b, self.w.wcs.crpix)
def extract(self, cutout_region): data = self.data data_shape = data.shape position, shape = self._get_position_shape(data_shape, cutout_region) self.logger.debug('Position {} and Shape {}'.format(position, shape)) # No pixels specified, so return the entire HDU if (not position and not shape) or shape == data_shape: self.logger.debug('Returning entire HDU data for {}'.format( cutout_region.get_extension())) cutout_data = data else: self.logger.debug('Cutting out {} at {} for extension {} from {}.'.format( shape, position, cutout_region.get_extension(), data.shape)) cutout_data, position = extract_array(data, shape, position, mode='partial', return_position=True) if self.wcs is not None: cutout_shape = cutout_data.shape output_wcs = deepcopy(self.wcs) wcs_crpix = output_wcs.wcs.crpix ranges = cutout_region.get_ranges() l_ranges = len(ranges) while len(wcs_crpix) < l_ranges: wcs_crpix = np.append(wcs_crpix, 1.0) for idx, _ in enumerate(ranges): wcs_crpix[idx] -= (ranges[idx][0] - 1) output_wcs._naxis = list(cutout_shape) if self.wcs.sip is not None: curr_sip = self.wcs.sip output_wcs.sip = Sip(curr_sip.a, curr_sip.b, curr_sip.ap, curr_sip.bp, wcs_crpix[0:2]) else: output_wcs = None return CutoutResult(data=cutout_data, wcs=output_wcs, wcs_crpix=wcs_crpix)
def fit_wcs_from_points(xy, world_coords, proj_point='center', projection='TAN', sip_degree=None): # pragma: no cover """ Given two matching sets of coordinates on detector and sky, compute the WCS. Fits a WCS object to matched set of input detector and sky coordinates. Optionally, a SIP can be fit to account for geometric distortion. Returns an `~astropy.wcs.WCS` object with the best fit parameters for mapping between input pixel and sky coordinates. The projection type (default 'TAN') can passed in as a string, one of the valid three-letter projection codes - or as a WCS object with projection keywords already set. Note that if an input WCS has any non-polynomial distortion, this will be applied and reflected in the fit terms and coefficients. Passing in a WCS object in this way essentially allows it to be refit based on the matched input coordinates and projection point, but take care when using this option as non-projection related keywords in the input might cause unexpected behavior. Notes ------ - The fiducial point for the spherical projection can be set to 'center' to use the mean position of input sky coordinates, or as an `~astropy.coordinates.SkyCoord` object. - Units in all output WCS objects will always be in degrees. - If the coordinate frame differs between `~astropy.coordinates.SkyCoord` objects passed in for ``world_coords`` and ``proj_point``, the frame for ``world_coords`` will override as the frame for the output WCS. - If a WCS object is passed in to ``projection`` the CD/PC matrix will be used as an initial guess for the fit. If this is known to be significantly off and may throw off the fit, set to the identity matrix (for example, by doing wcs.wcs.pc = [(1., 0.,), (0., 1.)]) Parameters ---------- xy : tuple of two `numpy.ndarray` x & y pixel coordinates. world_coords : `~astropy.coordinates.SkyCoord` Skycoord object with world coordinates. proj_point : 'center' or ~astropy.coordinates.SkyCoord` Defaults to 'center', in which the geometric center of input world coordinates will be used as the projection point. To specify an exact point for the projection, a Skycoord object with a coordinate pair can be passed in. For consistency, the units and frame of these coordinates will be transformed to match ``world_coords`` if they don't. projection : str or `~astropy.wcs.WCS` Three letter projection code, of any of standard projections defined in the FITS WCS standard. Optionally, a WCS object with projection keywords set may be passed in. sip_degree : None or int If set to a non-zero integer value, will fit SIP of degree ``sip_degree`` to model geometric distortion. Defaults to None, meaning no distortion corrections will be fit. Returns ------- wcs : `~astropy.wcs.WCS` The best-fit WCS to the points given. """ from astropy.coordinates import SkyCoord # here to avoid circular import import astropy.units as u from astropy.wcs import Sip from scipy.optimize import least_squares xp, yp = xy try: lon, lat = world_coords.data.lon.deg, world_coords.data.lat.deg except AttributeError: unit_sph = world_coords.unit_spherical lon, lat = unit_sph.lon.deg, unit_sph.lat.deg # verify input if (proj_point != 'center') and (type(proj_point) != type(world_coords)): raise ValueError("proj_point must be set to 'center', or an" + "`~astropy.coordinates.SkyCoord` object with " + "a pair of points.") if proj_point != 'center': assert proj_point.size == 1 proj_codes = [ 'AZP', 'SZP', 'TAN', 'STG', 'SIN', 'ARC', 'ZEA', 'AIR', 'CYP', 'CEA', 'CAR', 'MER', 'SFL', 'PAR', 'MOL', 'AIT', 'COP', 'COE', 'COD', 'COO', 'BON', 'PCO', 'TSC', 'CSC', 'QSC', 'HPX', 'XPH' ] if type(projection) == str: if projection not in proj_codes: raise ValueError( "Must specify valid projection code from list of " + "supported types: ", ', '.join(proj_codes)) # empty wcs to fill in with fit values wcs = celestial_frame_to_wcs(frame=world_coords.frame, projection=projection) else: #if projection is not string, should be wcs object. use as template. wcs = copy.deepcopy(projection) wcs.cdelt = (1., 1.) # make sure cdelt is 1 wcs.sip = None # Change PC to CD, since cdelt will be set to 1 if wcs.wcs.has_pc(): wcs.wcs.cd = wcs.wcs.pc wcs.wcs.__delattr__('pc') if (type(sip_degree) != type(None)) and (type(sip_degree) != int): raise ValueError("sip_degree must be None, or integer.") # set pixel_shape to span of input points wcs.pixel_shape = (xp.max() + 1 - xp.min(), yp.max() + 1 - yp.min()) # determine CRVAL from input close = lambda l, p: p[np.argmin(np.abs(l))] if str(proj_point) == 'center': # use center of input points sc1 = SkyCoord(lon.min() * u.deg, lat.max() * u.deg) sc2 = SkyCoord(lon.max() * u.deg, lat.min() * u.deg) pa = sc1.position_angle(sc2) sep = sc1.separation(sc2) midpoint_sc = directional_offset_by(sc1, pa, sep / 2) wcs.wcs.crval = ((midpoint_sc.data.lon.deg, midpoint_sc.data.lat.deg)) wcs.wcs.crpix = ((xp.max() + xp.min()) / 2., (yp.max() + yp.min()) / 2.) elif proj_point is not None: # convert units, initial guess for crpix proj_point.transform_to(world_coords) wcs.wcs.crval = (proj_point.data.lon.deg, proj_point.data.lat.deg) wcs.wcs.crpix = (close(lon - wcs.wcs.crval[0], xp), close(lon - wcs.wcs.crval[1], yp)) # fit linear terms, assign to wcs # use (1, 0, 0, 1) as initial guess, in case input wcs was passed in # and cd terms are way off. p0 = np.concatenate([wcs.wcs.cd.flatten(), wcs.wcs.crpix.flatten()]) xpmin, xpmax, ypmin, ypmax = xp.min(), xp.max(), yp.min(), yp.max() if xpmin == xpmax: xpmin, xpmax = xpmin - 0.5, xpmax + 0.5 if ypmin == ypmax: ypmin, ypmax = ypmin - 0.5, ypmax + 0.5 fit = least_squares( _linear_wcs_fit, p0, args=(lon, lat, xp, yp, wcs), bounds=[[-np.inf, -np.inf, -np.inf, -np.inf, xpmin, ypmin], [np.inf, np.inf, np.inf, np.inf, xpmax, ypmax]]) wcs.wcs.crpix = np.array(fit.x[4:6]) wcs.wcs.cd = np.array(fit.x[0:4].reshape((2, 2))) # fit SIP, if specified. Only fit forward coefficients if sip_degree: degree = sip_degree if '-SIP' not in wcs.wcs.ctype[0]: wcs.wcs.ctype = [x + '-SIP' for x in wcs.wcs.ctype] coef_names = [ '{0}_{1}'.format(i, j) for i in range(degree + 1) for j in range(degree + 1) if (i + j) < (degree + 1) and (i + j) > 1 ] p0 = np.concatenate((np.array(wcs.wcs.crpix), wcs.wcs.cd.flatten(), np.zeros(2 * len(coef_names)))) fit = least_squares(_sip_fit, p0, args=(lon, lat, xp, yp, wcs, degree, coef_names)) coef_fit = (list(fit.x[6:6 + len(coef_names)]), list(fit.x[6 + len(coef_names):])) # put fit values in wcs wcs.wcs.cd = fit.x[2:6].reshape((2, 2)) wcs.wcs.crpix = fit.x[0:2] a_vals = np.zeros((degree + 1, degree + 1)) b_vals = np.zeros((degree + 1, degree + 1)) for coef_name in coef_names: a_vals[int(coef_name[0])][int(coef_name[2])] = coef_fit[0].pop(0) b_vals[int(coef_name[0])][int(coef_name[2])] = coef_fit[1].pop(0) wcs.sip = Sip(a_vals, b_vals, np.zeros((degree + 1, degree + 1)), np.zeros((degree + 1, degree + 1)), wcs.wcs.crpix) return wcs
def __init__(self, data, position, size, wcs=None, mode='trim', fill_value=np.nan, copy=False): if wcs is None: wcs = getattr(data, 'wcs', None) if isinstance(position, SkyCoord): if wcs is None: raise ValueError('wcs must be input if position is a ' 'SkyCoord') position = skycoord_to_pixel(position, wcs, mode='all') # (x, y) if np.isscalar(size): size = np.repeat(size, 2) # special handling for a scalar Quantity if isinstance(size, u.Quantity): size = np.atleast_1d(size) if len(size) == 1: size = np.repeat(size, 2) if len(size) > 2: raise ValueError('size must have at most two elements') shape = np.zeros(2).astype(int) pixel_scales = None # ``size`` can have a mixture of int and Quantity (and even units), # so evaluate each axis separately for axis, side in enumerate(size): if not isinstance(side, u.Quantity): shape[axis] = int(np.round(size[axis])) # pixels else: if side.unit == u.pixel: shape[axis] = int(np.round(side.value)) elif side.unit.physical_type == 'angle': if wcs is None: raise ValueError('wcs must be input if any element ' 'of size has angular units') if pixel_scales is None: pixel_scales = u.Quantity( proj_plane_pixel_scales(wcs), wcs.wcs.cunit[axis]) shape[axis] = int(np.round( (side / pixel_scales[axis]).decompose())) else: raise ValueError('shape can contain Quantities with only ' 'pixel or angular units') data = np.asanyarray(data) # reverse position because extract_array and overlap_slices # use (y, x), but keep the input position pos_yx = position[::-1] cutout_data, input_position_cutout = extract_array( data, tuple(shape), pos_yx, mode=mode, fill_value=fill_value, return_position=True) if copy: cutout_data = np.copy(cutout_data) self.data = cutout_data self.input_position_cutout = input_position_cutout[::-1] # (x, y) slices_original, slices_cutout = overlap_slices( data.shape, shape, pos_yx, mode=mode) self.slices_original = slices_original self.slices_cutout = slices_cutout self.shape = self.data.shape self.input_position_original = position self.shape_input = shape ((self.ymin_original, self.ymax_original), (self.xmin_original, self.xmax_original)) = self.bbox_original ((self.ymin_cutout, self.ymax_cutout), (self.xmin_cutout, self.xmax_cutout)) = self.bbox_cutout # the true origin pixel of the cutout array, including any # filled cutout values self._origin_original_true = ( self.origin_original[0] - self.slices_cutout[1].start, self.origin_original[1] - self.slices_cutout[0].start) if wcs is not None: self.wcs = deepcopy(wcs) self.wcs.wcs.crpix -= self._origin_original_true self.wcs.array_shape = self.data.shape if wcs.sip is not None: self.wcs.sip = Sip(wcs.sip.a, wcs.sip.b, wcs.sip.ap, wcs.sip.bp, wcs.sip.crpix - self._origin_original_true) else: self.wcs = None
def solve_field(engine, xy, flux=None, width=None, height=None, ra_hours=0, dec_degs=0, radius=180, min_scale=0.1, max_scale=10, parity=None, sip_order=3, crpix_center=True, max_sources=None, retry_lost=True, callback=None): """ Obtain astrometric solution given XY coordinates of field stars :param :class:`Solver` engine: Astrometry.net engine solver instance :param array_like xy: (n x 2) array of 0-based X and Y pixel coordinates of stars :param array_like flux: optional n-element array of star fluxes :param int width: image width in pixels; defaults to the maximum minus minimum X coordinate of stars :param int height: image height in pixels; defaults to the maximum minus minimum Y coordinate of stars :param float ra_hours: optional RA of image center in hours; default: 0 :param float dec_degs: optional Dec of image center in degrees; default: 0 :param float radius: optional field search radius in degrees; default: 180 (search over the whole sky) :param float min_scale: optional minimum pixel scale in arcseconds per pixel; default: 0.1 :param float max_scale: optional maximum pixel scale in arcseconds per pixel; default: 10 :param bool | None parity: image parity (sign of coordinate transformation matrix determinant): True = normal parity, False = flipped image, None (default) = try both :param int sip_order: order of SIP distortion terms; default: 3; 0 - disable calculation of distortion :param bool crpix_center: set reference pixel to image center :param int max_sources: use only the given number of brightest sources; 0/""/None (default) = no limit :param bool retry_lost: if solution failed, retry in the "lost in space" mode, i.e. without coordinate restrictions (`radius` = 180) and with opposite parity, unless the initial search already had these restrictions disabled :param callable callback: optional callable that is regularly called by the solver, accepts no arguments, and returns 0 to interrupt the solution and 1 otherwise :return: astrometric solution object; its `wcs` attribute is set to None if solution was not found :rtype: :class:`Solution` """ solver = engine.solver ra = float(ra_hours) * 15 dec = float(dec_degs) r = float(radius) # Set timer callback if requested if callback is not None: an_engine.set_timer_callback( solver, ctypes.cast( ctypes.CFUNCTYPE(ctypes.c_int)(callback), ctypes.c_voidp).value) else: an_engine.set_timer_callback(solver, 0) # Set field star position array n = len(xy) xy = numpy.asanyarray(xy) field = an_engine.starxy_new(n, flux is not None, False) if flux is not None: flux = numpy.asanyarray(flux) if len(flux) != n: raise ValueError( 'Flux array must be of the same length as XY array') if max_sources: order = numpy.argsort(flux)[::-1] xy, flux = xy[order], flux[order] del order an_engine.starxy_set_flux_array(field, flux) an_engine.starxy_set_xy_array(field, xy.ravel()) an_engine.solver_set_field(solver, field) try: # Initialize solver parameters if width: minx, maxx = 0, int(width) - 1 else: minx, maxx = xy[:, 0].min(), xy[:, 0].max() if height: miny, maxy = 0, int(height) - 1 else: miny, maxy = xy[:, 1].min(), xy[:, 1].max() an_engine.solver_set_field_bounds(solver, minx, maxx, miny, maxy) solver.quadsize_min = 0.1 * min(maxx - minx + 1, maxy - miny + 1) if crpix_center != '': solver.set_crpix = solver.set_crpix_center = int(crpix_center) an_engine.solver_set_radec(solver, ra, dec, r) solver.funits_lower = float(min_scale) solver.funits_upper = float(max_scale) solver.logratio_tokeep = numpy.log(1e12) solver.distance_from_quad_bonus = True if parity is None or parity == '': solver.parity = an_engine.PARITY_BOTH elif int(parity): solver.parity = an_engine.PARITY_NORMAL else: solver.parity = an_engine.PARITY_FLIP enable_sip = sip_order and int(sip_order) >= 2 if enable_sip: solver.do_tweak = True solver.tweak_aborder = int(sip_order) solver.tweak_abporder = int(sip_order) + 1 else: solver.do_tweak = False if max_sources: solver.endobj = max_sources else: solver.endobj = 0 # Find indexes needed to solve the field fmin = solver.quadsize_min * min_scale fmax = numpy.hypot(width, height) * max_scale indices = [] for index in engine.indexes: if fmin > index.index_scale_upper or \ fmax < index.index_scale_lower: continue if not an_engine.index_is_within_range(index, ra, dec, r): continue indices.append(index) if not len(indices): raise ValueError( 'No indexes found for the given scale and position') # Sort indices by scale (larger scales/smaller indices first - should # be faster) then by distance from expected position indices.sort(key=lambda _idx: ( -_idx.index_scale_upper, an_engine.healpix_distance_to_radec(_idx.healpix, _idx.hpnside, ra, dec)[0] if _idx.healpix >= 0 else 0, )) an_engine.solver_clear_indexes(solver) for index in indices: an_engine.solver_add_index(solver, index) # Run the solver an_engine.solver_run(solver) sol = Solution() if solver.have_best_match: best_match = solver.best_match sol.log_odds = solver.best_logodds sol.n_match = best_match.nmatch sol.n_conflict = best_match.nconflict sol.n_field = best_match.nfield if best_match.index is not None: sol.index_name = best_match.index.indexname else: best_match = None if solver.best_match_solves: # Get WCS parameters of best solution sol.wcs = WCS(naxis=2) wcs_ctype = ('RA---TAN', 'DEC--TAN') if enable_sip: sip = best_match.sip wcstan = sip.wcstan a_order, b_order = sip.a_order, sip.b_order if a_order > 0 or b_order > 0: ap_order, bp_order = sip.ap_order, sip.bp_order maxorder = an_engine.SIP_MAXORDER a = array_from_swig(sip.a, (maxorder, maxorder))[:a_order + 1, :a_order + 1] b = array_from_swig(sip.b, (maxorder, maxorder))[:b_order + 1, :b_order + 1] if a.any() or b.any(): ap = array_from_swig( sip.ap, (maxorder, maxorder))[:ap_order + 1, :ap_order + 1] bp = array_from_swig( sip.bp, (maxorder, maxorder))[:bp_order + 1, :bp_order + 1] sol.wcs.sip = Sip(a, b, ap, bp, sol.wcs.wcs.crpix) wcs_ctype = ('RA---TAN-SIP', 'DEC--TAN-SIP') else: wcstan = best_match.wcstan sol.wcs.wcs.ctype = wcs_ctype sol.wcs.wcs.crpix = array_from_swig(wcstan.crpix, (2, )) sol.wcs.wcs.crval = array_from_swig(wcstan.crval, (2, )) sol.wcs.wcs.cd = array_from_swig(wcstan.cd, (2, 2)) elif retry_lost and (radius < 180 or parity is not None): # When no solution was found, retry with all constraints relaxed an_engine.solver_cleanup_field(solver) return solve_field(engine, xy, flux, width, height, 0, 0, 180, min_scale, max_scale, None, sip_order, crpix_center, max_sources, retry_lost=False) return sol finally: # Cleanup and make solver ready for the next solution an_engine.solver_cleanup_field(solver) an_engine.solver_clear_indexes(solver)
def format_wcs(self, cutout_shape): """ Re-calculate the CRPIX values for the WCS. The SIP values are also re-calculated, if present. CRPIX values are tricky and there exists some black magic math in here, not to mention some values are set differently to accommodate the subtle variations with Python 2/3 float values. Parameters ---------- cutout_shape: The tuple containing the bounding values (shape) of the resulting cutout. Returns ------- The formatted WCS object. """ output_wcs = deepcopy(self.wcs) wcs_crpix = output_wcs.wcs.crpix l_wcs_crpix = len(wcs_crpix) logger.debug('Adjusting WCS.') for idx, cutout_region in enumerate(cutout_shape): if idx < l_wcs_crpix: curr_val = wcs_crpix[idx] start = cutout_region.start step = cutout_region.step if start: wcs_crpix[idx] -= float(ceil(start + 0.5)) if step is not None: logger.debug('Taking step {} into account.'.format(step)) wcs_crpix[idx] /= step if start: wcs_crpix[idx] += 1.0 logger.debug( 'Adjusted wcs_crpix val from {} to {}'.format( curr_val, wcs_crpix[idx])) if output_wcs.wcs.has_pc(): pc = output_wcs.wcs.pc for i in range(output_wcs.wcs.naxis): for j in range(output_wcs.wcs.naxis): step = cutout_shape[j].step if step: pc[i][j] *= step elif output_wcs.wcs.has_cd(): cd = output_wcs.wcs.cd for i in range(output_wcs.wcs.naxis): for j in range(output_wcs.wcs.naxis): step = cutout_shape[j].step if step: cd[i][j] *= step if self.wcs.sip: curr_sip = self.wcs.sip output_wcs.sip = Sip(curr_sip.a, curr_sip.b, curr_sip.ap, curr_sip.bp, wcs_crpix[0:2]) logger.debug('WCS adjusted.') return output_wcs