def __init__(self, input_system, output_system): super(CoordinateTransform, self).__init__() self._input_system_name = input_system self._output_system_name = output_system if isinstance(self._input_system_name, WCS): self.input_system = wcs_to_celestial_frame(self._input_system_name) elif isinstance(self._input_system_name, six.string_types): self.input_system = frame_transform_graph.lookup_name(self._input_system_name) if self.input_system is None: raise ValueError("Frame {0} not found".format(self._input_system_name)) elif isinstance(self._input_system_name, BaseCoordinateFrame): self.input_system = self._input_system_name else: raise TypeError("input_system should be a WCS instance, string, or a coordinate frame instance") if isinstance(self._output_system_name, WCS): self.output_system = wcs_to_celestial_frame(self._output_system_name) elif isinstance(self._output_system_name, six.string_types): self.output_system = frame_transform_graph.lookup_name(self._output_system_name) if self.output_system is None: raise ValueError("Frame {0} not found".format(self._output_system_name)) elif isinstance(self._output_system_name, BaseCoordinateFrame): self.output_system = self._output_system_name else: raise TypeError("output_system should be a WCS instance, string, or a coordinate frame instance") if self.output_system == self.input_system: self.same_frames = True else: self.same_frames = False
def test_crop_rotated_celestial(ndcube_4d_ln_lt_l_t): # This is a regression test for a highly rotated image where all 4 corners # of the spatial ROI have to be used. header = dedent("""\ WCSAXES = 2 / Number of coordinate axes CRPIX1 = 2053.459961 / Pixel coordinate of reference point CRPIX2 = 2047.880005 / Pixel coordinate of reference point PC1_1 = 0.70734471922412 / Coordinate transformation matrix element PC1_2 = 0.70686876305701 / Coordinate transformation matrix element PC2_1 = -0.70686876305701 / Coordinate transformation matrix element PC2_2 = 0.70734471922412 / Coordinate transformation matrix element CDELT1 = 0.00016652472222222 / [deg] Coordinate increment at reference point CDELT2 = 0.00016652472222222 / [deg] Coordinate increment at reference point CUNIT1 = 'deg' / Units of coordinate increment and value CUNIT2 = 'deg' / Units of coordinate increment and value CTYPE1 = 'HPLN-TAN' / Coordinate type codegnomonic projection CTYPE2 = 'HPLT-TAN' / Coordinate type codegnomonic projection CRVAL1 = 0.0 / [deg] Coordinate value at reference point CRVAL2 = 0.0 / [deg] Coordinate value at reference point LONPOLE = 180.0 / [deg] Native longitude of celestial pole LATPOLE = 0.0 / [deg] Native latitude of celestial pole MJDREF = 0.0 / [d] MJD of fiducial time DATE-OBS= '2014-04-09T06:00:12.970' / ISO-8601 time of observation MJD-OBS = 56756.250150116 / [d] MJD of observation RSUN_REF= 696000000.0 / [m] Solar radius DSUN_OBS= 149860273889.04 / [m] Distance from centre of Sun to observer HGLN_OBS= -0.0058904803279347 / [deg] Stonyhurst heliographic lng of observer HGLT_OBS= -6.0489216362492 / [deg] Heliographic latitude of observer """) wcs = WCS(fits.Header.fromstring(header, sep="\n")) data = np.zeros((4096, 4096)) cube = NDCube(data, wcs=wcs) bottom_left = SkyCoord(-100, -100, unit=u.arcsec, frame=wcs_to_celestial_frame(wcs)) bottom_right = SkyCoord(600, -100, unit=u.arcsec, frame=wcs_to_celestial_frame(wcs)) top_left = SkyCoord(-100, 600, unit=u.arcsec, frame=wcs_to_celestial_frame(wcs)) top_right = SkyCoord(600, 600, unit=u.arcsec, frame=wcs_to_celestial_frame(wcs)) small = cube.crop(bottom_left, bottom_right, top_left, top_right) assert small.data.shape == (1652, 1652)
def _convert_wcs(lon_in, lat_in, frame_in, frame_out): """Convert (longitude, latitude) coordinates from the input frame to the specified output frame. Parameters ---------- lon_in : 1D `~numpy.ndarray` The longitude to convert, unit degree, [0, 360) lat_in : 1D `~numpy.ndarray` The latitude to convert, unit degree, [-90, 90] frame_in, frame_out : tuple or `~astropy.wcs.WCS` The input and output frames, which can be passed either as a tuple of ``(frame, lon_unit, lat_unit)`` or as a `~astropy.wcs.WCS` instance. Returns ------- lon_out, lat_out : 1D `~numpy.ndarray` Output longitude and latitude in the output frame References ---------- [1] reproject - wcs_utils.convert_world_coordinates() https://github.com/astrofrog/reproject """ if isinstance(frame_in, WCS): coordframe_in = wcs_to_celestial_frame(frame_in) lon_in_unit = au.Unit(frame_in.wcs.cunit[0]) lat_in_unit = au.Unit(frame_in.wcs.cunit[1]) else: coordframe_in, lon_in_unit, lat_in_unit = frame_in # if isinstance(frame_out, WCS): coordframe_out = wcs_to_celestial_frame(frame_out) lon_out_unit = au.Unit(frame_out.wcs.cunit[0]) lat_out_unit = au.Unit(frame_out.wcs.cunit[1]) else: coordframe_out, lon_out_unit, lat_out_unit = frame_out # logger.info("Convert coordinates from {0} to {1}".format( coordframe_in, coordframe_out)) logger.info("Input coordinates units: " "{0} (longitude), {1} (latitude)".format( lon_in_unit, lat_in_unit)) logger.info("Output coordinates units: " "{0} (longitude), {1} (latitude)".format( lon_out_unit, lat_out_unit)) # data = UnitSphericalRepresentation(lon_in * lon_in_unit, lat_in * lat_in_unit) coords_in = coordframe_in.realize_frame(data) coords_out = coords_in.transform_to(coordframe_out) data_out = coords_out.represent_as("unitspherical") lon_out = data_out.lon.to(lon_out_unit).value lat_out = data_out.lat.to(lon_out_unit).value return lon_out, lat_out
def _get_transform_no_transdata(self, frame): """ Return a transform from data to the specified frame """ if self.wcs is None and frame != 'pixel': raise ValueError( 'No WCS specified, so only pixel coordinates are available') if isinstance(frame, WCS): coord_in = wcs_to_celestial_frame(self.wcs) coord_out = wcs_to_celestial_frame(frame) if coord_in == coord_out: return (WCSPixel2WorldTransform(self.wcs, slice=self.slices) + WCSWorld2PixelTransform(frame)) else: return (WCSPixel2WorldTransform(self.wcs, slice=self.slices) + CoordinateTransform(self.wcs, frame) + WCSWorld2PixelTransform(frame)) elif frame == 'pixel': return Affine2D() elif isinstance(frame, Transform): pixel2world = WCSPixel2WorldTransform(self.wcs, slice=self.slices) return pixel2world + frame else: pixel2world = WCSPixel2WorldTransform(self.wcs, slice=self.slices) if frame == 'world': return pixel2world else: coordinate_transform = CoordinateTransform(self.wcs, frame) if coordinate_transform.same_frames: return pixel2world else: return pixel2world + CoordinateTransform(self.wcs, frame)
def _get_transform_no_transdata(self, frame): """ Return a transform from data to the specified frame """ if self.wcs is None and frame != 'pixel': raise ValueError('No WCS specified, so only pixel coordinates are available') if isinstance(frame, WCS): coord_in = wcs_to_celestial_frame(self.wcs) coord_out = wcs_to_celestial_frame(frame) if coord_in == coord_out: return (WCSPixel2WorldTransform(self.wcs, slice=self.slices) + WCSWorld2PixelTransform(frame)) else: return (WCSPixel2WorldTransform(self.wcs, slice=self.slices) + CoordinateTransform(self.wcs, frame) + WCSWorld2PixelTransform(frame)) elif frame == 'pixel': return Affine2D() elif isinstance(frame, Transform): pixel2world = WCSPixel2WorldTransform(self.wcs, slice=self.slices) return pixel2world + frame else: pixel2world = WCSPixel2WorldTransform(self.wcs, slice=self.slices) if frame == 'world': return pixel2world else: coordinate_transform = CoordinateTransform(self.wcs, frame) if coordinate_transform.same_frames: return pixel2world else: return pixel2world + CoordinateTransform(self.wcs, frame)
def convert_world_coordinates(lon_in, lat_in, wcs_in, wcs_out): """ Convert longitude/latitude coordinates from an input frame to an output frame. Parameters ---------- lon_in, lat_in : `~numpy.ndarray` The longitude and latitude to convert wcs_in, wcs_out : tuple or `~astropy.wcs.WCS` The input and output frames, which can be passed either as a tuple of ``(frame, lon_unit, lat_unit)`` or as a `~astropy.wcs.WCS` instance. Returns ------- lon_out, lat_out : `~numpy.ndarray` The output longitude and latitude """ if isinstance(wcs_in, WCS): # Extract the celestial component of the WCS in (lon, lat) order wcs_in = wcs_in.celestial frame_in = wcs_to_celestial_frame(wcs_in) lon_in_unit = u.Unit(wcs_in.wcs.cunit[0]) lat_in_unit = u.Unit(wcs_in.wcs.cunit[1]) else: frame_in, lon_in_unit, lat_in_unit = wcs_in if isinstance(wcs_out, WCS): # Extract the celestial component of the WCS in (lon, lat) order wcs_out = wcs_out.celestial frame_out = wcs_to_celestial_frame(wcs_out) lon_out_unit = u.Unit(wcs_out.wcs.cunit[0]) lat_out_unit = u.Unit(wcs_out.wcs.cunit[1]) else: frame_out, lon_out_unit, lat_out_unit = wcs_out data = UnitSphericalRepresentation(lon_in * lon_in_unit, lat_in * lat_in_unit) coords_in = frame_in.realize_frame(data) coords_out = coords_in.transform_to(frame_out) lon_out = coords_out.represent_as('unitspherical').lon.to( lon_out_unit).value lat_out = coords_out.represent_as('unitspherical').lat.to( lat_out_unit).value return lon_out, lat_out
def __init__(self, wcs, npix, cdelt=None, crpix=None, axes=None, cutout_info=None): self._wcs = wcs self._frame = wcs_to_celestial_frame(wcs).name self._projection = wcs.wcs.ctype[0][5:] self._axes = MapAxes.from_default(axes) if cdelt is None: cdelt = tuple(np.abs(self.wcs.wcs.cdelt)) # Shape to use for WCS transformations wcs_shape = max([get_shape(t) for t in [npix, cdelt]]) self._npix = cast_to_shape(npix, wcs_shape, int) self._cdelt = cast_to_shape(cdelt, wcs_shape, float) # By convention CRPIX is indexed from 1 if crpix is None: crpix = tuple(1.0 + (np.array(self._npix) - 1.0) / 2.0) self._crpix = crpix self._cutout_info = cutout_info # define cached methods self.get_coord = lru_cache()(self.get_coord) self.get_pix = lru_cache()(self.get_pix) self.solid_angle = lru_cache()(self.solid_angle) self.bin_volume = lru_cache()(self.bin_volume) self.to_image = lru_cache()(self.to_image)
def get_xy(self, wcs=None): """ Return the pixel coordinates of the path. If the path is defined in world coordinates, the appropriate WCS transformation should be passed. Parameters ---------- wcs : :class:`~astropy.wcs.WCS` The WCS transformation to assume in order to transform the path to pixel coordinates. """ if self._xy is not None: return self._xy else: if wcs is None: raise ValueError("`wcs` is needed in order to compute " "the pixel coordinates") else: # Extract the celestial component of the WCS wcs_sky = wcs.sub([WCSSUB_CELESTIAL]) # Find the astropy name for the coordinates celestial_system = wcs_to_celestial_frame(wcs_sky) world_coords = self._coords.transform_to(celestial_system) xw, yw = world_coords.spherical.lon.degree, world_coords.spherical.lat.degree return list(zip(*wcs_sky.wcs_world2pix(xw, yw, 0)))
def _set_data_coord_system(self, data): """ Check if data coordinates are in RA-DEC first. Then set viewers to the default coordinate system. :param data: input data """ is_ra_dec = isinstance(wcs_to_celestial_frame(data.coords.wcs), BaseRADecFrame) self.ra_dec_format_menu.setDisabled(not is_ra_dec) if not is_ra_dec: return is_coords_in_degrees = False for view in self.cube_views: viewer = view.widget() viewer.init_ra_dec() is_coords_in_degrees = viewer._coords_in_degrees if is_coords_in_degrees: format_name = "Decimal Degrees" else: format_name = "Sexagesimal" menu = self.ra_dec_format_menu for action in menu.actions(): if format_name == action.text(): action.setChecked(True) break
def frame(self): if self.region is None: return "icrs" try: return self.region.center.frame.name except AttributeError: return wcs_to_celestial_frame(self.wcs).name
def convert_world_coordinates(lon_in, lat_in, wcs_in, wcs_out): """ Convert longitude/latitude coordinates from an input frame to an output frame. Parameters ---------- lon_in, lat_in : `~numpy.ndarray` The longitude and latitude to convert wcs_in, wcs_out : tuple or `~astropy.wcs.WCS` The input and output frames, which can be passed either as a tuple of ``(frame, lon_unit, lat_unit)`` or as a `~astropy.wcs.WCS` instance. Returns ------- lon_out, lat_out : `~numpy.ndarray` The output longitude and latitude """ if isinstance(wcs_in, WCS): # Extract the celestial component of the WCS in (lon, lat) order wcs_in = wcs_in.celestial frame_in = wcs_to_celestial_frame(wcs_in) lon_in_unit = u.Unit(wcs_in.wcs.cunit[0]) lat_in_unit = u.Unit(wcs_in.wcs.cunit[1]) else: frame_in, lon_in_unit, lat_in_unit = wcs_in if isinstance(wcs_out, WCS): # Extract the celestial component of the WCS in (lon, lat) order wcs_out = wcs_out.celestial frame_out = wcs_to_celestial_frame(wcs_out) lon_out_unit = u.Unit(wcs_out.wcs.cunit[0]) lat_out_unit = u.Unit(wcs_out.wcs.cunit[1]) else: frame_out, lon_out_unit, lat_out_unit = wcs_out data = UnitSphericalRepresentation(lon_in * lon_in_unit, lat_in * lat_in_unit) coords_in = frame_in.realize_frame(data) coords_out = coords_in.transform_to(frame_out) lon_out = coords_out.represent_as("unitspherical").lon.to(lon_out_unit).value lat_out = coords_out.represent_as("unitspherical").lat.to(lat_out_unit).value return lon_out, lat_out
def xy(self): hpc_frame = wcs_to_celestial_frame(self.wcs) skycoord = SkyCoord(self.sky_coord, frame=hpc_frame, unit=(u.hourangle, u.deg)) ircs = np.array([[skycoord.ra.deg, skycoord.dec.deg]]) coords = np.array(self.wcs.wcs_world2pix(ircs,1)) y = int((coords[0])[0]) x = int((coords[0])[1]) return(x,y)
def __init__(self, wcs, slice=None): super().__init__() self.wcs = wcs self.slice = slice if self.slice is not None: self.x_index = slice.index('x') self.y_index = slice.index('y') if wcs.has_celestial: self.frame_out = wcs_to_celestial_frame(wcs)
def frame(self): """Coordinate system, either Galactic ("galactic") or Equatorial ("icrs").""" if self.region is None: return "icrs" try: return self.region.center.frame.name except AttributeError: return wcs_to_celestial_frame(self.wcs).name
def skycoord(hdulist, a): hdr = hdulist[a].header wcs = WCS(hdr) hpc_frame = wcs_to_celestial_frame(wcs) skycoord = SkyCoord(sky_coord, frame=hpc_frame, unit=(u.hourangle, u.deg)) ircs = np.array([[skycoord.ra.deg, skycoord.dec.deg]]) coords = np.array(wcs.wcs_world2pix(ircs, 1)) y = int((coords[0])[0]) x = int((coords[0])[1]) return (x, y, wcs)
def __init__(self, input_system, output_system): super().__init__() self._input_system_name = input_system self._output_system_name = output_system if isinstance(self._input_system_name, WCS): self.input_system = wcs_to_celestial_frame(self._input_system_name) elif isinstance(self._input_system_name, str): self.input_system = frame_transform_graph.lookup_name( self._input_system_name) if self.input_system is None: raise ValueError("Frame {0} not found".format( self._input_system_name)) elif isinstance(self._input_system_name, BaseCoordinateFrame): self.input_system = self._input_system_name else: raise TypeError( "input_system should be a WCS instance, string, or a coordinate frame instance" ) if isinstance(self._output_system_name, WCS): self.output_system = wcs_to_celestial_frame( self._output_system_name) elif isinstance(self._output_system_name, str): self.output_system = frame_transform_graph.lookup_name( self._output_system_name) if self.output_system is None: raise ValueError("Frame {0} not found".format( self._output_system_name)) elif isinstance(self._output_system_name, BaseCoordinateFrame): self.output_system = self._output_system_name else: raise TypeError( "output_system should be a WCS instance, string, or a coordinate frame instance" ) if self.output_system == self.input_system: self.same_frames = True else: self.same_frames = False
def skycoord_to_pixel(coords, wcs): """ Convert a set of SkyCoord coordinates into pixel coordinates. This function assumes that the coordinates are celestial. Parameters ---------- coords : `~astropy.coordinates.SkyCoord` The coordinates to convert wcs : `~astropy.wcs.WCS` The WCS transformation to use Returns ------- x, y : `~numpy.ndarray` The x and y pixel coordinates corresponding to the input coordinates """ # TODO this should be simplified once wcs_world2pix() supports # SkyCoord objects as input # TODO: remove local wcs_to_celestial_frame once Astropy 1.0 is out try: from astropy.wcs.utils import wcs_to_celestial_frame except ImportError: # Astropy < 1.0 from .extern.wcs_utils import wcs_to_celestial_frame # Keep only the celestial part of the axes, also re-orders lon/lat wcs = wcs.sub([WCSSUB_CELESTIAL]) if wcs.naxis != 2: raise ValueError("WCS should contain celestial component") # Check which frame the WCS uses frame = wcs_to_celestial_frame(wcs) # Check what unit the WCS needs xw_unit = u.Unit(wcs.wcs.cunit[0]) yw_unit = u.Unit(wcs.wcs.cunit[1]) # Convert positions to frame coords = coords.transform_to(frame) # Extract longitude and latitude lon = coords.spherical.lon.to(xw_unit) lat = coords.spherical.lat.to(yw_unit) # Convert to pixel coordinates xp, yp = wcs.wcs_world2pix(lon, lat, 0) return xp, yp
def test_wcs_to_celestial_frame_extend(): mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['XOFFSET', 'YOFFSET'] mywcs.wcs.set() with pytest.raises(ValueError): wcs_to_celestial_frame(mywcs) class OffsetFrame: pass def identify_offset(wcs): if wcs.wcs.ctype[0].endswith('OFFSET') and wcs.wcs.ctype[1].endswith('OFFSET'): return OffsetFrame() with custom_wcs_to_frame_mappings(identify_offset): frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, OffsetFrame) # Check that things are back to normal after the context manager with pytest.raises(ValueError): wcs_to_celestial_frame(mywcs)
def load_header(self, header, fobj=None): from astropy.wcs.utils import wcs_to_celestial_frame # reconstruct a pyfits header, because otherwise we take an # incredible performance hit in astropy.wcs self.header = pyfits.Header(header.items()) try: self.logger.debug("Trying to make astropy wcs object") self.wcs = pywcs.WCS(self.header, fobj=fobj, relax=True) self.coordframe = wcs_to_celestial_frame(self.wcs) except Exception as e: self.logger.error("Error making WCS object: %s" % (str(e))) self.wcs = None
def test_wcs_to_celestial_frame_correlated(): # Regression test for a bug that caused wcs_to_celestial_frame to fail when # the celestial axes were correlated with other axes. # Import astropy.coordinates here to avoid circular imports from astropy.coordinates.builtin_frames import ICRS mywcs = WCS(naxis=3) mywcs.wcs.ctype = 'RA---TAN', 'DEC--TAN', 'FREQ' mywcs.wcs.cd = np.ones((3, 3)) mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, ICRS)
def convert_world_coordinates(xw_in, yw_in, wcs_in, wcs_out): """ Convert world coordinates from an input frame to an output frame. Parameters ---------- xw_in, yw_in : `~numpy.ndarray` The input coordinates to convert wcs_in, wcs_out : tuple or `~astropy.wcs.WCS` The input and output frames, which can be passed either as a tuple of ``(frame, x_unit, y_unit)`` or as a `~astropy.wcs.WCS` instance. """ if isinstance(wcs_in, WCS): frame_in = wcs_to_celestial_frame(wcs_in) xw_in_unit = u.Unit(wcs_in.wcs.cunit[0]) yw_in_unit = u.Unit(wcs_in.wcs.cunit[1]) else: frame_in, xw_in_unit, yw_in_unit = wcs_in if isinstance(wcs_out, WCS): frame_out = wcs_to_celestial_frame(wcs_out) xw_out_unit = u.Unit(wcs_out.wcs.cunit[0]) yw_out_unit = u.Unit(wcs_out.wcs.cunit[1]) else: frame_out, xw_out_unit, yw_out_unit = wcs_out data = UnitSphericalRepresentation(xw_in * xw_in_unit, yw_in * yw_in_unit) coords_in = frame_in.realize_frame(data) coords_out = coords_in.transform_to(frame_out) xw_out = coords_out.spherical.lon.to(xw_out_unit).value yw_out = coords_out.spherical.lat.to(yw_out_unit).value return xw_out, yw_out
def _to_pixel_params(self, wcs, mode='all'): """ Convert the sky aperture parameters to those for a pixel aperture. Parameters ---------- wcs : `~astropy.wcs.WCS` The world coordinate system (WCS) transformation to use. mode : {'all', 'wcs'}, optional Whether to do the transformation including distortions (``'all'``; default) or only including only the core WCS transformation (``'wcs'``). Returns ------- pixel_params : `dict` A dictionary of parameters for an equivalent pixel aperture. """ pixel_params = {} xpos, ypos = skycoord_to_pixel(self.positions, wcs, mode=mode) pixel_params['positions'] = np.array([xpos, ypos]).transpose() # The aperture object must have a single value for each shape # parameter so we must use a single pixel scale for all positions. # Here, we define the scale at the WCS CRVAL position. crval = SkyCoord(*wcs.wcs.crval, frame=wcs_to_celestial_frame(wcs), unit=wcs.wcs.cunit) pixscale, angle = _pixel_scale_angle_at_skycoord(crval, wcs) shape_params = list(self._shape_params) theta_key = 'theta' if theta_key in shape_params: pixel_params[theta_key] = (self.theta + angle).to(u.radian).value shape_params.remove(theta_key) for shape_param in shape_params: value = getattr(self, shape_param) if value.unit.physical_type == 'angle': pixel_params[shape_param] = ((value / pixscale).to( u.pixel).value) else: pixel_params[shape_param] = value.value return pixel_params
def load_header(self, header, fobj=None): from astropy.wcs.utils import wcs_to_celestial_frame self.header = {} self.header.update(header.items()) self.fix_bad_headers() try: self.logger.debug("Trying to make astropy wcs object") self.wcs = pywcs.WCS(self.header, fobj=fobj, relax=True) self.coordframe = wcs_to_celestial_frame(self.wcs) except Exception as e: self.logger.error("Error making WCS object: %s" % (str(e))) self.wcs = None
def _to_pixel_params(self, wcs): """ Convert the sky aperture parameters to those for a pixel aperture. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS <https://docs.astropy.org/en/stable/wcs/wcsapi.html>`_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- pixel_params : `dict` A dictionary of parameters for an equivalent pixel aperture. """ pixel_params = {} xpos, ypos = _world_to_pixel(self.positions, wcs) pixel_params['positions'] = np.array([xpos, ypos]).transpose() # The aperture object must have a single value for each shape # parameter so we must use a single pixel scale for all positions. # Here, we define the scale at the WCS CRVAL position. crval = SkyCoord(*wcs.wcs.crval, frame=wcs_to_celestial_frame(wcs), unit=wcs.wcs.cunit) pixscale, angle = _pixel_scale_angle_at_skycoord(crval, wcs) shape_params = list(self._shape_params) theta_key = 'theta' if theta_key in shape_params: pixel_params[theta_key] = (self.theta + angle).to(u.radian).value shape_params.remove(theta_key) for shape_param in shape_params: value = getattr(self, shape_param) if value.unit.physical_type == 'angle': pixel_params[shape_param] = ((value / pixscale).to( u.pixel).value) else: pixel_params[shape_param] = value.value return pixel_params
def _to_pixel_params(self, wcs, mode='all'): """ Convert the sky aperture parameters to those for a pixel aperture. Parameters ---------- wcs : `~astropy.wcs.WCS` The world coordinate system (WCS) transformation to use. mode : {'all', 'wcs'}, optional Whether to do the transformation including distortions (``'all'``; default) or only including only the core WCS transformation (``'wcs'``). Returns ------- pixel_params : dict A dictionary of parameters for an equivalent pixel aperture. """ pixel_params = {} x, y = skycoord_to_pixel(self.positions, wcs, mode=mode) pixel_params['positions'] = np.array([x, y]).transpose() # The aperture object must have a single value for each shape # parameter so we must use a single pixel scale for all positions. # Here, we define the scale at the WCS CRVAL position. crval = SkyCoord([wcs.wcs.crval], frame=wcs_to_celestial_frame(wcs), unit=wcs.wcs.cunit) scale, angle = pixel_scale_angle_at_skycoord(crval, wcs) params = self._params[:] theta_key = 'theta' if theta_key in self._params: pixel_params[theta_key] = (self.theta + angle).to(u.radian).value params.remove(theta_key) param_vals = [getattr(self, param) for param in params] if param_vals[0].unit.physical_type == 'angle': for param, param_val in zip(params, param_vals): pixel_params[param] = (param_val / scale).to(u.pixel).value else: # pixels for param, param_val in zip(params, param_vals): pixel_params[param] = param_val.value return pixel_params
def _to_sky_params(self, wcs, mode='all'): """ Convert the pixel aperture parameters to those for a sky aperture. Parameters ---------- wcs : `~astropy.wcs.WCS` The world coordinate system (WCS) transformation to use. mode : {'all', 'wcs'}, optional Whether to do the transformation including distortions (``'all'``; default) or only including only the core WCS transformation (``'wcs'``). Returns ------- sky_params : `dict` A dictionary of parameters for an equivalent sky aperture. """ sky_params = {} xpos, ypos = np.transpose(self.positions) sky_params['positions'] = pixel_to_skycoord(xpos, ypos, wcs, mode=mode) # The aperture object must have a single value for each shape # parameter so we must use a single pixel scale for all positions. # Here, we define the scale at the WCS CRVAL position. crval = SkyCoord(*wcs.wcs.crval, frame=wcs_to_celestial_frame(wcs), unit=wcs.wcs.cunit) pixscale, angle = _pixel_scale_angle_at_skycoord(crval, wcs) shape_params = list(self._shape_params) theta_key = 'theta' if theta_key in shape_params: sky_params[theta_key] = (self.theta * u.rad) - angle.to(u.rad) shape_params.remove(theta_key) for shape_param in shape_params: value = getattr(self, shape_param) sky_params[shape_param] = (value * u.pix * pixscale).to(u.arcsec) return sky_params
def _to_sky_params(self, wcs, mode='all'): """ Convert the pixel aperture parameters to those for a sky aperture. Parameters ---------- wcs : `~astropy.wcs.WCS` The world coordinate system (WCS) transformation to use. mode : {'all', 'wcs'}, optional Whether to do the transformation including distortions (``'all'``; default) or only including only the core WCS transformation (``'wcs'``). Returns ------- sky_params : dict A dictionary of parameters for an equivalent sky aperture. """ sky_params = {} x, y = np.transpose(self.positions) sky_params['positions'] = pixel_to_skycoord(x, y, wcs, mode=mode) # The aperture object must have a single value for each shape # parameter so we must use a single pixel scale for all positions. # Here, we define the scale at the WCS CRVAL position. crval = SkyCoord([wcs.wcs.crval], frame=wcs_to_celestial_frame(wcs), unit=wcs.wcs.cunit) scale, angle = pixel_scale_angle_at_skycoord(crval, wcs) params = self._params[:] theta_key = 'theta' if theta_key in self._params: sky_params[theta_key] = (self.theta * u.rad) - angle.to(u.rad) params.remove(theta_key) param_vals = [getattr(self, param) for param in params] for param, param_val in zip(params, param_vals): sky_params[param] = (param_val * u.pix * scale).to(u.arcsec) return sky_params
def _to_sky_params(self, wcs): """ Convert the pixel aperture parameters to those for a sky aperture. Parameters ---------- wcs : WCS object A world coordinate system (WCS) transformation that supports the `astropy shared interface for WCS <https://docs.astropy.org/en/stable/wcs/wcsapi.html>`_ (e.g., `astropy.wcs.WCS`, `gwcs.wcs.WCS`). Returns ------- sky_params : `dict` A dictionary of parameters for an equivalent sky aperture. """ sky_params = {} xpos, ypos = np.transpose(self.positions) sky_params['positions'] = _pixel_to_world(xpos, ypos, wcs) # The aperture object must have a single value for each shape # parameter so we must use a single pixel scale for all positions. # Here, we define the scale at the WCS CRVAL position. crval = SkyCoord(*wcs.wcs.crval, frame=wcs_to_celestial_frame(wcs), unit=wcs.wcs.cunit) pixscale, angle = _pixel_scale_angle_at_skycoord(crval, wcs) shape_params = list(self._shape_params) theta_key = 'theta' if theta_key in shape_params: sky_params[theta_key] = (self.theta * u.rad) - angle.to(u.rad) shape_params.remove(theta_key) for shape_param in shape_params: value = getattr(self, shape_param) sky_params[shape_param] = (value * u.pix * pixscale).to(u.arcsec) return sky_params
def __init__(self, wcs, slice=None): super().__init__() self.wcs = wcs if self.wcs.wcs.naxis > 2: if slice is None: raise ValueError( "WCS has more than 2 dimensions, so ``slice`` should be set" ) elif len(slice) != self.wcs.wcs.naxis: raise ValueError("slice should have as many elements as WCS " "has dimensions (should be {})".format( self.wcs.wcs.naxis)) else: self.slice = slice self.x_index = slice.index('x') self.y_index = slice.index('y') else: self.slice = None if wcs.has_celestial: self.frame_in = wcs_to_celestial_frame(wcs)
def _convert_world_coordinates(lon_in, lat_in, wcs_in, wcs_out): frame_in, lon_in_unit, lat_in_unit = wcs_in wcs_out = wcs_out.celestial frame_out = wcs_to_celestial_frame(wcs_out) lon_out_unit = u.Unit(wcs_out.wcs.cunit[0]) lat_out_unit = u.Unit(wcs_out.wcs.cunit[1]) data = UnitSphericalRepresentation(lon_in * lon_in_unit, lat_in * lat_in_unit) coords_in = frame_in.realize_frame(data) coords_out = coords_in.transform_to(frame_out) lon_out = coords_out.represent_as('unitspherical').lon.to( lon_out_unit).value lat_out = coords_out.represent_as('unitspherical').lat.to( lat_out_unit).value return lon_out, lat_out
def toggle_coords_in_degrees(self): """ Switch coords_in_degrees state """ data = self.state.layers_data[0] is_ra_dec = isinstance(wcs_to_celestial_frame(data.coords.wcs), BaseRADecFrame) if self._coords_in_degrees: self._coords_in_degrees = False self._coords_format_function = self._format_to_hex_string if is_ra_dec: self.axes.coords[0].set_major_formatter('hh:mm:ss.s') self.axes.coords[1].set_major_formatter('dd:mm:ss') self.figure.canvas.draw() else: self._coords_in_degrees = True self._coords_format_function = self._format_to_degree_string if is_ra_dec: self.axes.coords[0].set_major_formatter('d.dddd') self.axes.coords[1].set_major_formatter('d.dddd') self.figure.canvas.draw()
def load_header(self, header, fobj=None): from astropy.wcs.utils import wcs_to_celestial_frame try: # reconstruct a pyfits header, because otherwise we take an # incredible performance hit in astropy.wcs self.header = pyfits.Header(header.items()) self.logger.debug("Trying to make astropy wcs object") self.wcs = pywcs.WCS(self.header, fobj=fobj, relax=True) try: self.coordframe = wcs_to_celestial_frame(self.wcs) except ValueError: sysname = get_coord_system_name(self.header) if sysname in ('raw', 'pixel'): self.coordframe = sysname else: raise except Exception as e: self.logger.error("Error making WCS object: %s" % (str(e))) self.wcs = None
def test_wcs_to_celestial_frame(): # Import astropy.coordinates here to avoid circular imports from astropy.coordinates.builtin_frames import ICRS, ITRS, FK5, FK4, Galactic mywcs = WCS(naxis=2) mywcs.wcs.set() with pytest.raises(ValueError, match="Could not determine celestial frame " "corresponding to the specified WCS object"): assert wcs_to_celestial_frame(mywcs) is None mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['XOFFSET', 'YOFFSET'] mywcs.wcs.set() with pytest.raises(ValueError): assert wcs_to_celestial_frame(mywcs) is None mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, ICRS) mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] mywcs.wcs.equinox = 1987. mywcs.wcs.set() print(mywcs.to_header()) frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, FK5) assert frame.equinox == Time(1987., format='jyear') mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] mywcs.wcs.equinox = 1982 mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, FK4) assert frame.equinox == Time(1982., format='byear') mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['GLON-SIN', 'GLAT-SIN'] mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, Galactic) mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['TLON-CAR', 'TLAT-CAR'] mywcs.wcs.dateobs = '2017-08-17T12:41:04.430' mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, ITRS) assert frame.obstime == Time('2017-08-17T12:41:04.430') for equinox in [np.nan, 1987, 1982]: mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] mywcs.wcs.radesys = 'ICRS' mywcs.wcs.equinox = equinox mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, ICRS) # Flipped order mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['DEC--TAN', 'RA---TAN'] mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, ICRS) # More than two dimensions mywcs = WCS(naxis=3) mywcs.wcs.ctype = ['DEC--TAN', 'VELOCITY', 'RA---TAN'] mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, ICRS) mywcs = WCS(naxis=3) mywcs.wcs.ctype = ['GLAT-CAR', 'VELOCITY', 'GLON-CAR'] mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, Galactic)
def find_optimal_celestial_wcs(input_data, frame=None, auto_rotate=False, projection='TAN', resolution=None, reference=None): """ Given one or more images, return an optimal WCS projection object and shape. This currently only works with 2-d images with celestial WCS. Parameters ---------- input_data : iterable One or more input data specifications to include in the calculation of the final WCS. This should be an iterable containing one entry for each specification, where a single data specification is one of: * The name of a FITS file * An `~astropy.io.fits.HDUList` object * An image HDU object such as a `~astropy.io.fits.PrimaryHDU`, `~astropy.io.fits.ImageHDU`, or `~astropy.io.fits.CompImageHDU` instance * A tuple where the first element is an Numpy array shape tuple the second element is either a `~astropy.wcs.WCS` or a `~astropy.io.fits.Header` object * A tuple where the first element is a `~numpy.ndarray` and the second element is either a `~astropy.wcs.WCS` or a `~astropy.io.fits.Header` object frame : str or `~astropy.coordinates.BaseCoordinateFrame` The coordinate system for the final image (defaults to the frame of the first image specified) auto_rotate : bool Whether to rotate the header to minimize the final image area (if `True`, requires shapely>=1.6 to be installed) projection : str Three-letter code for the WCS projection resolution : `~astropy.units.Quantity` The resolution of the final image. If not specified, this is the smallest resolution of the input images. reference : `~astropy.coordinates.SkyCoord` The reference coordinate for the final header. If not specified, this is determined automatically from the input images. Returns ------- wcs : :class:`~astropy.wcs.WCS` The optimal WCS determined from the input images. shape : tuple The optimal shape required to cover all the output. """ # TODO: support higher-dimensional datasets in future # TODO: take into account NaN values when determining the extent of the # final WCS if isinstance(frame, str): frame = frame_transform_graph.lookup_name(frame)() input_shapes = [parse_input_shape(shape) for shape in input_data] # We start off by looping over images, checking that they are indeed # celestial images, and building up a list of all corners and all reference # coordinates in celestial (ICRS) coordinates. corners = [] references = [] resolutions = [] for shape, wcs in input_shapes: if len(shape) != 2: raise ValueError( "Input data is not 2-dimensional (got shape {!r})".format( shape)) if wcs.naxis != 2: raise ValueError("Input WCS is not 2-dimensional") if not wcs.has_celestial: raise TypeError("WCS does not have celestial components") # Determine frame if it wasn't specified if frame is None: frame = wcs_to_celestial_frame(wcs) # Find pixel coordinates of corners. In future if we are worried about # significant distortions of the edges in the reprojection process we # could simply add arbitrary numbers of midpoints to this list. ny, nx = shape xc = np.array([-0.5, nx - 0.5, nx - 0.5, -0.5]) yc = np.array([-0.5, -0.5, ny - 0.5, ny - 0.5]) # We have to do .frame here to make sure that we get an ICRS object # without any 'hidden' attributes, otherwise the stacking below won't # work. TODO: check if we need to enable distortions here. corners.append(pixel_to_skycoord(xc, yc, wcs, origin=0).icrs.frame) # We now figure out the reference coordinate for the image in ICRS. The # easiest way to do this is actually to use pixel_to_skycoord with the # reference position in pixel coordinates. We have to set origin=1 # because crpix values are 1-based. xp, yp = wcs.wcs.crpix references.append(pixel_to_skycoord(xp, yp, wcs, origin=1).icrs.frame) # Find the pixel scale at the reference position - we take the minimum # since we are going to set up a header with 'square' pixels with the # smallest resolution specified. scales = proj_plane_pixel_scales(wcs) resolutions.append(np.min(np.abs(scales))) # We now stack the coordinates - however the ICRS class can't do this # so we have to use the high-level SkyCoord class. corners = SkyCoord(corners) references = SkyCoord(references) # If no reference coordinate has been passed in for the final header, we # determine the reference coordinate as the mean of all the reference # positions. This choice is as good as any and if the user really cares, # they can set it manually. if reference is None: reference = SkyCoord(references.data.mean(), frame=references.frame) # In any case, we need to convert the reference coordinate (either # specified or automatically determined) to the requested final frame. reference = reference.transform_to(frame) # Determine resolution if not specified if resolution is None: resolution = np.min(resolutions) * u.deg # Determine the resolution in degrees cdelt = resolution.to(u.deg).value # Construct WCS object centered on position wcs_final = celestial_frame_to_wcs(frame, projection=projection) rep = reference.represent_as('unitspherical') wcs_final.wcs.crval = rep.lon.degree, rep.lat.degree wcs_final.wcs.cdelt = -cdelt, cdelt # For now, set crpix to (1, 1) and we'll then figure out where all the # images fall in this projection, then we'll adjust crpix. wcs_final.wcs.crpix = (1, 1) # Find pixel coordinates of all corners in the final WCS projection. We use # origin=1 since we are trying to determine crpix values. xp, yp = skycoord_to_pixel(corners, wcs_final, origin=1) if auto_rotate: # Use shapely to represent the points and find the minimum rotated # rectangle from shapely.geometry import MultiPoint mp = MultiPoint(list(zip(xp, yp))) # The following returns a list of rectangle vertices - in fact there # are 5 coordinates because shapely represents it as a closed polygon # with the same first/last vertex. xr, yr = mp.minimum_rotated_rectangle.exterior.coords.xy xr, yr = xr[:4], yr[:4] # The order of the vertices is not guaranteed to be constant so we # take the vertices with the two smallest y values (which, for a # rectangle, guarantees that the vertices are neighboring) order = np.argsort(yr) x1, y1, x2, y2 = xr[order[0]], yr[order[0]], xr[order[1]], yr[order[1]] # Determine angle between two of the vertices. It doesn't matter which # ones they are, we just want to know how far from being straight the # rectangle is. angle = np.arctan2(y2 - y1, x2 - x1) # Determine the smallest angle that would cause the rectangle to be # lined up with the axes. angle = angle % (np.pi / 2) if angle > np.pi / 4: angle -= np.pi / 2 # Set rotation matrix (use PC instead of CROTA2 since PC is the # recommended approach) pc = np.array([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]]) wcs_final.wcs.pc = pc # Recompute pixel coordinates (more accurate than simply rotating xp, yp) xp, yp = skycoord_to_pixel(corners, wcs_final, origin=1) # Find the full range of values xmin = xp.min() xmax = xp.max() ymin = yp.min() ymax = yp.max() # Update crpix so that the lower range falls on the bottom and left. We add # 0.5 because in the final image the bottom left corner should be at (0.5, # 0.5) not (1, 1). wcs_final.wcs.crpix = (1 - xmin) + 0.5, (1 - ymin) + 0.5 # Return the final image shape too naxis1 = int(round(xmax - xmin)) naxis2 = int(round(ymax - ymin)) return wcs_final, (naxis2, naxis1)
def __init__(self, parent): self._ax = parent.ax self._wcs = parent.ax.wcs self.x = parent.x self.y = parent.y xcoord_type = self._ax.coords[self.x].coord_type ycoord_type = self._ax.coords[self.y].coord_type if xcoord_type == 'longitude' and ycoord_type == 'latitude': celestial = True inverted = False elif xcoord_type == 'latitude' and ycoord_type == 'longitude': celestial = True inverted = True else: celestial = inverted = False if celestial: frame = wcs_to_celestial_frame(self._wcs) else: frame = None if isinstance(frame, ICRS): xtext = 'RA (ICRS)' ytext = 'Dec (ICRS)' elif isinstance(frame, FK5): equinox = "{:g}".format(FK5.equinox.jyear) xtext = 'RA (J{0})'.format(equinox) ytext = 'Dec (J{0})'.format(equinox) elif isinstance(frame, FK4): equinox = "{:g}".format(FK4.equinox.byear) xtext = 'RA (B{0})'.format(equinox) ytext = 'Dec (B{0})'.format(equinox) elif isinstance(frame, Galactic): xtext = 'Galactic Longitude' ytext = 'Galactic Latitude' elif isinstance(frame, (HeliocentricTrueEcliptic, BarycentricTrueEcliptic)): # NOTE: once we support only Astropy 2.0+, we can use BaseEclipticFrame xtext = 'Ecliptic Longitude' ytext = 'Ecliptic Latitude' else: cunit_x = self._wcs.wcs.cunit[self.x] cunit_y = self._wcs.wcs.cunit[self.y] cname_x = self._wcs.wcs.cname[self.x] cname_y = self._wcs.wcs.cname[self.y] ctype_x = self._wcs.wcs.ctype[self.x] ctype_y = self._wcs.wcs.ctype[self.y] xunit = " (%s)" % cunit_x if cunit_x not in ["", None] else "" yunit = " (%s)" % cunit_y if cunit_y not in ["", None] else "" if len(cname_x) > 0: xtext = cname_x + xunit else: if len(ctype_x) == 8 and ctype_x[4] == '-': xtext = ctype_x[:4].replace('-', '') + xunit else: xtext = ctype_x + xunit if len(cname_y) > 0: ytext = cname_y + yunit else: if len(ctype_y) == 8 and ctype_y[4] == '-': ytext = ctype_y[:4].replace('-', '') + yunit else: ytext = ctype_y + yunit if inverted: xtext, ytext = ytext, xtext self.set_xtext(xtext) self.set_ytext(ytext) self.set_xposition('bottom') self.set_yposition('left')
def pixel_to_skycoord(xp, yp, wcs, origin=0, mode='all'): """ Convert a set of pixel coordinates into a SkyCoord coordinate. Parameters ---------- xp, yp : float or `numpy.ndarray` The coordinates to convert. wcs : `~astropy.wcs.WCS` The WCS transformation to use. origin : int Whether to return 0 or 1-based pixel coordinates. mode : 'all' or 'wcs' Whether to do the transformation including distortions (``'all'``) or only including only the core WCS transformation (``'wcs'``). Returns ------- coords : `~astropy.coordinates.SkyCoord` The celestial coordinates """ # temporary workaround has_distortion = any(getattr(wcs, dist_attr) is not None for dist_attr in ['cpdis1', 'cpdis2', 'det2im1', 'det2im2', 'sip']) if has_distortion and wcs.naxis != 2: raise ValueError("Can only handle WCS with distortions for 2-dimensional WCS") # Keep only the celestial part of the axes, also re-orders lon/lat wcs = wcs.sub([WCSSUB_CELESTIAL]) if wcs.naxis != 2: raise ValueError("WCS should contain celestial component") # TODO: remove local wcs_to_celestial_frame once Astropy 1.0 is out try: from astropy.wcs.utils import wcs_to_celestial_frame except ImportError: # Astropy < 1.0 from .extern.wcs_utils import wcs_to_celestial_frame # Check which frame the WCS uses frame = wcs_to_celestial_frame(wcs) # Check what unit the WCS gives lon_unit = u.Unit(wcs.wcs.cunit[0]) lat_unit = u.Unit(wcs.wcs.cunit[1]) # Convert pixel coordinates to celestial coordinates if mode == 'all': lon, lat = wcs.all_pix2world(xp, yp, origin) elif mode == 'wcs': lon, lat = wcs.wcs_pix2world(xp, yp, origin) else: raise ValueError("mode should be either 'all' or 'wcs'") # Add units to longitude/latitude lon = lon * lon_unit lat = lat * lat_unit # Create SkyCoord object data = UnitSphericalRepresentation(lon=lon, lat=lat) coords = SkyCoord(frame.realize_frame(data)) return coords
def test_wcs_to_celestial_frame(): # Import astropy.coordinates here to avoid circular imports from astropy.coordinates.builtin_frames import ICRS, ITRS, FK5, FK4, Galactic mywcs = WCS(naxis=2) mywcs.wcs.set() with pytest.raises(ValueError) as exc: assert wcs_to_celestial_frame(mywcs) is None assert exc.value.args[0] == "Could not determine celestial frame corresponding to the specified WCS object" mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['XOFFSET', 'YOFFSET'] mywcs.wcs.set() with pytest.raises(ValueError): assert wcs_to_celestial_frame(mywcs) is None mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, ICRS) mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] mywcs.wcs.equinox = 1987. mywcs.wcs.set() print(mywcs.to_header()) frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, FK5) assert frame.equinox == Time(1987., format='jyear') mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] mywcs.wcs.equinox = 1982 mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, FK4) assert frame.equinox == Time(1982., format='byear') mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['GLON-SIN', 'GLAT-SIN'] mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, Galactic) mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['TLON-CAR', 'TLAT-CAR'] mywcs.wcs.dateobs = '2017-08-17T12:41:04.430' mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, ITRS) assert frame.obstime == Time('2017-08-17T12:41:04.430') for equinox in [np.nan, 1987, 1982]: mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] mywcs.wcs.radesys = 'ICRS' mywcs.wcs.equinox = equinox mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, ICRS) # Flipped order mywcs = WCS(naxis=2) mywcs.wcs.ctype = ['DEC--TAN', 'RA---TAN'] mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, ICRS) # More than two dimensions mywcs = WCS(naxis=3) mywcs.wcs.ctype = ['DEC--TAN', 'VELOCITY', 'RA---TAN'] mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, ICRS) mywcs = WCS(naxis=3) mywcs.wcs.ctype = ['GLAT-CAR', 'VELOCITY', 'GLON-CAR'] mywcs.wcs.set() frame = wcs_to_celestial_frame(mywcs) assert isinstance(frame, Galactic)
def _get_components_and_classes(self): # The aim of this function is to return whatever is needed for # world_axis_object_components and world_axis_object_classes. It's easier # to figure it out in one go and then return the values and let the # properties return part of it. # Since this method might get called quite a few times, we need to cache # it. We start off by defining a hash based on the attributes of the # WCS that matter here (we can't just use the WCS object as a hash since # it is mutable) wcs_hash = (self.naxis, list(self.wcs.ctype), list(self.wcs.cunit), self.wcs.radesys, self.wcs.equinox, self.wcs.dateobs, self.wcs.lng, self.wcs.lat) # If the cache is present, we need to check that the 'hash' matches. if getattr(self, '_components_and_classes_cache', None) is not None: cache = self._components_and_classes_cache if cache[0] == wcs_hash: return cache[1] else: self._components_and_classes_cache = None # Avoid circular imports by importing here from astropy.wcs.utils import wcs_to_celestial_frame from astropy.coordinates import SkyCoord components = [None] * self.naxis classes = {} # Let's start off by checking whether the WCS has a pair of celestial # components if self.has_celestial: frame = wcs_to_celestial_frame(self) kwargs = {} kwargs['frame'] = frame kwargs['unit'] = u.deg classes['celestial'] = (SkyCoord, (), kwargs) components[self.wcs.lng] = ('celestial', 0, 'spherical.lon.degree') components[self.wcs.lat] = ('celestial', 1, 'spherical.lat.degree') # Fallback: for any remaining components that haven't been identified, just # return Quantity as the class to use if 'time' in self.world_axis_physical_types: warnings.warn('In future, times will be represented by the Time class ' 'instead of Quantity', FutureWarning) for i in range(self.naxis): if components[i] is None: name = self.axis_type_names[i].lower() if name == '': name = 'world' while name in classes: name += "_" classes[name] = (u.Quantity, (), {'unit': self.wcs.cunit[i]}) components[i] = (name, 0, 'value') # Keep a cached version of result self._components_and_classes_cache = wcs_hash, (components, classes) return components, classes
def aperture_photometry(data, positions, apertures, unit=None, wcs=None, error=None, gain=None, mask=None, method='exact', subpixels=5, pixelcoord=True, pixelwise_error=True, mask_method='skip'): """ Sum flux within an aperture at the given position(s). Parameters ---------- data : array_like, `~astropy.io.fits.ImageHDU`, `~astropy.io.fits.HDUList` The 2-d array on which to perform photometry. Units are used during the photometry, either provided along with the data array, or stored in the header keyword ``'BUNIT'``. positions : list, tuple, nd.array or `~astropy.coordinates.SkyCoord` Positions of the aperture centers, either in pixel or sky coordinates. If positions is `~astropy.coordinates.SkyCoord` or ``pixelcoord`` is `False` a wcs transformation is also needed to convert the input positions to pixel positions. If ``positions`` are sky positions but not an `~astropy.coordinates.SkyCoord` object, it need to be in the same celestial frame as the wcs transformation. apertures : tuple First element of the tuple is the mode, the currently supported ones are: ``'circular'``, ``'elliptical'``, ``'circular_annulus'``, ``'elliptical_annulus'``, ``'rectangular'``. The remaining (1 to 4) elements are the parameters for the given mode. Check the documentation of the relevant ``Aperture`` classes for more information. unit : `~astropy.units.UnitBase` instance, str An object that represents the unit associated with ``data``. Must be an `~astropy.units.UnitBase` object or a string parseable by the :mod:`~astropy.units` package. An error is raised if ``data`` already has a different unit. wcs : `~astropy.wcs.WCS`, optional Use this as the wcs transformation when either ``pixelcoord`` is `False` or ``positions`` is `~astropy.coordinates.SkyCoord`. It overrides any wcs transformation passed along with ``data`` either in the header or in an attribute. error : float or array_like, optional Error in each pixel, interpreted as Gaussian 1-sigma uncertainty. gain : float or array_like, optional Ratio of counts (e.g., electrons or photons) to units of the data (e.g., ADU), for the purpose of calculating Poisson error from the object itself. If ``gain`` is `None` (default), ``error`` is assumed to include all uncertainty in each pixel. If ``gain`` is given, ``error`` is assumed to be the "background error" only (not accounting for Poisson error in the flux in the apertures). mask : array_like (bool), optional Mask to apply to the data. method : str, optional Method to use for determining overlap between the aperture and pixels. Options include ['center', 'subpixel', 'exact'], but not all options are available for all types of apertures. More precise methods will generally be slower. * ``'center'`` A pixel is considered to be entirely in or out of the aperture depending on whether its center is in or out of the aperture. * ``'subpixel'`` A pixel is divided into subpixels and the center of each subpixel is tested (as above). With ``subpixels`` set to 1, this method is equivalent to 'center'. Note that for subpixel sampling, the input array is only resampled once for each object. * ``'exact'`` (default) The exact overlap between the aperture and each pixel is calculated. subpixels : int, optional For the ``'subpixel'`` method, resample pixels by this factor (in each dimension). That is, each pixel is divided into ``subpixels ** 2`` subpixels. pixelcoord : bool, optional If `True` (default), assume ``positions`` are pixel positions. If `False`, assume the input positions are sky coordinates and uses the wcs transformation (provided either via ``wcs`` or along with ``data``) to convert them to pixel positions. pixelwise_error : bool, optional For error and/or gain arrays. If `True`, assume error and/or gain vary significantly within an aperture: sum contribution from each pixel. If `False`, assume error and gain do not vary significantly within an aperture. Use the single value of error and/or gain at the center of each aperture as the value for the entire aperture. Default is `True`. mask_method : str, optional Method to treat masked pixels. Currently supported methods: * ``'skip'`` Leave out the masked pixels from all calculations. * ``'interpolation'`` The value of the masked pixels are replaced by the mean value of the neighbouring non-masked pixels. Returns ------- phot_table : `~astropy.table.Table` A table of the photometry with the following columns: * ``'aperture_sum'``: Sum of the values within the aperture. * ``'aperture_sum_err'``: Corresponding uncertainty in ``'aperture_sum'`` values. Returned only if input ``error`` is not `None`. * ``'pixel_center'``: pixel coordinate pairs of the center of the apertures. Unit is pixel. * ``'input_center'``: input coordinate pairs as they were given in the ``positions`` parameter. The metadata of the table stores the version numbers of both astropy and photutils, as well as the calling arguments. aux_dict : dict Auxilary dictionary storing all the auxilary information available. The element are the following: * ``'apertures'`` The `~photutils.Aperture` object containing the apertures to use during the photometry. """ dataunit = None datamask = None wcs_transformation = wcs if isinstance(data, (fits.PrimaryHDU, fits.ImageHDU)): header = data.header data = data.data if 'BUNIT' in header: dataunit = header['BUNIT'] # TODO check how a mask can be stored in the header, it seems like # full pixel masks are not supported by the FITS standard, look for # real life examples (e.g. header value stores the fits number of # fits extension where the pixelmask is stored?) if 'MASK' in header: datamask = header.mask elif isinstance(data, fits.HDUList): # TODO: do it in a 2d array, and thus get the light curves as a # side-product? Although it's not usual to store time series as # HDUList for i in range(len(data)): if data[i].data is not None: warnings.warn("Input data is a HDUList object, photometry is " "only run for the {0}. HDU." .format(i), AstropyUserWarning) return aperture_photometry(data[i], positions, apertures, unit, wcs, error, gain, mask, method, subpixels, pixelcoord, pixelwise_error, mask_method) # this is basically for NDData inputs and alike elif hasattr(data, 'data') and not isinstance(data, np.ndarray): if data.wcs is not None and wcs_transformation is None: wcs_transformation = data.wcs datamask = data.mask if hasattr(data, 'unit'): dataunit = data.unit if unit is not None and dataunit is not None: if unit != dataunit: raise u.UnitsError('Unit of input data ({0}) and unit given by ' 'unit argument ({1}) are not identical.'. format(dataunit, unit)) data = u.Quantity(data, unit=dataunit, copy=False) elif unit is None: if dataunit is not None: data = u.Quantity(data, unit=dataunit, copy=False) else: data = u.Quantity(data, copy=False) else: data = u.Quantity(data, unit=unit, copy=False) if datamask is None: data.mask = datamask # Check input array type and dimension. if np.iscomplexobj(data): raise TypeError('Complex type not supported') if data.ndim != 2: raise ValueError('{0}-d array not supported. ' 'Only 2-d arrays supported.'.format(data.ndim)) # Deal with the mask if it exist if mask is not None or datamask is not None: if mask is None: mask = datamask else: mask = np.asarray(mask) if np.iscomplexobj(mask): raise TypeError('Complex type not supported') if mask.ndim != 2: raise ValueError('{0}-d array not supported. ' 'Only 2-d arrays supported.' .format(mask.ndim)) if mask.shape != data.shape: raise ValueError('Shapes of mask array and data array ' 'must match') if datamask is not None: mask *= datamask if mask_method == 'skip': data *= ~mask if mask_method == 'interpolation': for i, j in zip(*np.nonzero(mask)): y0, y1 = max(i - 1, 0), min(i + 2, data.shape[0]) x0, x1 = max(j - 1, 0), min(j + 2, data.shape[1]) data[i, j] = np.mean(data[y0:y1, x0:x1][~mask[y0:y1, x0:x1]]) # Check whether we really need to calculate pixelwise errors, even if # requested. (If neither error nor gain is an array, we don't need to.) if ((error is None) or (np.isscalar(error) and gain is None) or (np.isscalar(error) and np.isscalar(gain))): pixelwise_error = False # Check error shape. if error is not None: if isinstance(error, u.Quantity): if np.isscalar(error.value): error = u.Quantity(np.broadcast_arrays(error, data), unit=error.unit)[0] elif np.isscalar(error): error = u.Quantity(np.broadcast_arrays(error, data), unit=data.unit)[0] else: error = u.Quantity(error, unit=data.unit, copy=False) if error.shape != data.shape: raise ValueError('shapes of error array and data array must' ' match') # Check gain shape. if gain is not None: # Gain doesn't do anything without error set, so raise an exception. # (TODO: instead, should we just set gain = None and ignore it?) if error is None: raise ValueError('gain requires error') if isinstance(gain, u.Quantity): if np.isscalar(gain.value): gain = u.Quantity(np.broadcast_arrays(gain, data), unit=gain.unit)[0] elif np.isscalar(gain): gain = np.broadcast_arrays(gain, data)[0] if gain.shape != data.shape: raise ValueError('shapes of gain array and data array must match') # Check that 'subpixels' is an int and is 1 or greater. if method == 'subpixel': subpixels = int(subpixels) if subpixels < 1: raise ValueError('subpixels: an integer greater than 0 is ' 'required') if not pixelcoord or isinstance(positions, SkyCoord): from astropy.wcs import wcs try: from astropy.wcs.utils import wcs_to_celestial_frame except ImportError: # Astropy < 1.0 from .extern.wcs_utils import wcs_to_celestial_frame if wcs_transformation is None: wcs_transformation = wcs.WCS(header) # TODO this should be simplified once wcs_world2pix() supports # SkyCoord objects as input if isinstance(positions, SkyCoord): # Check which frame the wcs uses framename = wcs_to_celestial_frame(wcs_transformation).name frame = getattr(positions, framename) component_names = list(frame.representation_component_names.keys())[0:2] if len(positions.shape) > 0: positions_repr = u.Quantity(zip(getattr(frame, component_names[0]).deg, getattr(frame, component_names[1]).deg), unit=u.deg) else: positions_repr = (u.Quantity((getattr(frame, component_names[0]).deg, getattr(frame, component_names[1]).deg), unit=u.deg), ) elif not isinstance(positions, u.Quantity): # TODO figure out the unit of the input positions for this case # TODO revise this once wcs_world2pix() accepts more input formats if len(positions) > 1 and not isinstance(positions, tuple): positions_repr = u.Quantity(positions, copy=False) else: positions_repr = (u.Quantity(positions, copy=False), ) pixelpositions = u.Quantity(wcs_transformation.wcs_world2pix (positions_repr, 0), unit=u.pixel, copy=False) else: positions = u.Quantity(positions, unit=u.pixel, copy=False) pixelpositions = positions if apertures[0] == 'circular': ap = CircularAperture(pixelpositions.value, apertures[1]) elif apertures[0] == 'circular_annulus': ap = CircularAnnulus(pixelpositions.value, *apertures[1:3]) elif apertures[0] == 'elliptical': ap = EllipticalAperture(pixelpositions.value, *apertures[1:4]) elif apertures[0] == 'elliptical_annulus': ap = EllipticalAnnulus(pixelpositions.value, *apertures[1:5]) elif apertures[0] == 'rectangular': if method == 'exact': warnings.warn("'exact' method is not implemented, defaults to " "'subpixel' instead", AstropyUserWarning) method = 'subpixel' ap = RectangularAperture(pixelpositions.value, *apertures[1:4]) # Prepare version return data from astropy import __version__ astropy_version = __version__ from photutils import __version__ photutils_version = __version__ photometry_result = ap.do_photometry(data, method=method, subpixels=subpixels, error=error, gain=gain, pixelwise_error=pixelwise_error) if error is None: phot_col_names = ('aperture_sum', ) else: phot_col_names = ('aperture_sum', 'aperture_sum_err') # Note: if wcs_transformation is None, 'pixel_center' will be the same # as 'input_center' # check whether single or multiple positions if len(pixelpositions) > 1 and pixelpositions[0].size >= 2: coord_columns = (pixelpositions, positions) else: coord_columns = ((pixelpositions,), (positions,)) coord_col_names = ('pixel_center', 'input_center') return (Table(data=(photometry_result + coord_columns), names=(phot_col_names + coord_col_names), meta={'name': 'Aperture photometry results', 'version': 'astropy: {0}, photutils: {1}' .format(astropy_version, photutils_version), 'calling_args': ('method={0}, subpixels={1}, ' 'error={2}, gain={3}, ' 'pixelwise_error={4}') .format(method, subpixels, error is not None, gain is not None, pixelwise_error)}), {'apertures': ap})
def maskFits(fitsfile, out='maskedimage.fits', img_hdu=None, mask_type=None, radius=180., radius2=None, angle=0., center=(0., 0.), extent=[-180., 180., -90., 90.], frame='galactic', unit='degree', clobber=False, float_min=1.17549e-38): """Mask the fits image Parameters ---------- fitsfile : str Path to the FITS file containing the the image which will be masked. out : str (Optional) Name of the output FITS file for which the mask wiil be applied img_hud : int or float (Optional) Name or integer for the FITS hdu containing the image. Default is 'Primary'. mask_type : str The geometry of the mask to be applied. Choices are 'radial' or 'square'. radius : float (Optional) Radius of the mask if mask_type is radial. Default is 180. radius2 : float (Optional) Second radius of the mask if the mask is not symmetric. Default is to use a symmetric mask. angle : float (Optional) Rotation angle of the ellipse. Default is 0. center : tuple (Optional) Center coordinates (C1, C2) of the radial mask. Default is (0., 0.). extent : list (Optional) [xmin, xmax, ymin, ymax] extent of the square mask. Default is [-180., 180., -90., 90.]. frame : str (Optional) Coordinate frame to use with mask coordinates. Choices are 'galactic', 'icrs', 'fk5', 'pixel'. Default is 'galactic'. unit : str (Optional) Units of coordinates. Default is 'degree'. clobber : bool Flag to overwrite a file of the same name if it exists. Default is False. float_min : float Minimum float value to use for pixels in the image after masking. gtobssim doesn't like pixel values <= 0. Default is 1.17549e-38. Returns ------- out : str or tuple If the input fits image is 2D the output is the full path to the masked fits image. If the input fits image is 3D the output is a tuple whose first entry is the full path to the masked fits image and whose second entry is the integrated flux of the masked fits image. """ if frame == 'galactic': frame_str = '(GLAT, GLON)' elif frame == 'icrs': frame_str = '(RA, DEC)' elif frame == 'fk5': frame_str = '(RAJ2000, DECJ2000)' elif frame == 'pixel': frame_str = '(PIX1, PIX2)' else: raise IOError("Invalid frame {0}".format(frame)) fitsfile = os.path.expandvars(fitsfile).replace( "$(FERMI_DIR)", os.environ.get("FERMI_DIR")) if os.environ.get( "FERMI_DIR") is not None else os.path.expandvars(fitsfile) hdu_list = pyfits.open(fitsfile) if img_hdu is None: # Assume the data image is in the primary hdu header = hdu_list['PRIMARY'].header img_hdu = 'PRIMARY' else: try: img_hdu = int(img_hdu) except ValueError: pass header = hdu_list[img_hdu].header wcs = WCS(header, naxis=2) # Probably shouldn't use these private attributes, but there doesn't seem to be another way to get the length of each axis from the WCS object. naxis1 = wcs._naxis1 naxis2 = wcs._naxis2 xgrid, ygrid = np.meshgrid(np.arange(naxis1), np.arange(naxis2)) if frame != 'pixel': flat_xcoords, flat_ycoords = wcs.wcs_pix2world(xgrid.flatten(), ygrid.flatten(), 0) xcoords = flat_xcoords.reshape(naxis2, naxis1) ycoords = flat_ycoords.reshape(naxis2, naxis1) else: xcoords = xgrid xcoords = ygrid del xgrid del ygrid if mask_type == 'radial': if frame != 'pixel': c = SkyCoord(center[0], center[1], frame=frame, unit=unit) c_new = c.transform_to(wcs_to_celestial_frame(wcs)) center_new = map(float, c_new.to_string().split(' ')) else: center_new = center mask = genRadialMask(xcoords, ycoords, radius, radius2, angle, center_new, frame=frame) hdu_list[img_hdu].header[ 'history'] = '{0} Applied radial mask to data.'.format( datetime.datetime.today().strftime('%d %B %Y')) hdu_list[img_hdu].header[ 'history'] = 'radius={0}, radius2={1}, angle={2}, center={3} {4}'.format( radius, radius2, angle, center, frame_str) elif mask_type == 'square': if frame != 'pixel': c = SkyCoord([extent[0], extent[2]], [extent[1], extent[3]], frame=frame, unit=unit) c_new = c.transform_to(wcs_to_celestial_frame(wcs)) l, t = map(float, c_new[0].to_string().split(' ')) r, b = map(float, c_new[1].to_string().split(' ')) extent_new = [l, r, b, t] else: extent_new = extent if 'GLON' in wcs.wcs.ctype[0]: mask = genSquareMask( np.where(xcoords < 180., xcoords, xcoords - 360.), ycoords, extent_new) else: mask = genSquarMask(xcoords, ycoords, extent_new) hdu_list[img_hdu].header[ 'history'] = '{0} Applied square mask to data.'.format( datetime.datetime.today().strftime('%d %B %Y')) hdu_list[img_hdu].header[ 'history'] = '[left, right, top, bottom]={0} {1}'.format( extent, frame_str) else: raise MaskTypeError( "{0} is not a supported mask geometry.".format(mask_type)) data_shape = hdu_list[img_hdu].data.shape xcoords_shape = xcoords.shape ycoords_shape = ycoords.shape naxis = header['NAXIS'] if naxis > 2: tile_shape = [1, 1, 1] E_idx = 0 for idx in range(1, 4): naxis_E = None if header['CTYPE{0}'.format(idx)] not in wcs.wcs.ctype: E_idx = idx naxis_E = header['NAXIS{0}'.format(idx)] tile_shape[naxis - idx] = naxis_E if naxis_E is None: raise NaxisError("Could not find the energy axis of the image.") try: # hdu_list[img_hdu].data = hdu_list[img_hdu].data*np.tile(mask, tuple(tile_shape)) hdu_list[img_hdu].data *= np.tile(mask, tuple(tile_shape)) except MemoryError: for idx in range(naxis_E): hdu_list[img_hdu].data[idx, :, :] *= mask del mask # Have to memmap data arrays because of intermediate arrays created by trapz tmp_dir = mkdtemp() try: energies_shape = hdu_list['ENERGIES'].data.energy.shape except AttributeError: energies_shape = hdu_list['ENERGIES'].data.Energy.shape m_data_path = os.path.join(tmp_dir, 'data_array.dat') m_energies_path = os.path.join(tmp_dir, 'energy_array.dat') m_xcoords_path = os.path.join(tmp_dir, 'xcoords_array.dat') m_ycoords_path = os.path.join(tmp_dir, 'ycoords_array.dat') m_data = np.memmap(m_data_path, dtype=np.float32, mode='w+', shape=data_shape) m_energies = np.memmap(m_energies_path, dtype=np.float32, mode='w+', shape=energies_shape) m_xcoords = np.memmap(m_xcoords_path, dtype=np.float32, mode='w+', shape=xcoords_shape) m_ycoords = np.memmap(m_ycoords_path, dtype=np.float32, mode='w+', shape=ycoords_shape) if wcs.wcs.cdelt[0] < 0.: m_data[:, :, :] = np.flip(hdu_list[img_hdu].data, axis=2)[:, :, :] m_xcoords[:, :] = np.flip(np.where(xcoords <= 180., xcoords, xcoords - 360.), axis=1)[:, :] else: m_data[:, :, :] = hdu_list[img_hdu].data[:, :, :] m_xcoords[:, :] = np.where(xcoords <= 180, xcoords, xcoords - 360.)[:, :] m_ycoords[:, :] = ycoords[:, :] try: m_energies[:] = hdu_list['ENERGIES'].data.energy[:] except AttributeError: m_energies[:] = hdu_list['ENERGIES'].data.Energy[:] del m_data del m_xcoords del m_ycoords del m_energies else: hdu_list[img_hdu].data *= mask del mask if not os.path.isabs(out): out = os.path.join(os.getcwd(), out) try: hdu_list[img_hdu].data[hdu_list[img_hdu].data < float_min] = float_min except MemoryError: for idx in range(naxis_E): hdu_list[img_hdu].data[idx, :, :][ hdu_list[img_hdu].data[idx, :, :] < float_min] = float_min try: hdu_list.writeto(out) except IOError: if clobber: os.remove(out) hdu_list.writeto(out) else: raise hdu_list.close() if naxis > 2: m_data = np.memmap(m_data_path, dtype=np.float32, mode='r', shape=data_shape) m_xcoords = np.memmap(m_xcoords_path, dtype=np.float32, mode='r', shape=xcoords_shape) m_ycoords = np.memmap(m_ycoords_path, dtype=np.float32, mode='r', shape=ycoords_shape) m_energies = np.memmap(m_energies_path, dtype=np.float32, mode='r', shape=energies_shape) flux = integrateMapCube(m_data, m_xcoords, m_ycoords, m_energies) del m_data del m_xcoords del m_ycoords del m_energies del xcoords del ycoords return out, flux else: return out
def _get_components_and_classes(self): # The aim of this function is to return whatever is needed for # world_axis_object_components and world_axis_object_classes. It's easier # to figure it out in one go and then return the values and let the # properties return part of it. # Since this method might get called quite a few times, we need to cache # it. We start off by defining a hash based on the attributes of the # WCS that matter here (we can't just use the WCS object as a hash since # it is mutable) wcs_hash = (self.naxis, list(self.wcs.ctype), list(self.wcs.cunit), self.wcs.radesys, self.wcs.specsys, self.wcs.equinox, self.wcs.dateobs, self.wcs.lng, self.wcs.lat) # If the cache is present, we need to check that the 'hash' matches. if getattr(self, '_components_and_classes_cache', None) is not None: cache = self._components_and_classes_cache if cache[0] == wcs_hash: return cache[1] else: self._components_and_classes_cache = None # Avoid circular imports by importing here from astropy.wcs.utils import wcs_to_celestial_frame from astropy.coordinates import SkyCoord, EarthLocation from astropy.time.formats import FITS_DEPRECATED_SCALES from astropy.time import Time, TimeDelta components = [None] * self.naxis classes = {} # Let's start off by checking whether the WCS has a pair of celestial # components if self.has_celestial: try: celestial_frame = wcs_to_celestial_frame(self) except ValueError: # Some WCSes, e.g. solar, can be recognized by WCSLIB as being # celestial but we don't necessarily have frames for them. celestial_frame = None else: kwargs = {} kwargs['frame'] = celestial_frame kwargs['unit'] = u.deg classes['celestial'] = (SkyCoord, (), kwargs) components[self.wcs.lng] = ('celestial', 0, 'spherical.lon.degree') components[self.wcs.lat] = ('celestial', 1, 'spherical.lat.degree') # Next, we check for spectral components if self.has_spectral: # Find index of spectral coordinate ispec = self.wcs.spec ctype = self.wcs.ctype[ispec][:4] ctype = ctype.upper() kwargs = {} # Determine observer location and velocity # TODO: determine how WCS standard would deal with observer on a # spacecraft far from earth. For now assume the obsgeo parameters, # if present, give the geocentric observer location. if np.isnan(self.wcs.obsgeo[0]): observer = None else: earth_location = EarthLocation(*self.wcs.obsgeo[:3], unit=u.m) obstime = Time(self.wcs.mjdobs, format='mjd', scale='utc', location=earth_location) observer_location = SkyCoord( earth_location.get_itrs(obstime=obstime)) if self.wcs.specsys in VELOCITY_FRAMES: frame = VELOCITY_FRAMES[self.wcs.specsys] observer = observer_location.transform_to(frame) if isinstance(frame, str): observer = attach_zero_velocities(observer) else: observer = update_differentials_to_match( observer_location, VELOCITY_FRAMES[self.wcs.specsys], preserve_observer_frame=True) elif self.wcs.specsys == 'TOPOCENT': observer = attach_zero_velocities(observer_location) else: raise NotImplementedError( f'SPECSYS={self.wcs.specsys} not yet supported') # Determine target # This is tricker. In principle the target for each pixel is the # celestial coordinates of the pixel, but we then need to be very # careful about SSYSOBS which is tricky. For now, we set the # target using the reference celestial coordinate in the WCS (if # any). if self.has_celestial and celestial_frame is not None: # NOTE: celestial_frame was defined higher up # NOTE: we set the distance explicitly to avoid warnings in SpectralCoord target = SkyCoord(self.wcs.crval[self.wcs.lng] * self.wcs.cunit[self.wcs.lng], self.wcs.crval[self.wcs.lat] * self.wcs.cunit[self.wcs.lat], frame=celestial_frame, distance=1000 * u.kpc) target = attach_zero_velocities(target) else: target = None # SpectralCoord does not work properly if either observer or target # are not convertible to ICRS, so if this is the case, we (for now) # drop the observer and target from the SpectralCoord and warn the # user. if observer is not None: try: observer.transform_to(ICRS()) except Exception: warnings.warn( 'observer cannot be converted to ICRS, so will ' 'not be set on SpectralCoord', AstropyUserWarning) observer = None if target is not None: try: target.transform_to(ICRS()) except Exception: warnings.warn( 'target cannot be converted to ICRS, so will ' 'not be set on SpectralCoord', AstropyUserWarning) target = None # NOTE: below we include Quantity in classes['spectral'] instead # of SpectralCoord - this is because we want to also be able to # accept plain quantities. if ctype == 'ZOPT': def spectralcoord_from_redshift(redshift): return SpectralCoord((redshift + 1) * self.wcs.restwav, unit=u.m, observer=observer, target=target) def redshift_from_spectralcoord(spectralcoord): # TODO: check target is consistent if observer is None: warnings.warn( 'No observer defined on WCS, SpectralCoord ' 'will be converted without any velocity ' 'frame change', AstropyUserWarning) return spectralcoord.to_value( u.m) / self.wcs.restwav - 1. else: return spectralcoord.in_observer_velocity_frame( observer).to_value(u.m) / self.wcs.restwav - 1. classes['spectral'] = (u.Quantity, (), {}, spectralcoord_from_redshift) components[self.wcs.spec] = ('spectral', 0, redshift_from_spectralcoord) elif ctype == 'BETA': def spectralcoord_from_beta(beta): return SpectralCoord(beta * C_SI, unit=u.m / u.s, doppler_convention='relativistic', doppler_rest=self.wcs.restwav * u.m, observer=observer, target=target) def beta_from_spectralcoord(spectralcoord): # TODO: check target is consistent doppler_equiv = u.doppler_relativistic(self.wcs.restwav * u.m) if observer is None: warnings.warn( 'No observer defined on WCS, SpectralCoord ' 'will be converted without any velocity ' 'frame change', AstropyUserWarning) return spectralcoord.to_value(u.m / u.s, doppler_equiv) / C_SI else: return spectralcoord.in_observer_velocity_frame( observer).to_value(u.m / u.s, doppler_equiv) / C_SI classes['spectral'] = (u.Quantity, (), {}, spectralcoord_from_beta) components[self.wcs.spec] = ('spectral', 0, beta_from_spectralcoord) else: kwargs['unit'] = self.wcs.cunit[ispec] if self.wcs.restfrq > 0: if ctype == 'VELO': kwargs['doppler_convention'] = 'relativistic' kwargs['doppler_rest'] = self.wcs.restfrq * u.Hz elif ctype == 'VRAD': kwargs['doppler_convention'] = 'radio' kwargs['doppler_rest'] = self.wcs.restfrq * u.Hz elif ctype == 'VOPT': kwargs['doppler_convention'] = 'optical' kwargs['doppler_rest'] = self.wcs.restwav * u.m def spectralcoord_from_value(value): return SpectralCoord(value, observer=observer, target=target, **kwargs) def value_from_spectralcoord(spectralcoord): # TODO: check target is consistent if observer is None: warnings.warn( 'No observer defined on WCS, SpectralCoord ' 'will be converted without any velocity ' 'frame change', AstropyUserWarning) return spectralcoord.to_value(**kwargs) else: return spectralcoord.in_observer_velocity_frame( observer).to_value(**kwargs) classes['spectral'] = (u.Quantity, (), {}, spectralcoord_from_value) components[self.wcs.spec] = ('spectral', 0, value_from_spectralcoord) # We can then make sure we correctly return Time objects where appropriate # (https://www.aanda.org/articles/aa/pdf/2015/02/aa24653-14.pdf) if 'time' in self.world_axis_physical_types: multiple_time = self.world_axis_physical_types.count('time') > 1 for i in range(self.naxis): if self.world_axis_physical_types[i] == 'time': if multiple_time: name = f'time.{i}' else: name = 'time' # Initialize delta reference_time_delta = None # Extract time scale scale = self.wcs.ctype[i].lower() if scale == 'time': if self.wcs.timesys: scale = self.wcs.timesys.lower() else: scale = 'utc' # Drop sub-scales if '(' in scale: pos = scale.index('(') scale, subscale = scale[:pos], scale[pos + 1:-1] warnings.warn( f'Dropping unsupported sub-scale ' f'{subscale.upper()} from scale {scale.upper()}', UserWarning) # TODO: consider having GPS as a scale in Time # For now GPS is not a scale, we approximate this by TAI - 19s if scale == 'gps': reference_time_delta = TimeDelta(19, format='sec') scale = 'tai' elif scale.upper() in FITS_DEPRECATED_SCALES: scale = FITS_DEPRECATED_SCALES[scale.upper()] elif scale not in Time.SCALES: raise ValueError( f'Unrecognized time CTYPE={self.wcs.ctype[i]}') # Determine location trefpos = self.wcs.trefpos.lower() if trefpos.startswith('topocent'): # Note that some headers use TOPOCENT instead of TOPOCENTER if np.any(np.isnan(self.wcs.obsgeo[:3])): warnings.warn( 'Missing or incomplete observer location ' 'information, setting location in Time to None', UserWarning) location = None else: location = EarthLocation(*self.wcs.obsgeo[:3], unit=u.m) elif trefpos == 'geocenter': location = EarthLocation(0, 0, 0, unit=u.m) elif trefpos == '': location = None else: # TODO: implement support for more locations when Time supports it warnings.warn( f"Observation location '{trefpos}' is not " "supported, setting location in Time to None", UserWarning) location = None reference_time = Time(np.nan_to_num(self.wcs.mjdref[0]), np.nan_to_num(self.wcs.mjdref[1]), format='mjd', scale=scale, location=location) if reference_time_delta is not None: reference_time = reference_time + reference_time_delta def time_from_reference_and_offset(offset): if isinstance(offset, Time): return offset return reference_time + TimeDelta(offset, format='sec') def offset_from_time_and_reference(time): return (time - reference_time).sec classes[name] = (Time, (), {}, time_from_reference_and_offset) components[i] = (name, 0, offset_from_time_and_reference) # Fallback: for any remaining components that haven't been identified, just # return Quantity as the class to use for i in range(self.naxis): if components[i] is None: name = self.wcs.ctype[i].split('-')[0].lower() if name == '': name = 'world' while name in classes: name += "_" classes[name] = (u.Quantity, (), {'unit': self.wcs.cunit[i]}) components[i] = (name, 0, 'value') # Keep a cached version of result self._components_and_classes_cache = wcs_hash, (components, classes) return components, classes
def make_cutouts(data, catalog, wcs=None, origin=0, verbose=True): """Make cutouts of catalog targets from a 2D image array. Expects input image WCS to be in the TAN projection. Parameters ---------- data : 2D `~numpy.ndarray` or `~astropy.nddata.NDData` The 2D cutout array. catalog : `~astropy.table.table.Table` Catalog table defining the sources to cut out. Must contain unit information as the cutout tool does not assume default units. wcs : `~astropy.wcs.wcs.WCS` WCS if the input image is `~numpy.ndarray`. origin : int Whether SkyCoord.from_pixel should use 0 or 1-based pixel coordinates. verbose : bool Print extra info. Default is `True`. Returns ------- cutouts : list A list of NDData. If cutout failed for a target, `None` will be added as a place holder. Output WCS will in be in Tan projection. Notes ----- The input Catalog must have the following columns, which MUST have units information where applicable: * ``'id'`` - ID string; no unit necessary. * ``'coords'`` - SkyCoord (Overrides ra, dec, x and y columns). * ``'ra'`` or ``'x'``- RA (angular units e.g., deg, H:M:S, arcsec etc..) or pixel x position (only in `~astropy.units.pix`). * ``'dec'`` or ``'y'`` - Dec (angular units e.g., deg, D:M:S, arcsec etc..) or pixel y position (only in `~astropy.units.pix`). * ``'cutout_width'`` - Cutout width (e.g., in arcsec, pix). * ``'cutout_height'`` - Cutout height (e.g., in arcsec, pix). Optional columns: * ``'cutout_pa'`` - Cutout angle (e.g., in deg, arcsec). This is only use if user chooses to rotate the cutouts. Positive value will result in a clockwise rotation. If saved to fits, cutouts are organized as follows: <output_dir>/ <id>.fits Each cutout image is a simple single-extension FITS with updated WCS. Its header has the following special keywords: * ``OBJ_RA`` - RA of the cutout object in degrees. * ``OBJ_DEC`` - DEC of the cutout object in degrees. * ``OBJ_ROT`` - Rotation of cutout object in degrees. """ # Do not rotate if column is missing. if 'cutout_pa' in catalog.colnames: if catalog['cutout_pa'].unit is None: raise u.UnitsError("Units not specified for cutout_pa.") apply_rotation = True else: apply_rotation = False # Optional dependencies... if apply_rotation: try: from reproject.interpolation.high_level import reproject_interp except ImportError as e: raise ImportError("Optional requirement not met: " + e.msg) # Search for wcs: if isinstance(data, NDData): if wcs is not None: raise Exception("Ambiguous: WCS defined in NDData and parameters.") wcs = data.wcs elif not isinstance(data, np.ndarray): raise TypeError("Input image should be a 2D `~numpy.ndarray` " "or `~astropy.nddata.NDData") elif wcs is None: raise Exception("WCS information was not provided.") if wcs.wcs.ctype[0] != 'RA---TAN' or wcs.wcs.ctype[1] != 'DEC--TAN': raise Exception("Expected WCS to be in the TAN projection.") # Calculate the pixel scale of input image: pixel_scales = proj_plane_pixel_scales(wcs) pixel_scale_width = pixel_scales[0] * u.Unit(wcs.wcs.cunit[0]) / u.pix pixel_scale_height = pixel_scales[1] * u.Unit(wcs.wcs.cunit[1]) / u.pix # Check if `SkyCoord`s are available: if 'coords' in catalog.colnames: coords = catalog['coords'] if not isinstance(coords, SkyCoord): raise TypeError('The coords column is not a SkyCoord') elif 'ra' in catalog.colnames and 'dec' in catalog.colnames: if 'x' in catalog.colnames and 'y' in catalog.colnames: raise Exception("Ambiguous catalog: Both (ra, dec) and pixel positions provided.") if catalog['ra'].unit is None or catalog['dec'].unit is None: raise u.UnitsError("Units not specified for ra and/or dec columns.") coords = SkyCoord(catalog['ra'], catalog['dec'], unit=(catalog['ra'].unit, catalog['dec'].unit)) elif 'x' in catalog.colnames and 'y' in catalog.colnames: coords = SkyCoord.from_pixel(catalog['x'].astype(float), catalog['y'].astype(float), wcs, origin=origin) else: try: coords = SkyCoord.guess_from_table(catalog) except Exception as e: raise e coords = coords.transform_to(wcs_to_celestial_frame(wcs)) # Figure out cutout size: if 'cutout_width' in catalog.colnames: if catalog['cutout_width'].unit is None: raise u.UnitsError("Units not specified for cutout_width.") if catalog['cutout_width'].unit == u.pix: width = catalog['cutout_width'].astype(float) # pix else: width = (catalog['cutout_width'] / pixel_scale_width).decompose().value # pix else: raise Exception("cutout_width column not found in catalog.") if 'cutout_height' in catalog.colnames: if catalog['cutout_height'].unit is None: raise u.UnitsError("Units not specified for cutout_height.") if catalog['cutout_height'].unit == u.pix: height = catalog['cutout_height'].astype(float) # pix else: height = (catalog['cutout_height'] / pixel_scale_height).decompose().value # pix else: raise Exception("cutout_height column not found in catalog.") cutcls = partial(Cutout2D, data.data, wcs=wcs, mode='partial') cutouts = [] for position, x_pix, y_pix, row in zip(coords, width, height, catalog): if apply_rotation: pix_rot = row['cutout_pa'].to(u.degree).value # Construct new rotated WCS: cutout_wcs = WCS(naxis=2) cutout_wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] cutout_wcs.wcs.crval = [position.ra.deg, position.dec.deg] cutout_wcs.wcs.crpix = [(x_pix + 1) * 0.5, (y_pix + 1) * 0.5] try: cutout_wcs.wcs.cd = wcs.wcs.cd cutout_wcs.rotateCD(-pix_rot) except AttributeError: cutout_wcs.wcs.cdelt = wcs.wcs.cdelt cutout_wcs.wcs.crota = [0, -pix_rot] cutout_hdr = cutout_wcs.to_header() # Rotate the image using reproject try: cutout_arr = reproject_interp( (data, wcs), cutout_hdr, shape_out=(math.floor(y_pix + math.copysign(0.5, y_pix)), math.floor(x_pix + math.copysign(0.5, x_pix))), order=1) except Exception: if verbose: log.info('reproject failed: ' 'Skipping {0}'.format(row['id'])) cutouts.append(None) continue cutout_arr = cutout_arr[0] # Ignore footprint cutout_hdr['OBJ_ROT'] = (pix_rot, 'Cutout rotation in degrees') else: # Make cutout or handle exceptions by adding None to output list try: cutout = cutcls(position, size=(y_pix, x_pix)) except NoConvergence: if verbose: log.info('WCS solution did not converge: ' 'Skipping {0}'.format(row['id'])) cutouts.append(None) continue except NoOverlapError: if verbose: log.info('Cutout is not on image: ' 'Skipping {0}'.format(row['id'])) cutouts.append(None) continue else: cutout_hdr = cutout.wcs.to_header() cutout_arr = cutout.data # If cutout result is empty, skip that target if np.array_equiv(cutout_arr, 0): if verbose: log.info('No data in cutout: Skipping {0}'.format(row['id'])) cutouts.append(None) continue # Finish constructing header. cutout_hdr['OBJ_RA'] = (position.ra.deg, 'Cutout object RA in deg') cutout_hdr['OBJ_DEC'] = (position.dec.deg, 'Cutout object DEC in deg') cutouts.append(NDData(data=cutout_arr, wcs=WCS(cutout_hdr), meta=cutout_hdr)) return cutouts