def get_fov(self, radius, fermi_frame=False): """ Returns ------- array of RA and DEC """ steps = 500 if fermi_frame: fermi = self._center poly = SphericalPolygon.from_cone(fermi.lon.value, fermi.lat.value, radius, steps=steps) else: j2000 = self._center.icrs poly = SphericalPolygon.from_cone(j2000.ra.value, j2000.dec.value, radius, steps=steps) # ra, dec return [p for p in poly.to_radec()][0]
def _update_bounding_polygon(self): polygons = [im.polygon for im in self._images] if len(polygons) == 0: self._polygon = SphericalPolygon([]) self._radec = [] else: self._polygon = SphericalPolygon.multi_union(polygons) self._radec = list(self._polygon.to_radec())
def get_difference(poly1, poly2, pixels=None): """attempt to get the difference poly1-poly2 as poly1^~poly2 use a pixelation to attempt to find an outside point""" import matplotlib.pyplot as plt from mpl_toolkits.basemap import Basemap m = Basemap(projection='moll', lon_0=0) if pixels is None: pixels = get_healpix_pixelation(4) bounding_xyz = list(poly2.points) #contained = np.zeros(pixels.shape[0],dtype==bool) #for itr in range(0,len(bounding_xyz)): # ra,dec = sgv.vector_to_radec( # contained = contained | contains_points(bounding_xyz[itr],pixels) contained = contains_points(pixels, poly2) poly2_complement = None #poly2_complement = poly2.invert_polygon() #contained_c = contains_points(pixels,poly2_complement) #assert not np.any(contained & contained_c) first_false = 100 + np.argmin(contained[100:]) #print("orig 1",contains_points(pixels[first_false:first_false+1],poly1),poly1.area()) #print("orig 2",contains_points(pixels[first_false:first_false+1],poly2),poly2.area()) print(poly2) colors = ['red', 'green', 'blue'] for itr in range(0, len(bounding_xyz)): first_false = 100 + itr + np.argmin(contained[100 + itr:]) theta_in = pixels[first_false, 0] phi_in = pixels[first_false, 1] inside_xyz = np.asarray( sgv.radec_to_vector(phi_in, theta_in - np.pi / 2., degrees=False)) loc_poly = SphericalPolygon(bounding_xyz[itr].copy(), inside_xyz.copy()) loc_poly.draw(m, color=colors[itr]) cont = contains_points(pixels[first_false:first_false + 1], loc_poly) print("loc contains: ", cont) if poly2_complement is None: poly2_complement = deepcopy(loc_poly) else: poly2_complement = deepcopy( poly2_complement.intersection(loc_poly)) cont_comp = contains_points(pixels[first_false:first_false + 1], poly2_complement) print("comp contains: ", cont_comp) print("test: ", np.all(contains_points(pixels[contained], poly2_complement))) print("test: ", np.any(contains_points(pixels[~contained], poly2_complement))) print("insp: ", list(poly2_complement.inside)) #print("comp ",itr,contains_points(pixels[first_false:first_false+1],poly2_complement),poly2_complement.area(),poly2_complement.is_clockwise()) #print("inside c ",list(poly2_complement.inside)) #print("vert c ",list(poly2_complement.points)) #for itr2 in range(0,len(list(poly2_complement.inside))): # print("loc cont inside c ",loc_poly.contains_point(list(poly2_complement.inside)[itr2])) #print(poly1.area(),poly2.area(),poly2_complement.area()) plt.show() return poly1.intersection(poly2_complement)
def get_earth_point_with_time(self,t): earth_radius = 6371. * u.km if self.sc_pos_f is not None and self.time_band is not None: x_f, y_f, z_f = self.sc_pos_f try: n = len(t) t = np.array(t) tband = t.max() - t.min() tband_sl = self.time_band[1] - self.time_band[0] if tband <= tband_sl + 2: t[t <= self.time_band[0]] = self.time_band[0] + 0.00001 t[t >= self.time_band[1]] = self.time_band[1] - 0.00001 else: t[t <= self.time_band[0]] = np.nan t[t >= self.time_band[1]] = np.nan x = x_f(t) * self.pos_unit y = y_f(t) * self.pos_unit z = z_f(t) * self.pos_unit earth_point_list = [] for i in range(n): position = cartesian_to_spherical(x[i],y[i],z[i]) xyz_position = SkyCoord(position[2].deg,position[1].deg,frame='icrs',unit='deg') fermi_radius = np.sqrt(x[i]**2 + y[i]**2 + z[i]**2) radius_deg = np.rad2deg(np.arcsin((earth_radius / fermi_radius).to(u.dimensionless_unscaled)).value) poly = SphericalPolygon.from_cone(position[2].deg,position[1].deg,radius_deg,steps=180) x_,y_ = np.array(list(poly.to_radec())[0]) earth_point_list.append([xyz_position,radius_deg,x_,y_]) return earth_point_list except (TypeError): if (t<=self.time_band[0]): if (self.time_band[0]-t<=1): t = self.time_band[0]+0.00001 else: t = np.nan if t>=self.time_band[1]: if (t-self.time_band[0]<=1): t = self.time_band[1]-0.00001 else: t = np.nan x = x_f(t) * self.pos_unit y = y_f(t) * self.pos_unit z = z_f(t) * self.pos_unit position = cartesian_to_spherical(x,y,z) xyz_position = SkyCoord(position[2].deg,position[1].deg,frame='icrs',unit='deg') fermi_radius = np.sqrt(x**2 + y**2 + z**2) radius_deg = np.rad2deg(np.arcsin((earth_radius / fermi_radius).to(u.dimensionless_unscaled)).value) poly = SphericalPolygon.from_cone(position[2].deg,position[1].deg,radius_deg,steps=180) x_,y_ = np.array(list(poly.to_radec())[0]) return xyz_position,radius_deg,x_,y_ else: return None
def build_polygon(self, member='total'): if self.edges_ra is None: self.get_edges_sky(member=member) self.polygon = SphericalPolygon.from_radec(self.edges_ra, self.edges_dec, self.meta_wcs.wcs.crval)
def get_earth_point(self, index=None): if self.sc_pos is not None: earth_point_list = [] if index is not None: sc_pos_list = self.sc_pos[index] else: sc_pos_list = self.sc_pos for sc_pos in sc_pos_list: position = cartesian_to_spherical(-sc_pos[0], -sc_pos[1], -sc_pos[2]) xyz_position = SkyCoord(position[2].deg, position[1].deg, frame='icrs', unit='deg') earth_radius = 6371. * u.km fermi_radius = np.sqrt((sc_pos**2).sum()) radius_deg = np.rad2deg( np.arcsin((earth_radius / fermi_radius).to( u.dimensionless_unscaled)).value) poly = SphericalPolygon.from_cone(position[2].deg, position[1].deg, radius_deg, steps=100) x, y = [p for p in poly.to_radec()][0] earth_point_list.append([xyz_position, radius_deg, x, y]) return earth_point_list else: print('No satellite position!') return None
def sky_cone(ra_c, dec_c, theta, steps=50, include_center=True): """ Get ra and dec coordinates of a cone on the sky. Parameters ---------- ra_c, dec_c: float Center of cone in degrees. theta: astropy Quantity, float, or int Angular radius of cone. Must be in arcsec if not a Quantity object. steps: int, optional Number of steps in the cone. include_center: bool, optional If True, include center point in cone. Returns ------- ra, dec: ndarry Coordinates of cone. """ if isinstance(theta, float) or isinstance(theta, int): theta = theta * u.Unit('arcsec') cone = SphericalPolygon.from_cone(ra_c, dec_c, theta.to('deg').value, steps=steps) ra, dec = list(cone.to_lonlat())[0] ra = np.mod(ra - 360., 360.0) if include_center: ra = np.concatenate([ra, [ra_c]]) dec = np.concatenate([dec, [dec_c]]) return ra, dec
def build_polygon(self): if self.edges_ra is None: self.get_edges_sky() self.polygon = SphericalPolygon.from_radec(self.edges_ra, self.edges_dec, self.meta_wcs.wcs.crval)
def build_polygon(self): inner_pix = self.wcs.pixel_to_world_values(2, 2) # define polygon on the sky self.polygon = SphericalPolygon.from_radec(self.corners[:, 0], self.corners[:, 1], inner_pix)
def getWcsRegion(imageRow): region = None stcs = imageRow["regionSTCS"] if (stcs is not None): a = stcs.split(' ') if len(a) == 10: ras = [ float(a[2]), float(a[4]), float(a[6]), float(a[8]), float(a[2]) ] decs = [ float(a[3]), float(a[5]), float(a[7]), float(a[9]), float(a[3]) ] print ras print decs region = SphericalPolygon.from_radec(ras, decs) return region
def get_fov(conter,radius = 10.): fov_point_list = [] for conter_i in conter: poly = SphericalPolygon.from_cone(conter_i.ra.value,conter_i.dec.value,radius,steps=100) x,y = [p for p in poly.to_radec()][0] fov_point_list.append([radius,x,y]) return fov_point_list
def get_fov(self,conter,radius = 10.): fov_point_list = [] for conter_i in conter: poly = SphericalPolygon.from_cone(conter_i.ra.value,conter_i.dec.value,radius,steps=180) x,y = np.array(list(poly.to_radec())[0]) fov_point_list.append([radius,x,y]) return fov_point_list
def contains_point(self, point): steps = 300 j2000 = self.center.icrs poly = SphericalPolygon.from_cone(j2000.ra.value, j2000.dec.value, self.radius, steps=steps) return poly.contains_point(point.cartesian.xyz.value)
def set_members(self, mlist, polygon): if mlist is None: member_list = [] elif isinstance(mlist, list): member_list = mlist if [1 for m in mlist if not isinstance(m, SkyLineMember)]: raise ValueError("The 'mlist' argument must be either " "a single 'SkyLineMember' object or a " "Python list of 'SkyLineMember' objects.") elif isinstance(mlist, SkyLineMember): member_list = [mlist] else: raise ValueError("The 'mlist' argument must be either " "a single 'SkyLineMember' object or a " "Python list of 'SkyLineMember' objects.") self._members = [] # Not using set to preserve order n = len(member_list) if n == 0: if polygon is None: self.polygon = SphericalPolygon([]) else: self.polygon = deepcopy(polygon) self._id = '' self._is_mf_mosaic = False elif n == 1: assert isinstance(member_list[0], SkyLineMember) if polygon is None: self.polygon = deepcopy(member_list[0].polygon) else: self.polygon = deepcopy(polygon) self._id = member_list[0].id self._members.append(member_list[0]) self._is_mf_mosaic = False else: assert isinstance(member_list[0], SkyLineMember) if polygon is None: mpol = deepcopy(member_list[0].polygon) else: mpol = deepcopy(polygon) self._members.append(member_list[0]) for m in member_list[1:]: # Report corrupted members list instead of skipping assert isinstance(m, SkyLineMember) if m not in self._members: self._members.append(m) if polygon is None: mpol = mpol.union(m.polygon) self.polygon = mpol self._update_mosaic_flag_id()
def get_poly(theta_vertices, phi_vertices, theta_in, phi_in): """get the SphericalPolygon object for the geometry""" bounding_theta = theta_vertices - np.pi / 2. #to radec bounding_phi = phi_vertices bounding_xyz = np.asarray( sgv.radec_to_vector(bounding_phi, bounding_theta, degrees=False)).T inside_xyz = np.asarray( sgv.radec_to_vector(phi_in, theta_in - np.pi / 2., degrees=False)) sp_poly = SphericalPolygon(bounding_xyz, inside=inside_xyz) return sp_poly
def get_fov(self,radius): '''这里是计算出探头标注''' if radius >= 60: steps = 500 elif radius >= 30: steps = 300 else: steps = 250 j2000 = self.center.icrs poly = SphericalPolygon.from_cone(j2000.ra.value,j2000.dec.value,radius,steps = steps) re = [p for p in poly.to_radec()][0] return re
def makeQueryRegion(ra, dec, size): region = None if size[1] is None: # Then we have a radius, not a box. radius = size[0] region = SphericalPolygon.from_cone(ra, dec, radius) else: # Make a box. radius = None # Figure out this stupid shape later... return region
def getWcsRegion(imageRow): region = None stcs = imageRow["regionSTCS"] if stcs is not None: a = stcs.split(" ") if len(a) == 10: ras = [float(a[2]), float(a[4]), float(a[6]), float(a[8]), float(a[2])] decs = [float(a[3]), float(a[5]), float(a[7]), float(a[9]), float(a[3])] print ras print decs region = SphericalPolygon.from_radec(ras, decs) return region
def get_fov(self, radius): if radius >= 60: steps = 5000 ## could be modified to speed up the plotting elif radius >= 30: steps = 400 ## could be modified to speed up the plotting else: steps = 100 ## could be modified to speed up the plotting j2000 = self.center.icrs poly = SphericalPolygon.from_cone(j2000.ra.value, j2000.dec.value, radius, steps=steps) re = [p for p in poly.to_radec()][0] return re
def run(self, dataSlice, slicePoint=None): # RA and Dec from dataSlice doesn't necessarily match healpix RA = dataSlice['fieldRA'][0] Dec = dataSlice['fieldDec'][0] # Get RA and Dec from slicer RA = slicePoint['ra'] Dec = slicePoint['dec'] nside = slicePoint['nside'] # get the boundaries from HealPix bounding = hp.boundaries(nside, slicePoint['sid'], step=1).T inside_val = np.asarray( sgv.radec_to_vector(slicePoint['ra'], slicePoint['dec'], degrees=False)) test_pointing = SphericalPolygon(bounding, inside_val) overlap_area = [ test_pointing.intersection(survey_polygon).area() for survey_polygon in survey_list[self.region_name] ] total = sum(overlap_area) healpix_area = test_pointing.area() return min(total, 4 * np.pi - total)
def plot_earth(self, t, satellite, **kwargs): if 'facecolor' not in kwargs: kwargs['facecolor'] = '#90d7ec' if 'edgecolor' not in kwargs: kwargs['edgecolor'] = '#90d7ec' try: met, ra, dec, radius = satellite.get_earth_point(t) for i in range(len(met)): poly = SphericalPolygon.from_cone(ra[i], dec[i], radius[i], steps=180) x_, y_ = np.array(list(poly.to_radec())[0]) earth = self.Polygon(list(zip(x_, y_))[::-1], **kwargs) self.ax.add_patch(earth) except (TypeError): met, ra, dec, radius = satellite.get_earth_point(t) poly = SphericalPolygon.from_cone(ra, dec, radius, steps=180) x_, y_ = np.array(list(poly.to_radec())[0]) earth = self.Polygon(list(zip(x_, y_))[::-1], **kwargs) self.ax.add_patch(earth)
def _draw_members(self, map, **kwargs): """ Draw individual extensions in members. Useful for debugging. Parameters ---------- map : Basemap axes object **kwargs : Any plot arguments to pass to basemap """ wcs_list = self._indv_mem_wcslist() for wcs in wcs_list: poly = SphericalPolygon.from_wcs(wcs) poly.draw(map, **kwargs)
def get_good_detector_centers(self, source=None): good_detector_centers = [] good_detector_index = [] if source is not None: for index, center in enumerate(self.detectors.center_all): steps = 250 j2000 = center.icrs poly = SphericalPolygon.from_cone(j2000.ra.value, j2000.dec.value, self.radius, steps=steps) if (poly.contains_point(source.cartesian.xyz.value)): good_detector_centers.append(center) good_detector_index.append(index) else: print('No source! return []') return good_detector_index, good_detector_centers
def get_fov(self, radius): if radius >= 60: steps = 500 elif (radius >= 30): steps = 300 else: steps = 250 j2000 = self.center_all.icrs c = [] for index, center in enumerate(j2000): print(str(index)) poly = SphericalPolygon.from_cone(center.ra.value, center.dec.value, radius, steps=steps) re = [p for p in poly.to_radec()][0] #print(re) c.append(re) return c
def quad_to_poly(ra, dec, **kwargs): p = SphericalPolygon.from_radec(ra, dec, degrees=True) points = p.polygons[0]._points _ra, _dec = [], [] for A, B in zip(points[0:-1], points[1:]): length = great_circle_arc.length(A, B, degrees=True) if not np.isfinite(length): length = 2 interpolated = great_circle_arc.interpolate(A, B, length * 4) lon, lat = vector.vector_to_lonlat(interpolated[:, 0], interpolated[:, 1], interpolated[:, 2], degrees=True) for lon0, lat0, lon1, lat1 in zip(lon[0:-1], lat[0:-1], lon[1:], lat[1:]): _ra.append(lon0) _dec.append(lat0) _ra.append(lon1) _dec.append(lat1) return PolyCollection([np.c_[_ra, _dec]], **kwargs)
def true_axial_analysis(df_row, traj_def, euler_def, path_fnc, add_axial_rot_fnc): traj = df_row[traj_def] start_idx = df_row['up_down_analysis'].max_run_up_start_idx end_idx = df_row['up_down_analysis'].max_run_up_end_idx orient = np.rad2deg(rgetattr(traj, euler_def)) true_axial = np.rad2deg(getattr(traj, 'true_axial_rot')) apparent_orient_diff = orient[end_idx, 2] - orient[start_idx, 2] true_axial_diff = true_axial[end_idx] - true_axial[start_idx] add_axial_rot = add_axial_rot_fnc(orient, start_idx, end_idx) long, lat = path_fnc(orient, start_idx, end_idx) # compute the area mid_ix = int((start_idx + end_idx) / 2) sp = SphericalPolygon.from_lonlat(long, lat, center=(orient[mid_ix, 0], orient[mid_ix, 1]/2)) area = np.rad2deg(sp.area()) # if the actual path and the "euler" path cross each other the spherical_geometry polygon incorrectly estimates the # area while area > 180: area -= 180 return apparent_orient_diff, true_axial_diff, area, add_axial_rot, sp.is_clockwise()
def _calc_sky_orig(self, overlap=None, delta=True): """ Compute sky background value. Parameters ---------- overlap : SkyImage, SkyGroup, SphericalPolygon, list of tuples, \ None, optional Another `SkyImage`, `SkyGroup`, :py:class:`spherical_geometry.polygons.SphericalPolygon`, or a list of tuples of (RA, DEC) of vertices of a spherical polygon. This parameter is used to indicate that sky statistics should computed only in the region of intersection of *this* image with the polygon indicated by `overlap`. When `overlap` is `None`, sky statistics will be computed over the entire image. delta : bool, optional Should this function return absolute sky value or the difference between the computed value and the value of the sky stored in the `sky` property. Returns ------- skyval : float, None Computed sky value (absolute or relative to the `sky` attribute). If there are no valid data to perform this computations (e.g., because this image does not overlap with the image indicated by `overlap`), `skyval` will be set to `None`. npix : int Number of pixels used to compute sky statistics. polyarea : float Area (in srad) of the polygon that bounds data used to compute sky statistics. """ if overlap is None: if self.mask is None: data = self.image else: data = self.image[self.mask] polyarea = self.poly_area else: fill_mask = np.zeros(self.image.shape, dtype=bool) if isinstance(overlap, (SkyImage, SkyGroup, SphericalPolygon)): intersection = self.intersection(overlap) polyarea = np.fabs(intersection.area()) radec = intersection.to_radec() else: # assume a list of (ra, dec) tuples: radec = [] polyarea = 0.0 for r, d in overlap: poly = SphericalPolygon.from_radec(r, d) polyarea1 = np.fabs(poly.area()) if polyarea1 == 0.0 or len(r) < 4: continue polyarea += polyarea1 radec.append(self.intersection(poly).to_radec()) if polyarea == 0.0: return (None, 0, 0.0) for ra, dec in radec: if len(ra) < 4: continue # set pixels in 'fill_mask' that are inside a polygon to True: x, y = self.wcs_inv(ra, dec) poly_vert = list(zip(*[x, y])) polygon = region.Polygon(True, poly_vert) fill_mask = polygon.scan(fill_mask) if self.mask is not None: fill_mask &= self.mask data = self.image[fill_mask] if data.size < 1: return (None, 0, 0.0) # Calculate sky try: skyval, npix = self._skystat(data) except ValueError: return (None, 0, 0.0) if delta: skyval -= self._sky return skyval, npix, polyarea
def __init__(self, image, wcs_fwd, wcs_inv, pix_area=1.0, convf=1.0, mask=None, id=None, skystat=None, stepsize=None, meta=None): """ Initializes the SkyImage object. Parameters ---------- image : numpy.ndarray A 2D array of image data. wcs_fwd : function "forward" pixel-to-world transformation function. wcs_inv : function "inverse" world-to-pixel transformation function. pix_area : float, optional Average pixel's sky area. convf : float, optional Conversion factor that when multiplied to `image` data converts the data to "uniform" (across multiple images) surface brightness units. .. note:: The functionality to support this conversion is not yet implemented and at this moment `convf` is ignored. mask : numpy.ndarray A 2D array that indicates what pixels in the input `image` should be used for sky computations (``1``) and which pixels should **not** be used for sky computations (``0``). id : anything The value of this parameter is simple stored within the `SkyImage` object. While it can be of any type, it is prefereble that `id` be of a type with nice string representation. skystat : callable, None, optional A callable object that takes a either a 2D image (2D `numpy.ndarray`) or a list of pixel values (a Nx1 array) and returns a tuple of two values: some statistics (e.g., mean, median, etc.) and number of pixels/values from the input image used in computing that statistics. When `skystat` is not set, `SkyImage` will use :py:class:`~jwst_pipeline.skymatch.skystatistics.SkyStats` object to perform sky statistics on image data. stepsize : int, None, optional Spacing between vertices of the image's bounding polygon. Default value of `None` creates bounding polygons with four vertices corresponding to the corners of the image. meta : dict, None, optional A dictionary of various items to be stored within the `SkyImage` object. """ self.image = image self.convf = convf self.meta = meta self._id = id self._pix_area = pix_area # WCS self.wcs_fwd = wcs_fwd self.wcs_inv = wcs_inv # initial sky value: self._sky = 0.0 # check that mask has the same shape as image: if mask is None: self.mask = None else: if image is None: raise ValueError("'mask' must be None when 'image' is None") self.mask = np.asanyarray(mask, dtype=np.bool) if self.mask.shape != image.shape: raise ValueError("'mask' must have the same shape as 'image'.") # create spherical polygon bounding the image if image is None or wcs_fwd is None or wcs_inv is None: self._radec = [(np.array([]), np.array([]))] self._polygon = SphericalPolygon([]) self._poly_area = 0.0 else: self.calc_bounding_polygon(stepsize) # set sky statistics function (NOTE: it must return statistics and # the number of pixels used after clipping) if skystat is None: self.set_builtin_skystat() else: self.skystat = skystat
class SkyImage: """ Container that holds information about properties of a *single* image such as: * image data; * WCS of the chip image; * bounding spherical polygon; * id; * pixel area; * sky background value; * sky statistics parameters; * mask associated image data indicating "good" (1) data. """ def __init__(self, image, wcs_fwd, wcs_inv, pix_area=1.0, convf=1.0, mask=None, id=None, skystat=None, stepsize=None, meta=None): """ Initializes the SkyImage object. Parameters ---------- image : numpy.ndarray A 2D array of image data. wcs_fwd : function "forward" pixel-to-world transformation function. wcs_inv : function "inverse" world-to-pixel transformation function. pix_area : float, optional Average pixel's sky area. convf : float, optional Conversion factor that when multiplied to `image` data converts the data to "uniform" (across multiple images) surface brightness units. .. note:: The functionality to support this conversion is not yet implemented and at this moment `convf` is ignored. mask : numpy.ndarray A 2D array that indicates what pixels in the input `image` should be used for sky computations (``1``) and which pixels should **not** be used for sky computations (``0``). id : anything The value of this parameter is simple stored within the `SkyImage` object. While it can be of any type, it is prefereble that `id` be of a type with nice string representation. skystat : callable, None, optional A callable object that takes a either a 2D image (2D `numpy.ndarray`) or a list of pixel values (a Nx1 array) and returns a tuple of two values: some statistics (e.g., mean, median, etc.) and number of pixels/values from the input image used in computing that statistics. When `skystat` is not set, `SkyImage` will use :py:class:`~jwst_pipeline.skymatch.skystatistics.SkyStats` object to perform sky statistics on image data. stepsize : int, None, optional Spacing between vertices of the image's bounding polygon. Default value of `None` creates bounding polygons with four vertices corresponding to the corners of the image. meta : dict, None, optional A dictionary of various items to be stored within the `SkyImage` object. """ self.image = image self.convf = convf self.meta = meta self._id = id self._pix_area = pix_area # WCS self.wcs_fwd = wcs_fwd self.wcs_inv = wcs_inv # initial sky value: self._sky = 0.0 # check that mask has the same shape as image: if mask is None: self.mask = None else: if image is None: raise ValueError("'mask' must be None when 'image' is None") self.mask = np.asanyarray(mask, dtype=np.bool) if self.mask.shape != image.shape: raise ValueError("'mask' must have the same shape as 'image'.") # create spherical polygon bounding the image if image is None or wcs_fwd is None or wcs_inv is None: self._radec = [(np.array([]), np.array([]))] self._polygon = SphericalPolygon([]) self._poly_area = 0.0 else: self.calc_bounding_polygon(stepsize) # set sky statistics function (NOTE: it must return statistics and # the number of pixels used after clipping) if skystat is None: self.set_builtin_skystat() else: self.skystat = skystat @property def id(self): """ Set or get `SkyImage`'s `id`. While `id` can be of any type, it is prefereble that `id` be of a type with nice string representation. """ return self._id @id.setter def id(self, id): self._id = id @property def pix_area(self): """ Set or get mean pixel area. """ return self._pix_area @pix_area.setter def pix_area(self, pix_area): self._pix_area = pix_area @property def poly_area(self): """ Get bounding polygon area in srad units. """ return self._poly_area @property def sky(self): """ Sky background value. See `calc_sky` for more details. """ return self._sky @sky.setter def sky(self, sky): self._sky = sky @property def radec(self): """ Get RA and DEC of the verteces of the bounding polygon as a `~numpy.ndarray` of shape (N, 2) where N is the number of verteces + 1. """ return self._radec @property def polygon(self): """ Get image's bounding polygon. """ return self._polygon def intersection(self, skyimage): """ Compute intersection of this `SkyImage` object and another `SkyImage`, `SkyGroup`, or :py:class:`~spherical_geometry.polygon.SphericalPolygon` object. Parameters ---------- skyimage : SkyImage, SkyGroup, SphericalPolygon Another object that should be intersected with this `SkyImage`. Returns ------- polygon : SphericalPolygon A :py:class:`~spherical_geometry.polygon.SphericalPolygon` that is the intersection of this `SkyImage` and `skyimage`. """ if isinstance(skyimage, (SkyImage, SkyGroup)): return self._polygon.intersection(skyimage.polygon) else: return self._polygon.intersection(skyimage) def calc_bounding_polygon(self, stepsize=None): """ Compute image's bounding polygon. Parameters ---------- stepsize : int, None, optional Indicates the maximum separation between two adjacent vertices of the bounding polygon along each side of the image. Corners of the image are included automatically. If `stepsize` is `None`, bounding polygon will contain only vertices of the image. """ ny, nx = self.image.shape if stepsize is None: nintx = 2 ninty = 2 else: nintx = max(2, int(np.ceil((nx + 1.0) / stepsize))) ninty = max(2, int(np.ceil((ny + 1.0) / stepsize))) xs = np.linspace(-0.5, nx - 0.5, nintx, dtype=np.float) ys = np.linspace(-0.5, ny - 0.5, ninty, dtype=np.float)[1:-1] nptx = xs.size npty = ys.size npts = 2 * (nptx + npty) borderx = np.empty((npts + 1, ), dtype=np.float) bordery = np.empty((npts + 1, ), dtype=np.float) # "bottom" points: borderx[:nptx] = xs bordery[:nptx] = -0.5 # "right" sl = np.s_[nptx:nptx + npty] borderx[sl] = nx - 0.5 bordery[sl] = ys # "top" sl = np.s_[nptx + npty:2 * nptx + npty] borderx[sl] = xs[::-1] bordery[sl] = ny - 0.5 # "left" sl = np.s_[2 * nptx + npty:-1] borderx[sl] = -0.5 bordery[sl] = ys[::-1] # close polygon: borderx[-1] = borderx[0] bordery[-1] = bordery[0] ra, dec = self.wcs_fwd(borderx, bordery, with_bounding_box=False) # TODO: for strange reasons, occasionally ra[0] != ra[-1] and/or # dec[0] != dec[-1] (even though we close the polygon in the # previous two lines). Then SphericalPolygon fails because # points are not closed. Threfore we force it to be closed: ra[-1] = ra[0] dec[-1] = dec[0] self._radec = [(ra, dec)] self._polygon = SphericalPolygon.from_radec(ra, dec) self._poly_area = np.fabs(self._polygon.area()) @property def skystat(self): """ Stores/retrieves a callable object that takes a either a 2D image (2D `numpy.ndarray`) or a list of pixel values (a Nx1 array) and returns a tuple of two values: some statistics (e.g., mean, median, etc.) and number of pixels/values from the input image used in computing that statistics. When `skystat` is not set, `SkyImage` will use :py:class:`~jwst_pipeline.skymatch.skystatistics.SkyStats` object to perform sky statistics on image data. """ return self._skystat @skystat.setter def skystat(self, skystat): self._skystat = skystat def set_builtin_skystat(self, skystat='median', lower=None, upper=None, nclip=5, lsigma=4.0, usigma=4.0, binwidth=0.1): """ Replace already set `skystat` with a "built-in" version of a statistics callable object used to measure sky background. See :py:class:`~jwst_pipeline.skymatch.skystatistics.SkyStats` for the parameter description. """ self._skystat = SkyStats(skystat=skystat, lower=lower, upper=upper, nclip=nclip, lsig=lsigma, usig=usigma, binwidth=binwidth) # TODO: due to a bug in the sphere package, see # https://github.com/spacetelescope/sphere/issues/74 # intersections with polygons formed as union does not work. # For this reason I re-implement 'calc_sky' below with # a workaround for the bug. # The original implementation (now called ``_calc_sky_orig`` # should replace current 'calc_sky' once the bug is fixed. # def calc_sky(self, overlap=None, delta=True): """ Compute sky background value. Parameters ---------- overlap : SkyImage, SkyGroup, SphericalPolygon, list of tuples, \ None, optional Another `SkyImage`, `SkyGroup`, :py:class:`spherical_geometry.polygons.SphericalPolygon`, or a list of tuples of (RA, DEC) of vertices of a spherical polygon. This parameter is used to indicate that sky statistics should computed only in the region of intersection of *this* image with the polygon indicated by `overlap`. When `overlap` is `None`, sky statistics will be computed over the entire image. delta : bool, optional Should this function return absolute sky value or the difference between the computed value and the value of the sky stored in the `sky` property. Returns ------- skyval : float, None Computed sky value (absolute or relative to the `sky` attribute). If there are no valid data to perform this computations (e.g., because this image does not overlap with the image indicated by `overlap`), `skyval` will be set to `None`. npix : int Number of pixels used to compute sky statistics. polyarea : float Area (in srad) of the polygon that bounds data used to compute sky statistics. """ if overlap is None: if self.mask is None: data = self.image else: data = self.image[self.mask] polyarea = self.poly_area else: fill_mask = np.zeros(self.image.shape, dtype=bool) if isinstance(overlap, SkyImage): intersection = self.intersection(overlap) polyarea = np.fabs(intersection.area()) radec = list(intersection.to_radec()) elif isinstance(overlap, SkyGroup): radec = [] polyarea = 0.0 for im in overlap: intersection = self.intersection(im) polyarea1 = np.fabs(intersection.area()) if polyarea1 == 0.0: continue polyarea += polyarea1 radec += list(intersection.to_radec()) elif isinstance(overlap, SphericalPolygon): radec = [] polyarea = 0.0 for p in overlap._polygons: intersection = self.intersection(SphericalPolygon([p])) polyarea1 = np.fabs(intersection.area()) if polyarea1 == 0.0: continue polyarea += polyarea1 radec += list(intersection.to_radec()) else: # assume a list of (ra, dec) tuples: radec = [] polyarea = 0.0 for r, d in overlap: poly = SphericalPolygon.from_radec(r, d) polyarea1 = np.fabs(poly.area()) if polyarea1 == 0.0 or len(r) < 4: continue polyarea += polyarea1 radec.append(self.intersection(poly).to_radec()) if polyarea == 0.0: return (None, 0, 0.0) for ra, dec in radec: if len(ra) < 4: continue # set pixels in 'fill_mask' that are inside a polygon to True: x, y = self.wcs_inv(ra, dec) poly_vert = list(zip(*[x, y])) polygon = region.Polygon(True, poly_vert) fill_mask = polygon.scan(fill_mask) if self.mask is not None: fill_mask &= self.mask data = self.image[fill_mask] if data.size < 1: return (None, 0, 0.0) # Calculate sky try: skyval, npix = self._skystat(data) except ValueError: return (None, 0, 0.0) if delta: skyval -= self._sky return skyval, npix, polyarea def _calc_sky_orig(self, overlap=None, delta=True): """ Compute sky background value. Parameters ---------- overlap : SkyImage, SkyGroup, SphericalPolygon, list of tuples, \ None, optional Another `SkyImage`, `SkyGroup`, :py:class:`spherical_geometry.polygons.SphericalPolygon`, or a list of tuples of (RA, DEC) of vertices of a spherical polygon. This parameter is used to indicate that sky statistics should computed only in the region of intersection of *this* image with the polygon indicated by `overlap`. When `overlap` is `None`, sky statistics will be computed over the entire image. delta : bool, optional Should this function return absolute sky value or the difference between the computed value and the value of the sky stored in the `sky` property. Returns ------- skyval : float, None Computed sky value (absolute or relative to the `sky` attribute). If there are no valid data to perform this computations (e.g., because this image does not overlap with the image indicated by `overlap`), `skyval` will be set to `None`. npix : int Number of pixels used to compute sky statistics. polyarea : float Area (in srad) of the polygon that bounds data used to compute sky statistics. """ if overlap is None: if self.mask is None: data = self.image else: data = self.image[self.mask] polyarea = self.poly_area else: fill_mask = np.zeros(self.image.shape, dtype=bool) if isinstance(overlap, (SkyImage, SkyGroup, SphericalPolygon)): intersection = self.intersection(overlap) polyarea = np.fabs(intersection.area()) radec = intersection.to_radec() else: # assume a list of (ra, dec) tuples: radec = [] polyarea = 0.0 for r, d in overlap: poly = SphericalPolygon.from_radec(r, d) polyarea1 = np.fabs(poly.area()) if polyarea1 == 0.0 or len(r) < 4: continue polyarea += polyarea1 radec.append(self.intersection(poly).to_radec()) if polyarea == 0.0: return (None, 0, 0.0) for ra, dec in radec: if len(ra) < 4: continue # set pixels in 'fill_mask' that are inside a polygon to True: x, y = self.wcs_inv(ra, dec) poly_vert = list(zip(*[x, y])) polygon = region.Polygon(True, poly_vert) fill_mask = polygon.scan(fill_mask) if self.mask is not None: fill_mask &= self.mask data = self.image[fill_mask] if data.size < 1: return (None, 0, 0.0) # Calculate sky try: skyval, npix = self._skystat(data) except ValueError: return (None, 0, 0.0) if delta: skyval -= self._sky return skyval, npix, polyarea def copy(self): """ Return a shallow copy of the `SkyImage` object. """ si = SkyImage(image=None, wcs_fwd=self.wcs_fwd, wcs_inv=self.wcs_inv, pix_area=self.pix_area, convf=self.convf, mask=None, id=self.id, stepsize=None, meta=self.meta) si.image = self.image si.mask = self.mask si._radec = self._radec si._polygon = self._polygon si._poly_area = self._poly_area si.sky = self.sky return si
def calc_bounding_polygon(self, stepsize=None): """ Compute image's bounding polygon. Parameters ---------- stepsize : int, None, optional Indicates the maximum separation between two adjacent vertices of the bounding polygon along each side of the image. Corners of the image are included automatically. If `stepsize` is `None`, bounding polygon will contain only vertices of the image. """ ny, nx = self.image.shape if stepsize is None: nintx = 2 ninty = 2 else: nintx = max(2, int(np.ceil((nx + 1.0) / stepsize))) ninty = max(2, int(np.ceil((ny + 1.0) / stepsize))) xs = np.linspace(-0.5, nx - 0.5, nintx, dtype=np.float) ys = np.linspace(-0.5, ny - 0.5, ninty, dtype=np.float)[1:-1] nptx = xs.size npty = ys.size npts = 2 * (nptx + npty) borderx = np.empty((npts + 1, ), dtype=np.float) bordery = np.empty((npts + 1, ), dtype=np.float) # "bottom" points: borderx[:nptx] = xs bordery[:nptx] = -0.5 # "right" sl = np.s_[nptx:nptx + npty] borderx[sl] = nx - 0.5 bordery[sl] = ys # "top" sl = np.s_[nptx + npty:2 * nptx + npty] borderx[sl] = xs[::-1] bordery[sl] = ny - 0.5 # "left" sl = np.s_[2 * nptx + npty:-1] borderx[sl] = -0.5 bordery[sl] = ys[::-1] # close polygon: borderx[-1] = borderx[0] bordery[-1] = bordery[0] ra, dec = self.wcs_fwd(borderx, bordery, with_bounding_box=False) # TODO: for strange reasons, occasionally ra[0] != ra[-1] and/or # dec[0] != dec[-1] (even though we close the polygon in the # previous two lines). Then SphericalPolygon fails because # points are not closed. Threfore we force it to be closed: ra[-1] = ra[0] dec[-1] = dec[0] self._radec = [(ra, dec)] self._polygon = SphericalPolygon.from_radec(ra, dec) self._poly_area = np.fabs(self._polygon.area())
class SkyImage: """ Container that holds information about properties of a *single* image such as: * image data; * WCS of the chip image; * bounding spherical polygon; * id; * pixel area; * sky background value; * sky statistics parameters; * mask associated image data indicating "good" (1) data. """ def __init__(self, image, wcs_fwd, wcs_inv, pix_area=1.0, convf=1.0, mask=None, id=None, skystat=None, stepsize=None, meta=None): """ Initializes the SkyImage object. Parameters ---------- image : numpy.ndarray A 2D array of image data. wcs_fwd : function "forward" pixel-to-world transformation function. wcs_inv : function "inverse" world-to-pixel transformation function. pix_area : float, optional Average pixel's sky area. convf : float, optional Conversion factor that when multiplied to `image` data converts the data to "uniform" (across multiple images) surface brightness units. .. note:: The functionality to support this conversion is not yet implemented and at this moment `convf` is ignored. mask : numpy.ndarray A 2D array that indicates what pixels in the input `image` should be used for sky computations (``1``) and which pixels should **not** be used for sky computations (``0``). id : anything The value of this parameter is simple stored within the `SkyImage` object. While it can be of any type, it is prefereble that `id` be of a type with nice string representation. skystat : callable, None, optional A callable object that takes a either a 2D image (2D `numpy.ndarray`) or a list of pixel values (a Nx1 array) and returns a tuple of two values: some statistics (e.g., mean, median, etc.) and number of pixels/values from the input image used in computing that statistics. When `skystat` is not set, `SkyImage` will use :py:class:`~jwst_pipeline.skymatch.skystatistics.SkyStats` object to perform sky statistics on image data. stepsize : int, None, optional Spacing between vertices of the image's bounding polygon. Default value of `None` creates bounding polygons with four vertices corresponding to the corners of the image. meta : dict, None, optional A dictionary of various items to be stored within the `SkyImage` object. """ self.image = image self.convf = convf self.meta = meta self._id = id self._pix_area = pix_area # WCS self.wcs_fwd = wcs_fwd self.wcs_inv = wcs_inv # initial sky value: self._sky = 0.0 self._sky_is_valid = False # check that mask has the same shape as image: if mask is None: self.mask = None else: if image is None: raise ValueError("'mask' must be None when 'image' is None") self.mask = np.asanyarray(mask, dtype=np.bool) if self.mask.shape != image.shape: raise ValueError("'mask' must have the same shape as 'image'.") # create spherical polygon bounding the image if image is None or wcs_fwd is None or wcs_inv is None: self._radec = [(np.array([]), np.array([]))] self._polygon = SphericalPolygon([]) self._poly_area = 0.0 else: self.calc_bounding_polygon(stepsize) # set sky statistics function (NOTE: it must return statistics and # the number of pixels used after clipping) if skystat is None: self.set_builtin_skystat() else: self.skystat = skystat @property def id(self): """ Set or get `SkyImage`'s `id`. While `id` can be of any type, it is prefereble that `id` be of a type with nice string representation. """ return self._id @id.setter def id(self, id): self._id = id @property def pix_area(self): """ Set or get mean pixel area. """ return self._pix_area @pix_area.setter def pix_area(self, pix_area): self._pix_area = pix_area @property def poly_area(self): """ Get bounding polygon area in srad units. """ return self._poly_area @property def sky(self): """ Sky background value. See `calc_sky` for more details. """ return self._sky @sky.setter def sky(self, sky): self._sky = sky @property def is_sky_valid(self): """ Indicates whether sky value was successfully computed. Must be set externally. """ return self._sky_is_valid @is_sky_valid.setter def is_sky_valid(self, valid): self._sky_is_valid = valid @property def radec(self): """ Get RA and DEC of the verteces of the bounding polygon as a `~numpy.ndarray` of shape (N, 2) where N is the number of verteces + 1. """ return self._radec @property def polygon(self): """ Get image's bounding polygon. """ return self._polygon def intersection(self, skyimage): """ Compute intersection of this `SkyImage` object and another `SkyImage`, `SkyGroup`, or :py:class:`~spherical_geometry.polygon.SphericalPolygon` object. Parameters ---------- skyimage : SkyImage, SkyGroup, SphericalPolygon Another object that should be intersected with this `SkyImage`. Returns ------- polygon : SphericalPolygon A :py:class:`~spherical_geometry.polygon.SphericalPolygon` that is the intersection of this `SkyImage` and `skyimage`. """ if isinstance(skyimage, (SkyImage, SkyGroup)): return self._polygon.intersection(skyimage.polygon) else: return self._polygon.intersection(skyimage) def calc_bounding_polygon(self, stepsize=None): """ Compute image's bounding polygon. Parameters ---------- stepsize : int, None, optional Indicates the maximum separation between two adjacent vertices of the bounding polygon along each side of the image. Corners of the image are included automatically. If `stepsize` is `None`, bounding polygon will contain only vertices of the image. """ ny, nx = self.image.shape if stepsize is None: nintx = 2 ninty = 2 else: nintx = max(2, int(np.ceil((nx + 1.0) / stepsize))) ninty = max(2, int(np.ceil((ny + 1.0) / stepsize))) xs = np.linspace(-0.5, nx - 0.5, nintx, dtype=np.float) ys = np.linspace(-0.5, ny - 0.5, ninty, dtype=np.float)[1:-1] nptx = xs.size npty = ys.size npts = 2 * (nptx + npty) borderx = np.empty((npts + 1,), dtype=np.float) bordery = np.empty((npts + 1,), dtype=np.float) # "bottom" points: borderx[:nptx] = xs bordery[:nptx] = -0.5 # "right" sl = np.s_[nptx:nptx + npty] borderx[sl] = nx - 0.5 bordery[sl] = ys # "top" sl = np.s_[nptx + npty:2 * nptx + npty] borderx[sl] = xs[::-1] bordery[sl] = ny - 0.5 # "left" sl = np.s_[2 * nptx + npty:-1] borderx[sl] = -0.5 bordery[sl] = ys[::-1] # close polygon: borderx[-1] = borderx[0] bordery[-1] = bordery[0] ra, dec = self.wcs_fwd(borderx, bordery, with_bounding_box=False) # TODO: for strange reasons, occasionally ra[0] != ra[-1] and/or # dec[0] != dec[-1] (even though we close the polygon in the # previous two lines). Then SphericalPolygon fails because # points are not closed. Threfore we force it to be closed: ra[-1] = ra[0] dec[-1] = dec[0] self._radec = [(ra, dec)] self._polygon = SphericalPolygon.from_radec(ra, dec) self._poly_area = np.fabs(self._polygon.area()) @property def skystat(self): """ Stores/retrieves a callable object that takes a either a 2D image (2D `numpy.ndarray`) or a list of pixel values (a Nx1 array) and returns a tuple of two values: some statistics (e.g., mean, median, etc.) and number of pixels/values from the input image used in computing that statistics. When `skystat` is not set, `SkyImage` will use :py:class:`~jwst_pipeline.skymatch.skystatistics.SkyStats` object to perform sky statistics on image data. """ return self._skystat @skystat.setter def skystat(self, skystat): self._skystat = skystat def set_builtin_skystat(self, skystat='median', lower=None, upper=None, nclip=5, lsigma=4.0, usigma=4.0, binwidth=0.1): """ Replace already set `skystat` with a "built-in" version of a statistics callable object used to measure sky background. See :py:class:`~jwst_pipeline.skymatch.skystatistics.SkyStats` for the parameter description. """ self._skystat = SkyStats( skystat=skystat, lower=lower, upper=upper, nclip=nclip, lsig=lsigma, usig=usigma, binwidth=binwidth ) # TODO: due to a bug in the sphere package, see # https://github.com/spacetelescope/sphere/issues/74 # intersections with polygons formed as union does not work. # For this reason I re-implement 'calc_sky' below with # a workaround for the bug. # The original implementation (now called ``_calc_sky_orig`` # should replace current 'calc_sky' once the bug is fixed. # def calc_sky(self, overlap=None, delta=True): """ Compute sky background value. Parameters ---------- overlap : SkyImage, SkyGroup, SphericalPolygon, list of tuples, \ None, optional Another `SkyImage`, `SkyGroup`, :py:class:`spherical_geometry.polygons.SphericalPolygon`, or a list of tuples of (RA, DEC) of vertices of a spherical polygon. This parameter is used to indicate that sky statistics should computed only in the region of intersection of *this* image with the polygon indicated by `overlap`. When `overlap` is `None`, sky statistics will be computed over the entire image. delta : bool, optional Should this function return absolute sky value or the difference between the computed value and the value of the sky stored in the `sky` property. Returns ------- skyval : float, None Computed sky value (absolute or relative to the `sky` attribute). If there are no valid data to perform this computations (e.g., because this image does not overlap with the image indicated by `overlap`), `skyval` will be set to `None`. npix : int Number of pixels used to compute sky statistics. polyarea : float Area (in srad) of the polygon that bounds data used to compute sky statistics. """ if overlap is None: if self.mask is None: data = self.image else: data = self.image[self.mask] polyarea = self.poly_area else: fill_mask = np.zeros(self.image.shape, dtype=bool) if isinstance(overlap, SkyImage): intersection = self.intersection(overlap) polyarea = np.fabs(intersection.area()) radec = list(intersection.to_radec()) elif isinstance(overlap, SkyGroup): radec = [] polyarea = 0.0 for im in overlap: intersection = self.intersection(im) polyarea1 = np.fabs(intersection.area()) if polyarea1 == 0.0: continue polyarea += polyarea1 radec += list(intersection.to_radec()) elif isinstance(overlap, SphericalPolygon): radec = [] polyarea = 0.0 for p in overlap._polygons: intersection = self.intersection(SphericalPolygon([p])) polyarea1 = np.fabs(intersection.area()) if polyarea1 == 0.0: continue polyarea += polyarea1 radec += list(intersection.to_radec()) else: # assume a list of (ra, dec) tuples: radec = [] polyarea = 0.0 for r, d in overlap: poly = SphericalPolygon.from_radec(r, d) polyarea1 = np.fabs(poly.area()) if polyarea1 == 0.0 or len(r) < 4: continue polyarea += polyarea1 radec.append(self.intersection(poly).to_radec()) if polyarea == 0.0: return (None, 0, 0.0) for ra, dec in radec: if len(ra) < 4: continue # set pixels in 'fill_mask' that are inside a polygon to True: x, y = self.wcs_inv(ra, dec) poly_vert = list(zip(*[x, y])) polygon = region.Polygon(True, poly_vert) fill_mask = polygon.scan(fill_mask) if self.mask is not None: fill_mask &= self.mask data = self.image[fill_mask] if data.size < 1: return (None, 0, 0.0) # Calculate sky try: skyval, npix = self._skystat(data) except ValueError: return (None, 0, 0.0) if delta: skyval -= self._sky return skyval, npix, polyarea def _calc_sky_orig(self, overlap=None, delta=True): """ Compute sky background value. Parameters ---------- overlap : SkyImage, SkyGroup, SphericalPolygon, list of tuples, \ None, optional Another `SkyImage`, `SkyGroup`, :py:class:`spherical_geometry.polygons.SphericalPolygon`, or a list of tuples of (RA, DEC) of vertices of a spherical polygon. This parameter is used to indicate that sky statistics should computed only in the region of intersection of *this* image with the polygon indicated by `overlap`. When `overlap` is `None`, sky statistics will be computed over the entire image. delta : bool, optional Should this function return absolute sky value or the difference between the computed value and the value of the sky stored in the `sky` property. Returns ------- skyval : float, None Computed sky value (absolute or relative to the `sky` attribute). If there are no valid data to perform this computations (e.g., because this image does not overlap with the image indicated by `overlap`), `skyval` will be set to `None`. npix : int Number of pixels used to compute sky statistics. polyarea : float Area (in srad) of the polygon that bounds data used to compute sky statistics. """ if overlap is None: if self.mask is None: data = self.image else: data = self.image[self.mask] polyarea = self.poly_area else: fill_mask = np.zeros(self.image.shape, dtype=bool) if isinstance(overlap, (SkyImage, SkyGroup, SphericalPolygon)): intersection = self.intersection(overlap) polyarea = np.fabs(intersection.area()) radec = intersection.to_radec() else: # assume a list of (ra, dec) tuples: radec = [] polyarea = 0.0 for r, d in overlap: poly = SphericalPolygon.from_radec(r, d) polyarea1 = np.fabs(poly.area()) if polyarea1 == 0.0 or len(r) < 4: continue polyarea += polyarea1 radec.append(self.intersection(poly).to_radec()) if polyarea == 0.0: return (None, 0, 0.0) for ra, dec in radec: if len(ra) < 4: continue # set pixels in 'fill_mask' that are inside a polygon to True: x, y = self.wcs_inv(ra, dec) poly_vert = list(zip(*[x, y])) polygon = region.Polygon(True, poly_vert) fill_mask = polygon.scan(fill_mask) if self.mask is not None: fill_mask &= self.mask data = self.image[fill_mask] if data.size < 1: return (None, 0, 0.0) # Calculate sky try: skyval, npix = self._skystat(data) except ValueError: return (None, 0, 0.0) if delta: skyval -= self._sky return skyval, npix, polyarea def copy(self): """ Return a shallow copy of the `SkyImage` object. """ si = SkyImage( image=None, wcs_fwd=self.wcs_fwd, wcs_inv=self.wcs_inv, pix_area=self.pix_area, convf=self.convf, mask=None, id=self.id, stepsize=None, meta=self.meta ) si.image = self.image si.mask = self.mask si._radec = self._radec si._polygon = self._polygon si._poly_area = self._poly_area si.sky = self.sky return si
def _skycoord_to_spherical_polygon(skycoord): return SphericalPolygon.from_radec(skycoord.spherical.lon.degree, skycoord.spherical.lat.degree, degrees=True)
def __init__(self, ra, dec, inside=None, max_depth=10): ra = ra.to(u.rad).value dec = dec.to(u.rad).value # Check if the vertices form a closed polygon if ra[0] != ra[-1] or dec[0] != dec[-1]: # If not, append the first vertex to ``vertices`` ra = np.append(ra, ra[0]) dec = np.append(dec, dec[0]) vertices = SkyCoord(ra=ra, dec=dec, unit="rad", frame="icrs") if inside: # Convert it to (x, y, z) cartesian coordinates on the sphere inside = (inside.icrs.ra.rad, inside.icrs.dec.rad) self.polygon = SphericalPolygon.from_lonlat(lon=ra, lat=dec, center=inside, degrees=False) start_depth, ipixels = self._get_starting_depth() end_depth = max_depth # When the start depth returned is > to the depth requested # For that specific case, we only do one iteration at start_depth # Thus the MOC will contain the partially intersecting cells with the # contained ones at start_depth # And we degrade the MOC to the max_depth self.degrade_to_max_depth = False if start_depth > end_depth: end_depth = start_depth self.degrade_to_max_depth = True self.ipix_d = {str(order): [] for order in range(start_depth, end_depth + 1)} ## Iterative version of the algorithm: seems a bit faster than the recursive one for depth in range(start_depth, end_depth + 1): # Define a HEALPix at the current depth hp = HEALPix(nside=(1 << depth), order='nested', frame=ICRS()) # Get the lon and lat of the corners of the pixels # intersecting the polygon lon, lat = hp.boundaries_lonlat(ipixels, step=1) lon = lon.to(u.rad).value lat = lat.to(u.rad).value # closes the lon and lat array so that their first and last value matches lon = self._closes_numpy_2d_array(lon) lat = self._closes_numpy_2d_array(lat) num_ipix_inter_poly = ipixels.shape[0] # Define a 3d numpy array containing the corners coordinates of the intersecting pixels # The first dim is the num of ipixels # The second is the number of coordinates (5 as it defines the closed polygon of a HEALPix cell) # The last is of size 2 (lon and lat) shapes = np.vstack((lon.ravel(), lat.ravel())).T.reshape(num_ipix_inter_poly, 5, -1) ipix_in_polygon_l = [] ipix_inter_polygon_l = [] for i in range(num_ipix_inter_poly): shape = shapes[i] # Definition of a SphericalPolygon from the border coordinates of a HEALPix cell ipix_shape = SphericalPolygon.from_radec(lon=shape[:, 0], lat=shape[:, 1], degrees=False) ipix = ipixels[i] if self.polygon.intersects_poly(ipix_shape): # If we are at the max depth then we direcly add to the MOC the intersecting ipixels if depth == end_depth: ipix_in_polygon_l.append(ipix) else: # Check whether polygon contains ipix or not if self.polygon_contains_ipix(ipix_shape): ipix_in_polygon_l.append(ipix) else: # The ipix is just intersecting without being contained in the polygon # We split it in its 4 children child_ipix = ipix << 2 ipix_inter_polygon_l.extend([child_ipix, child_ipix + 1, child_ipix + 2, child_ipix + 3]) self.ipix_d.update({str(depth): ipix_in_polygon_l}) ipixels = np.asarray(ipix_inter_polygon_l)
def calc_bounding_polygon(self, stepsize=None): """ Compute image's bounding polygon. Parameters ---------- stepsize : int, None, optional Indicates the maximum separation between two adjacent vertices of the bounding polygon along each side of the image. Corners of the image are included automatically. If `stepsize` is `None`, bounding polygon will contain only vertices of the image. """ ny, nx = self.image.shape if stepsize is None: nintx = 2 ninty = 2 else: nintx = max(2, int(np.ceil((nx + 1.0) / stepsize))) ninty = max(2, int(np.ceil((ny + 1.0) / stepsize))) xs = np.linspace(-0.5, nx - 0.5, nintx, dtype=np.float) ys = np.linspace(-0.5, ny - 0.5, ninty, dtype=np.float)[1:-1] nptx = xs.size npty = ys.size npts = 2 * (nptx + npty) borderx = np.empty((npts + 1,), dtype=np.float) bordery = np.empty((npts + 1,), dtype=np.float) # "bottom" points: borderx[:nptx] = xs bordery[:nptx] = -0.5 # "right" sl = np.s_[nptx:nptx + npty] borderx[sl] = nx - 0.5 bordery[sl] = ys # "top" sl = np.s_[nptx + npty:2 * nptx + npty] borderx[sl] = xs[::-1] bordery[sl] = ny - 0.5 # "left" sl = np.s_[2 * nptx + npty:-1] borderx[sl] = -0.5 bordery[sl] = ys[::-1] # close polygon: borderx[-1] = borderx[0] bordery[-1] = bordery[0] ra, dec = self.wcs_fwd(borderx, bordery, with_bounding_box=False) # TODO: for strange reasons, occasionally ra[0] != ra[-1] and/or # dec[0] != dec[-1] (even though we close the polygon in the # previous two lines). Then SphericalPolygon fails because # points are not closed. Threfore we force it to be closed: ra[-1] = ra[0] dec[-1] = dec[0] self._radec = [(ra, dec)] self._polygon = SphericalPolygon.from_radec(ra, dec) self._poly_area = np.fabs(self._polygon.area())
def __init__(self, image, ext, dq_bits=0, dqimage=None, dqext=None, usermask=None, usermask_ext=None): """ Parameters ---------- image : ImageRef An :py:class:`~stsci.skypac.utils.ImageRef` object that refers to an open FITS file ext : tuple, int, str Extension specification in the `image` the `SkyLineMember` object will be associated with. An int `ext` specifies extension number. A tuple in the form (str, int) specifies extension name and number. A string `ext` specifies extension name and the extension version is assumed to be 1. See documentation for `astropy.io.fits.getData` for examples. dq_bits : int, None (Default = 0) Integer sum of all the DQ bit values from the input `image`'s DQ array that should be considered "good" when building masks for sky computations. For example, if pixels in the DQ array can be combinations of 1, 2, 4, and 8 flags and one wants to consider DQ "defects" having flags 2 and 4 as being acceptable for sky computations, then `dq_bits` should be set to 2+4=6. Then a DQ pixel having values 2,4, or 6 will be considered a good pixel, while a DQ pixel with a value, e.g., 1+2=3, 4+8=12, etc. will be flagged as a "bad" pixel. | Default value (0) will make *all* non-zero pixels in the DQ mask to be considered "bad" pixels, and the corresponding image pixels will not be used for sky computations. | Set `dq_bits` to `None` to turn off the use of image's DQ array for sky computations. .. note:: DQ masks (if used), *will be* combined with user masks specified by the `usermask` parameter. dqimage : ImageRef An :py:class:`~stsci.skypac.utils.ImageRef` object that refers to an open FITS file that has DQ data of the input `image`. .. note:: When DQ data are located in the same FITS file as the science image data (e.g., HST/ACS, HST/WFC3, etc.), `dqimage` may point to the same :py:class:`~stsci.skypac.utils.ImageRef` object. In this case the reference count of the \ :py:class:`~stsci.skypac.utils.ImageRef` object must be increased adequately. dqext : tuple, int, str Extension specification of the `dqimage` that contains `image`'s DQ information. See help for `ext` for more details on acceptable formats for this parameter. usermask : ImageRef An :py:class:`~stsci.skypac.utils.ImageRef` object that refers to an open FITS file that has user mask data that indicate what pixels in the input `image` should be used for sky computations (``1``) and which pixels should **not** be used for sky computations (``0``). usermask_ext : tuple, int, str Extension specification of the `usermask` mask file that contains user's mask data that should be associated with the input `image` and `ext`. See help for `ext` for more details on acceptable formats for this parameter. """ assert(hasattr(self.__class__, '_initialized') and \ self.__class__._initialized) self._reset() # check that input images and extensions are valid -- # either integers or tuples of strings and integers, e.g., ('sci',1): _check_valid_imgext(image, 'image', ext, 'ext', can_img_be_None=False) if dq_bits is not None: if dqimage is None: dq_bits = 0 else: _check_valid_imgext(dqimage, 'dqimage', dqext, 'dqext') _check_valid_imgext(usermask, 'usermask', usermask_ext,'usermask_ext') # get telescope, instrument, and detector info: self.telescope, self.instrument, self.detector = get_instrument_info( image, ext) # check dq_bits: if dq_bits is not None and not isinstance(dq_bits,int): if image: dqimage.release() if usermask: usermask.release() if dqimage: dqimage.release() raise TypeError("Argument 'dq_bits' must be either an integer or None.") # buld mask: self._buildMask(image.original_fname, ext, dq_bits, dqimage, dqext, usermask, usermask_ext) if dqimage: dqimage.release() if usermask: usermask.release() # save file, user mask, and DQ extension info: self._fname = image.original_fname self._basefname = basename(self._fname) self._image = image self._ext = ext self._can_free_image = image.can_reload_data and self.optimize != 'speed' # check extension and create a string representation: try: extstr = ext2str(ext) except ValueError: raise ValueError("Unexpected extension type \'{}\' for file {}.".\ format(ext,self._basefname)) self._id = "{:s}[{:s}]".format(self._basefname, extstr) # extract WCS for bounding-box computation try: if hasattr(image.hdu[ext], 'wcs'): self._wcs = image.hdu[ext].wcs else: if self.telescope in supported_telescopes: self._wcs = wcsutil.HSTWCS(image.hdu, ext) else: self._wcs = pywcs.WCS(image.hdu[ext].header, image.hdu) if self._wcs is None: raise Exception("Invalid WCS.") except: msg = "Unable to obtain WCS information for the file {:s}." \ .format(self._id) self._ml.error(msg) self._ml.flush() self._release_all() raise # determine pixel scale: self._get_pixel_scale() # see if image data are in counts or count-rate # and compute count(-rate) to flux (per arcsec^2) conversion factor: self._brightness_conv_from_hdu(image.hdu, self._idcscale) # process Sky user's keyword and its value: self._init_skyuser(image.hdu[ext].header) # Set polygon to be the bounding box of the chip: self._polygon = SphericalPolygon.from_wcs(self.wcs, steps=1)
class SkyGroup: """ Holds multiple :py:class:`SkyImage` objects whose sky background values must be adjusted together. `SkyGroup` provides methods for obtaining bounding polygon of the group of :py:class:`SkyImage` objects and to compute sky value of the group. """ def __init__(self, images, id=None, sky=0.0): if isinstance(images, SkyImage): self._images = [images] elif hasattr(images, '__iter__'): self._images = [] for im in images: if not isinstance(im, SkyImage): raise TypeError("Each element of the 'images' parameter " "must be an 'SkyImage' object.") self._images.append(im) else: raise TypeError( "Parameter 'images' must be either a single " "'SkyImage' object or a list of 'SkyImage' objects") self._id = id self._update_bounding_polygon() self._sky = sky for im in self._images: im.sky += sky @property def id(self): """ Set or get `SkyImage`'s `id`. While `id` can be of any type, it is prefereble that `id` be of a type with nice string representation. """ return self._id @id.setter def id(self, id): self._id = id @property def sky(self): """ Sky background value. See `calc_sky` for more details. """ return self._sky @sky.setter def sky(self, sky): delta_sky = sky - self._sky self._sky = sky for im in self._images: im.sky += delta_sky @property def radec(self): """ Get RA and DEC of the verteces of the bounding polygon as a `~numpy.ndarray` of shape (N, 2) where N is the number of verteces + 1. """ return self._radec @property def polygon(self): """ Get image's bounding polygon. """ return self._polygon def intersection(self, skyimage): """ Compute intersection of this `SkyImage` object and another `SkyImage`, `SkyGroup`, or :py:class:`~spherical_geometry.polygon.SphericalPolygon` object. Parameters ---------- skyimage : SkyImage, SkyGroup, SphericalPolygon Another object that should be intersected with this `SkyImage`. Returns ------- polygon : SphericalPolygon A :py:class:`~spherical_geometry.polygon.SphericalPolygon` that is the intersection of this `SkyImage` and `skyimage`. """ if isinstance(skyimage, (SkyImage, SkyGroup)): return self._polygon.intersection(skyimage.polygon) else: return self._polygon.intersection(skyimage) def _update_bounding_polygon(self): polygons = [im.polygon for im in self._images] if len(polygons) == 0: self._polygon = SphericalPolygon([]) self._radec = [] else: self._polygon = SphericalPolygon.multi_union(polygons) self._radec = list(self._polygon.to_radec()) def __len__(self): return len(self._images) def __getitem__(self, idx): return self._images[idx] def __setitem__(self, idx, value): if not isinstance(value, SkyImage): raise TypeError("Item must be of 'SkyImage' type") value.sky += self._sky self._images[idx] = value self._update_bounding_polygon() def __delitem__(self, idx): del self._images[idx] if len(self._images) == 0: self._sky = 0.0 self._id = None self._update_bounding_polygon() def __iter__(self): for image in self._images: yield image def insert(self, idx, value): """Inserts a `SkyImage` into the group. """ if not isinstance(value, SkyImage): raise TypeError("Item must be of 'SkyImage' type") value.sky += self._sky self._images.insert(idx, value) self._update_bounding_polygon() def append(self, value): """Appends a `SkyImage` to the group. """ if not isinstance(value, SkyImage): raise TypeError("Item must be of 'SkyImage' type") value.sky += self._sky self._images.append(value) self._update_bounding_polygon() def calc_sky(self, overlap=None, delta=True): """ Compute sky background value. Parameters ---------- overlap : SkyImage, SkyGroup, SphericalPolygon, list of tuples, \ None, optional Another `SkyImage`, `SkyGroup`, :py:class:`spherical_geometry.polygons.SphericalPolygon`, or a list of tuples of (RA, DEC) of vertices of a spherical polygon. This parameter is used to indicate that sky statistics should computed only in the region of intersection of *this* image with the polygon indicated by `overlap`. When `overlap` is `None`, sky statistics will be computed over the entire image. delta : bool, optional Should this function return absolute sky value or the difference between the computed value and the value of the sky stored in the `sky` property. Returns ------- skyval : float, None Computed sky value (absolute or relative to the `sky` attribute). If there are no valid data to perform this computations (e.g., because this image does not overlap with the image indicated by `overlap`), `skyval` will be set to `None`. npix : int Number of pixels used to compute sky statistics. polyarea : float Area (in srad) of the polygon that bounds data used to compute sky statistics. """ if len(self._images) == 0: return (None, 0, 0.0) wght = 0 area = 0.0 if overlap is None: # compute minimum sky across all images in the group: wsky = None for image in self._images: # make sure all images have the same background: image.background = self._sky sky, npix, imarea = image.calc_sky(overlap=None, delta=delta) if sky is None: continue if wsky is None or wsky > sky: wsky = sky wght = npix area = imarea return (wsky, wght, area) ################################################ ## compute weighted sky in various overlaps: ## ################################################ wsky = 0.0 for image in self._images: # make sure all images have the same background: image.background = self._sky sky, npix, area1 = image.calc_sky(overlap=overlap, delta=delta) area += area1 if sky is not None and npix > 0: pix_area = npix * image.pix_area wsky += sky * pix_area wght += pix_area if wght == 0.0 or area == 0.0: return (None, wght, area) else: return (wsky / wght, wght, area)
class SkyGroup: """ Holds multiple :py:class:`SkyImage` objects whose sky background values must be adjusted together. `SkyGroup` provides methods for obtaining bounding polygon of the group of :py:class:`SkyImage` objects and to compute sky value of the group. """ def __init__(self, images, id=None, sky=0.0): if isinstance(images, SkyImage): self._images = [images] elif hasattr(images, '__iter__'): self._images = [] for im in images: if not isinstance(im, SkyImage): raise TypeError("Each element of the 'images' parameter " "must be an 'SkyImage' object.") self._images.append(im) else: raise TypeError("Parameter 'images' must be either a single " "'SkyImage' object or a list of 'SkyImage' objects") self._id = id self._update_bounding_polygon() self._sky = sky for im in self._images: im.sky += sky @property def id(self): """ Set or get `SkyImage`'s `id`. While `id` can be of any type, it is prefereble that `id` be of a type with nice string representation. """ return self._id @id.setter def id(self, id): self._id = id @property def sky(self): """ Sky background value. See `calc_sky` for more details. """ return self._sky @sky.setter def sky(self, sky): delta_sky = sky - self._sky self._sky = sky for im in self._images: im.sky += delta_sky @property def radec(self): """ Get RA and DEC of the verteces of the bounding polygon as a `~numpy.ndarray` of shape (N, 2) where N is the number of verteces + 1. """ return self._radec @property def polygon(self): """ Get image's bounding polygon. """ return self._polygon def intersection(self, skyimage): """ Compute intersection of this `SkyImage` object and another `SkyImage`, `SkyGroup`, or :py:class:`~spherical_geometry.polygon.SphericalPolygon` object. Parameters ---------- skyimage : SkyImage, SkyGroup, SphericalPolygon Another object that should be intersected with this `SkyImage`. Returns ------- polygon : SphericalPolygon A :py:class:`~spherical_geometry.polygon.SphericalPolygon` that is the intersection of this `SkyImage` and `skyimage`. """ if isinstance(skyimage, (SkyImage, SkyGroup)): return self._polygon.intersection(skyimage.polygon) else: return self._polygon.intersection(skyimage) def _update_bounding_polygon(self): polygons = [im.polygon for im in self._images] if len(polygons) == 0: self._polygon = SphericalPolygon([]) self._radec = [] else: self._polygon = SphericalPolygon.multi_union(polygons) self._radec = list(self._polygon.to_radec()) def __len__(self): return len(self._images) def __getitem__(self, idx): return self._images[idx] def __setitem__(self, idx, value): if not isinstance(value, SkyImage): raise TypeError("Item must be of 'SkyImage' type") value.sky += self._sky self._images[idx] = value self._update_bounding_polygon() def __delitem__(self, idx): del self._images[idx] if len(self._images) == 0: self._sky = 0.0 self._id = None self._update_bounding_polygon() def __iter__(self): for image in self._images: yield image def insert(self, idx, value): """Inserts a `SkyImage` into the group. """ if not isinstance(value, SkyImage): raise TypeError("Item must be of 'SkyImage' type") value.sky += self._sky self._images.insert(idx, value) self._update_bounding_polygon() def append(self, value): """Appends a `SkyImage` to the group. """ if not isinstance(value, SkyImage): raise TypeError("Item must be of 'SkyImage' type") value.sky += self._sky self._images.append(value) self._update_bounding_polygon() def calc_sky(self, overlap=None, delta=True): """ Compute sky background value. Parameters ---------- overlap : SkyImage, SkyGroup, SphericalPolygon, list of tuples, \ None, optional Another `SkyImage`, `SkyGroup`, :py:class:`spherical_geometry.polygons.SphericalPolygon`, or a list of tuples of (RA, DEC) of vertices of a spherical polygon. This parameter is used to indicate that sky statistics should computed only in the region of intersection of *this* image with the polygon indicated by `overlap`. When `overlap` is `None`, sky statistics will be computed over the entire image. delta : bool, optional Should this function return absolute sky value or the difference between the computed value and the value of the sky stored in the `sky` property. Returns ------- skyval : float, None Computed sky value (absolute or relative to the `sky` attribute). If there are no valid data to perform this computations (e.g., because this image does not overlap with the image indicated by `overlap`), `skyval` will be set to `None`. npix : int Number of pixels used to compute sky statistics. polyarea : float Area (in srad) of the polygon that bounds data used to compute sky statistics. """ if len(self._images) == 0: return (None, 0, 0.0) wght = 0 area = 0.0 if overlap is None: # compute minimum sky across all images in the group: wsky = None for image in self._images: # make sure all images have the same background: image.background = self._sky sky, npix, imarea = image.calc_sky(overlap=None, delta=delta) if sky is None: continue if wsky is None or wsky > sky: wsky = sky wght = npix area = imarea return (wsky, wght, area) ################################################ ## compute weighted sky in various overlaps: ## ################################################ wsky = 0.0 for image in self._images: # make sure all images have the same background: image.background = self._sky sky, npix, area1 = image.calc_sky(overlap=overlap, delta=delta) area += area1 if sky is not None and npix > 0: pix_area = npix * image.pix_area wsky += sky * pix_area wght += pix_area if wght == 0.0 or area == 0.0: return (None, wght, area) else: return (wsky / wght, wght, area)
def __init__(self, image, wcs_fwd, wcs_inv, pix_area=1.0, convf=1.0, mask=None, id=None, skystat=None, stepsize=None, meta=None): """ Initializes the SkyImage object. Parameters ---------- image : numpy.ndarray A 2D array of image data. wcs_fwd : function "forward" pixel-to-world transformation function. wcs_inv : function "inverse" world-to-pixel transformation function. pix_area : float, optional Average pixel's sky area. convf : float, optional Conversion factor that when multiplied to `image` data converts the data to "uniform" (across multiple images) surface brightness units. .. note:: The functionality to support this conversion is not yet implemented and at this moment `convf` is ignored. mask : numpy.ndarray A 2D array that indicates what pixels in the input `image` should be used for sky computations (``1``) and which pixels should **not** be used for sky computations (``0``). id : anything The value of this parameter is simple stored within the `SkyImage` object. While it can be of any type, it is prefereble that `id` be of a type with nice string representation. skystat : callable, None, optional A callable object that takes a either a 2D image (2D `numpy.ndarray`) or a list of pixel values (a Nx1 array) and returns a tuple of two values: some statistics (e.g., mean, median, etc.) and number of pixels/values from the input image used in computing that statistics. When `skystat` is not set, `SkyImage` will use :py:class:`~jwst_pipeline.skymatch.skystatistics.SkyStats` object to perform sky statistics on image data. stepsize : int, None, optional Spacing between vertices of the image's bounding polygon. Default value of `None` creates bounding polygons with four vertices corresponding to the corners of the image. meta : dict, None, optional A dictionary of various items to be stored within the `SkyImage` object. """ self.image = image self.convf = convf self.meta = meta self._id = id self._pix_area = pix_area # WCS self.wcs_fwd = wcs_fwd self.wcs_inv = wcs_inv # initial sky value: self._sky = 0.0 self._sky_is_valid = False # check that mask has the same shape as image: if mask is None: self.mask = None else: if image is None: raise ValueError("'mask' must be None when 'image' is None") self.mask = np.asanyarray(mask, dtype=np.bool) if self.mask.shape != image.shape: raise ValueError("'mask' must have the same shape as 'image'.") # create spherical polygon bounding the image if image is None or wcs_fwd is None or wcs_inv is None: self._radec = [(np.array([]), np.array([]))] self._polygon = SphericalPolygon([]) self._poly_area = 0.0 else: self.calc_bounding_polygon(stepsize) # set sky statistics function (NOTE: it must return statistics and # the number of pixels used after clipping) if skystat is None: self.set_builtin_skystat() else: self.skystat = skystat