def perform_pixelscaleChecks(w1, w2, verbose=False, debugmode=False): # Check that the pixel scale in 2 directions on the image are similar, if not exit, as this script isn't coded for it (YOLO) pixel_size1 = utils.proj_plane_pixel_scales(w1) pixel_size2 = utils.proj_plane_pixel_scales(w2) if (pixel_size1[0] - pixel_size1[1]) / pixel_size1[0] > 0.01: sys.exit( f'ERROR : input to determine_imageOverlap, {f1}, has pixels that are not very square, this code is not set up to deal with that YOLO' ) if (pixel_size2[0] - pixel_size2[1]) / pixel_size2[0] > 0.01: sys.exit( f'ERROR : input to determine_imageOverlap, {f2}, has pixels that are not very square, this code is not set up to deal with that YOLO' ) # Check the pixel scale of two input images are similar, if not, throw a warning if abs(pixel_size1[0] - pixel_size2[0]) / pixel_size1[0] < 0.01: printme = f'The two image pixel scales are very similar, so this should not be an issue: {pixel_size1},{pixel_size2}' print_debug_string(printme, debugmode=debugmode) elif abs(pixel_size1[0] - pixel_size2[0]) / pixel_size1[0] < 0.1: printme = f'The two image pixel scales are within 10%, so this should not be an issue: {pixel_size1},{pixel_size2}' print_debug_string(printme, debugmode=debugmode) elif abs(pixel_size1[0] - pixel_size2[0]) / pixel_size1[0] < 2.0: printme = f'The two image pixel scales have a diff > 10% but smaller than a factor of 2, could be an issue: {pixel_size1},{pixel_size2}' print_debug_string(printme, debugmode=debugmode) else: sys.exit( 'ERROR : Input images to determine_imageOverlap differ in pixel scale by more than factor of 2, check if this is ok, if ok, then time to update determine_imageOverlap to allow this.' ) # Print some extra information if in verbose mode printme = f'If SWarp is used with PIXELSCALE_TYPE default of MEDIAN, the output image pix scale is the median of pixel scales at centre of input images.' print_verbose_string(printme, verbose=verbose) return None
def test_pixscale_nodrop(): mywcs = WCS(naxis=2) mywcs.wcs.cdelt = [0.1, 0.2] mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] assert_almost_equal(proj_plane_pixel_scales(mywcs), (0.1, 0.2)) mywcs.wcs.cdelt = [-0.1, 0.2] assert_almost_equal(proj_plane_pixel_scales(mywcs), (0.1, 0.2))
def test_pixscale_withdrop(): mywcs = WCS(naxis=3) mywcs.wcs.cdelt = [0.1, 0.2, 1] mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN', 'VOPT'] assert_almost_equal(proj_plane_pixel_scales(mywcs.celestial), (0.1, 0.2)) mywcs.wcs.cdelt = [-0.1, 0.2, 1] assert_almost_equal(proj_plane_pixel_scales(mywcs.celestial), (0.1, 0.2))
def wcs_pixel_scale(self, method='cdelt'): """Pixel scale. Returns angles along each axis of the image pixel at the CRPIX location once it is projected onto the plane of intermediate world coordinates. Calls `~astropy.wcs.utils.proj_plane_pixel_scales`. Parameters ---------- method : {'cdelt', 'proj_plane'} (default 'cdelt') Result is calculated according to the 'cdelt' or 'proj_plane' methods. Returns ------- angle : `~astropy.coordinates.Angle` An angle of projection plane increments corresponding to each pixel side (axis). Examples -------- >>> from gammapy.image import SkyImage >>> image = SkyImage.empty(nxpix=3, nypix=2) >>> image.wcs_pixel_scale() <Angle [ 0.02, 0.02] deg> """ if method == 'cdelt': scales = np.abs(self.wcs.wcs.cdelt) elif method == 'proj_plane': scales = proj_plane_pixel_scales(wcs=self.wcs) else: raise ValueError('Invalid method: {}'.format(method)) return Angle(scales, unit='deg')
def viewinwwt(filename, imageurl): fname = '{:s}.txt'.format(os.path.splitext(os.path.split(filename)[1])[0]) outfile = os.path.join(public_folder, fname) header = fits.getheader(filename) reqd = {'imageurl': imageurl} try: reqd['x'] = header['CRPIX1'] reqd['y'] = header['CRPIX2'] ra_str = header['RA'] dec_str = header['DEC'] reqd['rotation'] = _calculate_rotation_angle('icrs', header) except: print('{:s} does not have the needed header keys'.format(filename)) return coord = SkyCoord('{} {}'.format(ra_str, dec_str), unit=(u.hourangle, u.deg)) reqd['ra'] = coord.ra.value reqd['dec'] = coord.dec.value wcs = WCS(header) reqd['scale'] = proj_plane_pixel_scales(wcs)[0] reqd['name'] = os.path.split(filename)[0] request = 'name={name}&ra={ra}&dec={dec}&x={x}&y={y}&rotation={rotation}&imageurl={imageurl}'.format( **reqd) with open(outfile, 'w') as outp: outp.write('{0:s}{1:s}'.format(base_url, request)) return
def imcopy(im, size=size, positions=positions): ''' size=10 position=(ra,dec), default (False,False) ''' outname = os.path.splitext(im)[0] + '_' + str(size) + 'min_cut.fits' if os.path.isfile(outname): os.system('rm ' + outname) hdr = fits.getheader(im) w = WCS(hdr) xpscale, ypscale = wcsutils.proj_plane_pixel_scales(w) * 3600 pixscale = (xpscale + ypscale) / 2. if positions == (False, False): print('RA or DEC input, False, position will be center of', im) px, py = hdr['NAXIS1'] / 2., hdr['NAXIS2'] / 2. ax, bx = px - size / 2 / pixscale * 60, px + size / 2 / pixscale * 60 ay, by = py - size / 2 / pixscale * 60, py + size / 2 / pixscale * 60 else: ra, dec = positions[0], positions[1] px, py = w.wcs_world2pix(ra, dec, 1) print('center pixel coordinates', int(px), int(py)) ax, bx = px - size / 2 / pixscale * 60, px + size / 2 / pixscale * 60 ay, by = py - size / 2 / pixscale * 60, py + size / 2 / pixscale * 60 print('pixel scale =', '%.3f' % (pixscale), size, 'arcmin rectangular cut =', int(bx - ax), 'pixels') region = '[' + str(int(ax)) + ':' + str(int(bx)) + ',' + str( int(ay)) + ':' + str(int(by)) + ']' print(outname, 'will be created') #region='[200:2048,60:2048]' chinim = im + region iraf.imcopy(chinim, output=outname) return 'Done'
def _ang_size(self): if not hasattr(self, "_header"): raise AttributeError("No header has not been given.") pix_scale = np.abs(proj_plane_pixel_scales(self._wcs)[0]) return pix_scale * u.Unit(self._wcs.wcs.cunit[1])
def from_pixel(cls, region, wcs): """ This function ... :param region: :param wcs: :return: """ # Get center sky coordinate center = region.center.to_sky(wcs) ## GET THE PIXELSCALE result = utils.proj_plane_pixel_scales(wcs) # returns: A vector (ndarray) of projection plane increments corresponding to each pixel side (axis). # The units of the returned results are the same as the units of cdelt, crval, and cd for the celestial WCS # and can be obtained by inquiring the value of cunit property of the input WCS WCS object. x_pixelscale = result[0] * u("deg") y_pixelscale = result[1] * u("deg") # pixelscale = Extent(x_pixelscale, y_pixelscale) semimajor = region.semimajor * x_pixelscale semiminor = region.semiminor * y_pixelscale radius = SkyStretch(semimajor, semiminor) # Create a new SkyEllipse return cls(center, radius, region.angle, meta=region.meta)
def _instantiate(self, hdul=None): if hdul is None: try: hdul = self.get_pyfits() except (AttributeError, IOError): return self.data = hdul[0].data self.header = hdul[0].header self.wcs = WCS(self.header) if self.wcs: cd1, cd2 = proj_plane_pixel_scales(self.wcs) cu1, cu2 = self.wcs.wcs.cunit cd1 = u.Quantity(cd1, unit=cu1) cd2 = u.Quantity(cd2, unit=cu2) try: self.pxscale = np.sqrt(cd1 * cd2).to(u.arcsec) / u.pix except Exception as e: print 'Error parsing wcs' print e, '\n' self.wcs = None self.pxscale = None else: self.pxscale = None self.tempdatafile = self.__generate_temp_file(data=hdul, suffix='.fits', ref=self.tempdatafile) self.tempphotfile = self.__generate_temp_file(suffix='.dat', ref=self.tempphotfile) self.tempcoofile = self.__generate_temp_file(suffix='.coo', ref=self.tempcoofile) self.tempradfile = self.__generate_temp_file(suffix='.prf', ref=self.tempradfile)
def to_pixel(self, wcs): """ This function ... :param wcs: :return: """ center = self.center.to_pixel(wcs) ## GET THE PIXELSCALE result = utils.proj_plane_pixel_scales(wcs) # returns: A vector (ndarray) of projection plane increments corresponding to each pixel side (axis). # The units of the returned results are the same as the units of cdelt, crval, and cd for the celestial WCS # and can be obtained by inquiring the value of cunit property of the input WCS WCS object. x_pixelscale = result[0] * Unit("deg/pix") y_pixelscale = result[1] * Unit("deg/pix") #pixelscale = Extent(x_pixelscale, y_pixelscale) major = (self.major / x_pixelscale).to("pix").value minor = (self.minor / y_pixelscale).to("pix").value radius = Extent(major, minor) # Create a new Ellipse and return it return Ellipse(center, radius, self.angle, meta=self.meta)
def from_pixel(cls, ellipse, wcs): """ This function ... :param ellipse: :param wcs: :return: """ center = SkyCoordinate.from_pixel(Coordinate(ellipse.center.x, ellipse.center.y), wcs) ## GET THE PIXELSCALE result = utils.proj_plane_pixel_scales(wcs) # returns: A vector (ndarray) of projection plane increments corresponding to each pixel side (axis). # The units of the returned results are the same as the units of cdelt, crval, and cd for the celestial WCS # and can be obtained by inquiring the value of cunit property of the input WCS WCS object. x_pixelscale = result[0] * Unit("deg/pix") y_pixelscale = result[1] * Unit("deg/pix") #pixelscale = Extent(x_pixelscale, y_pixelscale) major = ellipse.major * Unit("pix") * x_pixelscale minor = ellipse.minor * Unit("pix") * y_pixelscale radius = Extent(major, minor) # Create a new SkyEllipse return cls(center, radius, ellipse.angle, meta=ellipse.meta)
def viewinwwt(filename, imageurl): fname = '{:s}.txt'.format(os.path.splitext(os.path.split(filename)[1])[0]) outfile = os.path.join(public_folder, fname) header = fits.getheader(filename) reqd = {'imageurl': imageurl} try: reqd['x'] = header['CRPIX1'] reqd['y'] = header['CRPIX2'] ra_str = header['RA'] dec_str = header['DEC'] reqd['rotation'] = _calculate_rotation_angle('icrs', header) except: print('{:s} does not have the needed header keys'.format(filename)) return coord = SkyCoord('{} {}'.format(ra_str, dec_str), unit=(u.hourangle, u.deg)) reqd['ra'] = coord.ra.value reqd['dec'] = coord.dec.value wcs = WCS(header) reqd['scale'] = proj_plane_pixel_scales(wcs)[0] reqd['name'] = os.path.split(filename)[0] request = 'name={name}&ra={ra}&dec={dec}&x={x}&y={y}&rotation={rotation}&imageurl={imageurl}'.format(**reqd) with open(outfile, 'w') as outp: outp.write('{0:s}{1:s}'.format(base_url, request)) return
def photometry_custom(image_data, radpix, wcs): #aperture = CircularAperture((image_data.shape[0]/2,image_data.shape[0]/2), radpix[19]) position = [image_data.shape[0] / 2, image_data.shape[0] / 2] apertures = [CircularAperture(position, r=r) for r in radpix] annulus_aperture = CircularAnnulus(position, r_in=radpix[19], r_out=radpix[19] + radpix[16]) phot_table = aperture_photometry(image_data, apertures, wcs=wcs) annulus_masks = annulus_aperture.to_mask(method='center') annulus_data = annulus_masks.multiply(image_data) mask = annulus_masks.data annulus_data_1d = annulus_data[mask > 0] _, median_sigclip, _ = sigma_clipped_stats(annulus_data_1d) bkg_mean = median_sigclip #sky_aperture = to_sky(apertures,wcs) phot_array = np.zeros(20) bkg_sum_array = np.zeros(20) for i in range(0, 20): phot_array[i] = phot_table['aperture_sum_' + str(i)][0] bkg_sum_array[i] = bkg_mean * apertures[i].area final_sum = phot_array - bkg_sum_array scale = np.mean(proj_plane_pixel_scales(wcs)) phot_Jy_custom = final_sum print('Backgorud outer radius =', radpix[19] + radpix[16], 'pixels') print(phot_table) return (phot_Jy_custom)
def MakeRGB(df, p, xs, ys, r, g, b, w, bp, s, q): pixelscale = utils.proj_plane_pixel_scales(w) dx = int(0.5 * xs * ARCMIN_TO_DEG / pixelscale[0]) # pixelscale is in degrees (CUNIT) dy = int(0.5 * ys * ARCMIN_TO_DEG / pixelscale[1]) image = mlrgb(r, g, b, minimum=bp, stretch=s, Q=q) image = Image.fromarray(image, mode='RGB') image = image.transpose(PIL.Image.FLIP_TOP_BOTTOM) if 'XSIZE' in df and not np.isnan(df['XSIZE'][p]): udx = int(0.5 * df['XSIZE'][p] * ARCMIN_TO_DEG / pixelscale[0]) else: udx = dx if 'YSIZE' in df and not np.isnan(df['YSIZE'][p]): udy = int(0.5 * df['YSIZE'][p] * ARCMIN_TO_DEG / pixelscale[0]) else: udy = dy if image.size != (2*udx, 2*udy): issmaller = True else: issmaller = False return image, issmaller
def _calculate_rotation_angle(reg_coordinate_frame, header): """Calculates the rotation angle from the region to the header's frame This attempts to be compatible with the implementation used by SAOImage DS9. In particular, this measures the rotation of the north axis as measured at the center of the image, and therefore requires a `~astropy.io.fits.Header` object with defined 'NAXIS1' and 'NAXIS2' keywords. Parameters ---------- reg_coordinate_frame : str Coordinate frame used by the region file header : `~astropy.io.fits.Header` instance Header describing the image Returns ------- y_axis_rot : float Degrees by which the north axis in the region's frame is rotated when transformed to pixel coordinates """ new_wcs = WCS(header) region_frame = SkyCoord( '0d 0d', frame=reg_coordinate_frame, obstime='J2000') region_frame = SkyCoord( '0d 0d', frame=reg_coordinate_frame, obstime='J2000', equinox=region_frame.equinox) origin = SkyCoord.from_pixel( header['NAXIS1']/2, header['NAXIS2']/2, wcs=new_wcs, origin=1).transform_to(region_frame) offset = proj_plane_pixel_scales(new_wcs)[1] origin_x, origin_y = origin.to_pixel(new_wcs, origin=1) origin_lon = origin.data.lon.degree origin_lat = origin.data.lat.degree offset_point = SkyCoord( origin_lon, origin_lat+offset, unit='degree', frame=origin.frame.name, obstime='J2000') offset_x, offset_y = offset_point.to_pixel(new_wcs, origin=1) north_rot = np.arctan2( offset_y-origin_y, offset_x-origin_x) / np.pi*180. cdelt = new_wcs.wcs.get_cdelt() if (cdelt > 0).all() or (cdelt < 0).all(): return north_rot - 90 else: return -(north_rot - 90)
def ds9_region(image_path, image, segm, wcs, ds9_region): """"Creates ds9 region file. This function creates a ds9 region file to display the sources detected by the segmentation function. This file is written to the same directory the fits files are in. Args: image_path(str, required): Image path to particular FITs. File image(array, required): This is the image data segm: The segmentation image wcs: World Coordinte System object ds9_region(boolean, opt): If true, creates region file """ if ds9_region is True: data_path = os.path.splitext(image_path) region_path = str(data_path[0]) + '_ds9region' scale = proj_plane_pixel_scales(wcs) image_scale = scale[0] reg = source_properties(image, segm, wcs=wcs) with open(region_path + '.reg', 'w') as f: f.write('# Region file format: DS9 version 7.6\n\n') f.write('global color=#ff7733\n') f.write('global width=2\n') f.write('fk5\n\n') for i in range(0, len(reg.id)): x = reg[i].sky_centroid_icrs.ra.to(u.deg) y = reg[i].sky_centroid_icrs.dec r = image_scale * reg[i].equivalent_radius f.write('circle(' + str(x.value) + ',' + str(y.value) + ',' + str(r.value) + ')' + ' # Source Number:' + str(reg[i].id) + '\n')
def get_postage_stamp(radecstr, size, image, image_wcs): """ Extract a postage stamp image from a larger image Parameters ---------- radecstr : str SkyCoord pareseable string for ra, dec coordinates size : 2 element tuple size in ra and dec in arcsec image : 2D numpy.ndarray image to cut postage stamps from image_wcs : astropy.wcs WCS for image """ position = SkyCoord(radecstr) pix_scales = 3600. * proj_plane_pixel_scales(image_wcs) pix_size = (size[0] / pix_scales[0], size[1] / pix_scales[1]) try: result = Cutout2D(image, position, pix_size, wcs=image_wcs) except: return None return result
def __get_img_coordinates(self, header, fits_naxis1, fits_naxis2): """ set the image attributes ra, dec, fov_bmin and fov_bmaj, radius from the image file header """ wcs = WCS(header, naxis=2) pix_centre = [[header[fits_naxis1] / 2., header[fits_naxis2] / 2.]] self.ra, self.dec = wcs.wcs_pix2world(pix_centre, 1)[0] # The field-of-view (in pixels) is assumed to be a circle in the centre # of the image. This may be an ellipse on the sky, eg MOST images. # We leave a pixel margin at the edge that we don't use. # TODO: move unused pixel as argument unusedpix = 0. usable_radius_pix = self.__get_radius_pixels(header, fits_naxis1, fits_naxis2) - unusedpix cdelt1, cdelt2 = proj_plane_pixel_scales(WCS(header).celestial) self.fov_bmin = usable_radius_pix * abs(cdelt1) self.fov_bmaj = usable_radius_pix * abs(cdelt2) self.physical_bmin = header[fits_naxis1] * abs(cdelt1) self.physical_bmaj = header[fits_naxis2] * abs(cdelt2) # set the pixels radius # TODO: check calcs self.radius_pixels = usable_radius_pix
def mask_galaxy(image, wcs, Ra, Dec, name, radius): """Masks galaxy at Ra, Dec within a radius given in arcminutes Creates a circular mask centered at a given Ra, Dec. The radius is given in arcmins. The wcs object is used to convert these inputs to pixel locations. A pixel scale is also determined. If the object name is suppled, SESAME is used to find object center. If no active internet connection is available, center location must be manually entered, in degrees. If no center coordinates are supplied, (0, 0) is the default center. Args: image(array, required): Image data wcs: World Coordinte System object name(str, optional): Name of galaxy or object Ra(str): Right Ascention Dec(str): Declination Radius(float, required): Radius to be masked, in arcminutes Returns: masked_img(array): Image which has been masked mask(boolean array): Mask of the given object""" # Radius must be given in arcminutes # Dimentions of the image dim = (image.shape) y, x = dim[0], dim[1] # Finds the center of an object by inputting its name into SESAME # This requires an active internet connection # a, b are the coordinates of the center given in pixels try: center = SkyCoord.from_name(name) except Exception: print("No active internet connection. Manually enter Ra, Dec.") Ra = Ra Dec = Dec center = SkyCoord(Ra, Dec, unit="deg") c_pix = skycoord_to_pixel(center, wcs) a, b = c_pix[0], c_pix[1] print(center) radius = radius * u.arcmin # Finds pixel scale using WSC object. The default units can be found by # unit = header['CUNIT1'], they are degrees by convention # degrees are converted to arcmins and radius in computed in pixels scale = proj_plane_pixel_scales(wcs) pix_scale = scale[0] * u.deg.to(u.arcmin) print('Image Scale: ' + str(pix_scale) + ' arcmin/pix') rad_pix = (radius / pix_scale).value # Indexes each pixel and checks if its is >= radius from center Y, X = np.ogrid[:y, :x] dist_from_center = np.sqrt((X - a)**2 + (Y - b)**2) mask = dist_from_center <= rad_pix return mask
def process_new_image_event(self, event: NewImageEvent, sender: str, *args, **kwargs): """Puts a new images in the DB with the given ID. Args: event: New image event sender: Who sent the event? Returns: Success """ # filter by source if self._sources is not None and sender not in self._sources: return # put into queue log.info('Received new image event from %s.', sender) # download image try: log.info('Downloading file %s...', event.filename) image = self.vfs.read_image(event.filename) except FileNotFoundError: log.error('Could not download image.') return # get catalog cat = image.catalog if cat is None: # no catalog found in file return # filter by ellipticity cat = cat[cat['ellipticity'] < self._max_ellipticity] # get WCS and pixel size wcs = WCS(image.header) pix_size = abs(proj_plane_pixel_scales(wcs)[0] * 3600.) # calculate seeing seeing = np.mean(cat['fwhm']) * pix_size # correct for airmass? if self._correct_for_airmass: # Seeing S as function of seeing S0 at zenith and airmass a: # S = S0 * a^0.6 # see https://www.astro.auth.gr/~seeing-gr/seeing_gr_files/theory/node17.html # need airmass if 'AIRMASS' in image.header: seeing /= image.header['AIRMASS']**0.6 else: # could not correct return # log it if self._publisher is not None: self._publisher(time=Time.now().isot, seeing=seeing)
def test_pixscale_pc_rotated(angle): mywcs = WCS(naxis=2) rho = np.radians(angle) scale = 0.1 mywcs.wcs.cdelt = [-scale, scale] mywcs.wcs.pc = [[np.cos(rho), -np.sin(rho)], [np.sin(rho), np.cos(rho)]] mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] assert_almost_equal(proj_plane_pixel_scales(mywcs), (0.1, 0.1))
def binsz_wcs(self): """Angular bin size of the underlying `~WcsGeom` Returns ------- binsz_wcs: `~astropy.coordinates.Angle` """ return Angle(proj_plane_pixel_scales(self.wcs), unit="deg")
def test_pixscale_cd_rotated(angle): mywcs = WCS(naxis=2) rho = np.radians(angle) scale = 0.1 mywcs.wcs.cd = [[scale * np.cos(rho), -scale * np.sin(rho)], [scale * np.sin(rho), scale * np.cos(rho)]] mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] assert_almost_equal(proj_plane_pixel_scales(mywcs), (0.1, 0.1))
def MakeTiffCut(tiledir, outdir, positions, xs, ys, df, maketiff, makepngs): logger = logging.getLogger(__name__) os.makedirs(outdir, exist_ok=True) imgname = glob.glob(tiledir + '*.tiff') try: im = Image.open(imgname[0]) #except IOError as e: except IndexError as e: print('No TIFF file found for tile ' + df['TILENAME'][0] + '. Will not create true-color cutout.') logger.error('MakeTiffCut - No TIFF file found for tile ' + df['TILENAME'][0] + '. Will not create true-color cutout.') return # try opening I band FITS (fallback on G, R bands) hdul = None for _i in ['i','g','r','z','Y']: tilename = glob.glob(tiledir+'*_{}.fits.fz'.format(_i)) try: hdul = fits.open(tilename[0]) except IOError as e: hdul = None logger.warning('MakeTiffCut - Could not find master FITS file: ' + tilename) continue else: break if not hdul: print('Cannot find a master fits file for this tile.') logger.error('MakeTiffCut - Cannot find a master fits file for this tile.') return w = WCS(hdul['SCI'].header) pixelscale = utils.proj_plane_pixel_scales(w) dx = int(0.5 * xs * ARCMIN_TO_DEG / pixelscale[0]) # pixelscale is in degrees (CUNIT) dy = int(0.5 * ys * ARCMIN_TO_DEG / pixelscale[1]) pixcoords = utils.skycoord_to_pixel(positions, w, origin=0, mode='wcs') for i in range(len(positions)): if 'COADD_OBJECT_ID' in df: filenm = outdir + str(df['COADD_OBJECT_ID'][i]) else: filenm = outdir + 'x{0}y{1}'.format(df['RA'][i], df['DEC'][i]) #filenm = outdir + 'DESJ' + _DecConverter(df['RA'][0], df['DEC'][0]) left = max(0, pixcoords[0][i] - dx) upper = max(0, im.size[1] - pixcoords[1][i] - dy) right = min(pixcoords[0][i] + dx, 10000) lower = min(im.size[1] - pixcoords[1][i] + dy, 10000) newimg = im.crop((left, upper, right, lower)) if newimg.size != (2*dx, 2*dy): logger.info('MakeTiffCut - {} is smaller than user requested. This is likely because the object/coordinate was in close proximity to the edge of a tile.'.format(filenm.split('/')[-1])) if maketiff: newimg.save(filenm+'.tiff', format='TIFF') if makepngs: newimg.save(filenm+'.png', format='PNG') logger.info('MakeTiffCut - Tile {} complete.'.format(df['TILENAME'][0]))
def convert_to_imagecoord(shape, header): """Convert the coordlist of `shape` to image coordinates Parameters ---------- shape : `pyregion.parser_helper.Shape` The `Shape` to convert coordinates header : `~astropy.io.fits.Header` Specifies what WCS transformations to use. Returns ------- new_coordlist : list A list of image coordinates defining the shape. """ arg_types = _generate_arg_types(len(shape.coord_list), shape.name) new_coordlist = [] is_even_distance = True coord_list_iter = iter(zip(shape.coord_list, arg_types)) new_wcs = WCS(header) pixel_scales = proj_plane_pixel_scales(new_wcs) for coordinate, coordinate_type in coord_list_iter: if coordinate_type == CoordOdd: even_coordinate = next(coord_list_iter)[0] old_coordinate = SkyCoord(coordinate, even_coordinate, frame=shape.coord_format, unit='degree', obstime='J2000') new_coordlist.extend( np.asscalar(x) for x in old_coordinate.to_pixel(new_wcs, origin=1) ) elif coordinate_type == Distance: if arg_types[-1] == Angle: degree_per_pixel = pixel_scales[0 if is_even_distance else 1] is_even_distance = not is_even_distance else: degree_per_pixel = np.sqrt(proj_plane_pixel_area(new_wcs)) new_coordlist.append(coordinate / degree_per_pixel) elif coordinate_type == Angle: new_angle = _estimate_angle(coordinate, shape.coord_format, header) new_coordlist.append(new_angle) else: new_coordlist.append(coordinate) return new_coordlist
def _calculate_rotation_angle(reg_coordinate_frame, header): """Calculates the rotation angle from the region to the header's frame This attempts to be compatible with the implementation used by SAOImage DS9. In particular, this measures the rotation of the north axis as measured at the center of the image, and therefore requires a `~astropy.io.fits.Header` object with defined 'NAXIS1' and 'NAXIS2' keywords. Parameters ---------- reg_coordinate_frame : str Coordinate frame used by the region file header : `~astropy.io.fits.Header` instance Header describing the image Returns ------- y_axis_rot : float Degrees by which the north axis in the region's frame is rotated when transformed to pixel coordinates """ new_wcs = WCS(header) region_frame = SkyCoord('0d 0d', frame=reg_coordinate_frame, obstime='J2000') region_frame = SkyCoord('0d 0d', frame=reg_coordinate_frame, obstime='J2000', equinox=region_frame.equinox) origin = SkyCoord.from_pixel(header['NAXIS1'] / 2, header['NAXIS2'] / 2, wcs=new_wcs, origin=1).transform_to(region_frame) offset = proj_plane_pixel_scales(new_wcs)[1] origin_x, origin_y = origin.to_pixel(new_wcs, origin=1) origin_lon = origin.data.lon.degree origin_lat = origin.data.lat.degree offset_point = SkyCoord(origin_lon, origin_lat + offset, unit='degree', frame=origin.frame.name, obstime='J2000') offset_x, offset_y = offset_point.to_pixel(new_wcs, origin=1) north_rot = np.arctan2(offset_y - origin_y, offset_x - origin_x) / np.pi * 180. cdelt = new_wcs.wcs.get_cdelt() if (cdelt > 0).all() or (cdelt < 0).all(): return north_rot - 90 else: return -(north_rot - 90)
def draw_hips_order(self) -> int: """Compute HiPS tile order matching a given image pixel size.""" # Sky image angular resolution (pixel size in degrees) resolution = np.min(proj_plane_pixel_scales(self.geometry.wcs)) desired_order = hips_order_for_pixel_resolution(self.hips_survey.tile_width, resolution) # Return the desired order, or the highest resolution available. # Note that HiPS never has resolution less than 3, # and that limit is handled in _get_hips_order_for_resolution return np.min([desired_order, self.hips_survey.hips_order])
def convert_to_imagecoord(shape, header): """Convert the coordlist of `shape` to image coordinates Parameters ---------- shape : `pyregion.parser_helper.Shape` The `Shape` to convert coordinates header : `~astropy.io.fits.Header` Specifies what WCS transformations to use. Returns ------- new_coordlist : list A list of image coordinates defining the shape. """ arg_types = _generate_arg_types(len(shape.coord_list), shape.name) new_coordlist = [] is_even_distance = True coord_list_iter = iter(zip(shape.coord_list, arg_types)) new_wcs = WCS(header) pixel_scales = proj_plane_pixel_scales(new_wcs) for coordinate, coordinate_type in coord_list_iter: if coordinate_type == CoordOdd: even_coordinate = next(coord_list_iter)[0] old_coordinate = SkyCoord(coordinate, even_coordinate, frame=shape.coord_format, unit='degree', obstime='J2000') new_coordlist.extend( np.asscalar(x) for x in old_coordinate.to_pixel(new_wcs, origin=1)) elif coordinate_type == Distance: if arg_types[-1] == Angle: degree_per_pixel = pixel_scales[0 if is_even_distance else 1] is_even_distance = not is_even_distance else: degree_per_pixel = np.sqrt(proj_plane_pixel_area(new_wcs)) new_coordlist.append(coordinate / degree_per_pixel) elif coordinate_type == Angle: new_angle = _estimate_angle(coordinate, shape.coord_format, header) new_coordlist.append(new_angle) else: new_coordlist.append(coordinate) return new_coordlist
def get_pixscale(self, **kwargs_wcs): ''' return pixel scale of input fits in unit of arcsec/pixel ''' from astropy.wcs.utils import proj_plane_pixel_scales w=self.get_wcs(**kwargs_wcs) pixel_scales=proj_plane_pixel_scales(w)*3600 # arcsec/pixel return np.average(pixel_scales)
def pixel_to_angular(pixel_size, wcs): pixel_scales = proj_plane_pixel_scales(wcs) assert np.allclose(*pixel_scales) pixel_scale = pixel_scales[0] * wcs.wcs.cunit[0] / u.pix if not hasattr(pixel_size, 'unit'): pixel_size = pixel_size * u.pix angular_diameter = pixel_size * pixel_scale.to(u.arcsec / u.pix) return angular_diameter
def angular_to_pixel(angular_diameter, wcs): pixel_scales = proj_plane_pixel_scales(wcs) assert np.allclose(*pixel_scales) pixel_scale = pixel_scales[0] * wcs.wcs.cunit[0] / u.pix pixel_size = angular_diameter / pixel_scale.to( angular_diameter.unit / u.pix) pixel_size = pixel_size.value return pixel_size
def get_image_scale(wcs): """ Return the image scale as a list of projection plane increments corresponding to each axis, calculated from the given FITS file WCS information. The units of the returned results are the same as the units of cdelt, crval, and cd for the celestial WCS and can be obtained by inquiring the value of cunit property of the input WCS object. """ return list(proj_plane_pixel_scales(wcs))
def __init__(self, filename=None, hdu=0, unit=None, zero_point=None, pixel_scales=None, wcs_rotation=None, mask=None, verbose=True): ''' Parameters ---------- filename (optional) : string FITS file name of the image. hdu : int (default: 0) The number of extension to load from the FITS file. unit (optional) : string Unit of the image flux for CCDData. zero_point (optional) : float Magnitude zero point. pixel_scales (optional) : tuple Pixel scales along the first and second directions, units: arcsec. wcs_rotation (optional) : float WCS rotation, east of north, units: radian. mask (optional) : 2D bool array The image mask. verbose : bool (default: True) Print out auxiliary data. ''' if filename is None: self.data = None else: self.data = CCDData.read(filename, hdu=hdu, unit=unit, mask=mask) if self.data.wcs and (pixel_scales is None): pixel_scales = proj_plane_pixel_scales( self.data.wcs) * u.degree.to('arcsec') self.zero_point = zero_point if pixel_scales is None: self.pixel_scales = None else: self.pixel_scales = (pixel_scales[0] * u.arcsec, pixel_scales[1] * u.arcsec) if self.data.wcs and (wcs_rotation is None): self.wcs_rotation = get_wcs_rotation(self.data.wcs) elif wcs_rotation is not None: self.wcs_rotation = wcs_rotation * u.radian else: self.wcs_rotation = None self.sources_catalog = None self.sigma_image = None self.sources_skycord = None self.ss_data = None self.PSF = None
def pixel_scale(self): """The pixel scales of the detector in the X and Y image dimensions. """ from astropy.wcs.utils import proj_plane_pixel_scales from astropy import units as u units = self.wcs.world_axis_units u1 = getattr(u, units[0]) u2 = getattr(u, units[1]) scales = proj_plane_pixel_scales(self.wcs) ps1 = (scales[0] * u1).to('arcsec').value ps2 = (scales[1] * u2).to('arcsec').value return np.asarray([ps1, ps2]) * u.arcsec
def pixel_scales(self): """ Pixel scale. Returns angles along each axis of the image at the CRPIX location once it is projected onto the plane of intermediate world coordinates. Returns ------- angle: `~astropy.coordinates.Angle` """ return Angle(proj_plane_pixel_scales(self.wcs), "deg")
def from_sky(cls, region, wcs): """ This function ... :param region: :param wcs: :return: """ # Get center pixel coordinate center = region.center.to_pixel(wcs) ## GET THE PIXELSCALE result = utils.proj_plane_pixel_scales(wcs) # returns: A vector (ndarray) of projection plane increments corresponding to each pixel side (axis). # The units of the returned results are the same as the units of cdelt, crval, and cd for the celestial WCS # and can be obtained by inquiring the value of cunit property of the input WCS WCS object. x_pixelscale = result[0] * u("deg") y_pixelscale = result[1] * u("deg") # pixelscale = Extent(x_pixelscale, y_pixelscale) #print(region.semimajor) #print(region.semiminor) #print(x_pixelscale) #print(y_pixelscale) semimajor = (region.semimajor / x_pixelscale).to("").value semiminor = (region.semiminor / y_pixelscale).to("").value radius = PixelStretch(semimajor, semiminor) # Convert angle # Set the angle angle = region.angle if angle is not None: try: orientation = wcs.standard_orientation_angle except ValueError: orientation = wcs.orientation_angle # Add the orientation angle (w.r.t. standard E-W and S-N projection on the x and y axes) to the position angle # that is expressed in the standard way #return self.pa + orientation angle = angle + orientation else: angle = Angle(0.0, "deg") # Create a new PixelEllipse return cls(center, radius, angle, meta=region.meta, label=region.label, include=region.include, appearance=region.appearance)
def pixelscale(self): """ This function ... :return: """ result = utils.proj_plane_pixel_scales(self) # returns: A vector (ndarray) of projection plane increments corresponding to each pixel side (axis). # The units of the returned results are the same as the units of cdelt, crval, and cd for the celestial WCS # and can be obtained by inquiring the value of cunit property of the input WCS WCS object. x_pixelscale = result[0] * Unit("deg/pix") y_pixelscale = result[1] * Unit("deg/pix") # Return the pixel scale as an extent return Pixelscale(x_pixelscale, y_pixelscale)
def linear_offset_coordinates(wcs, center): ''' return a locally linear offset coordinate system does the simplest thing possible and assumes no projection distortions ''' assert isinstance(center, coords.SkyCoord), \ '`center` must by of type `SkyCoord`' assert center.isscalar, '`center` must have length 1' # Convert center to pixel coordinates xp, yp = skycoord_to_pixel(center, wcs) # Set up new WCS new_wcs = WCS(naxis=2) new_wcs.wcs.crpix = xp + 1, yp + 1 new_wcs.wcs.crval = 0., 0. new_wcs.wcs.cdelt = proj_plane_pixel_scales(wcs) new_wcs.wcs.ctype = 'XOFFSET', 'YOFFSET' new_wcs.wcs.cunit = 'deg', 'deg' return new_wcs
def extract_image_metadata(header, title=None, image_url=None): """Return a dictionary of metadata pulled from a fits header.""" try: wcs = WCS(header) except ValueError: raise ScrapeException('No WCS found in header') if not hasattr(wcs.wcs, 'cd'): raise ScrapeException('Insufficient WCS data in header') center = 0.5*(header['NAXIS1'] - 1), 0.5*(header['NAXIS2'] - 1) center_wcs = wcs.wcs_pix2world([center[0]], [center[1]], 0) corners_x = (0, 0, header['NAXIS1']-1, header['NAXIS1']-1) corners_y = (0, header['NAXIS2']-1, header['NAXIS2']-1, 0) corners_wcs = wcs.wcs_pix2world(corners_x, corners_y, 0) regionSTCS = 'Polygon ICRS' for wcs_x, wcs_y in zip(*corners_wcs): regionSTCS = regionSTCS + ' {} {}'.format(wcs_x, wcs_y) metadata = ImageMetadata( title=title, ra_center=center_wcs[0][0], dec_center=center_wcs[1][0], naxes=wcs.naxis, naxis='{} {}'.format(header['NAXIS1'], header['NAXIS2']), scale='{} {}'.format(*proj_plane_pixel_scales(wcs)), format='image/fits', image_url=image_url, crpix='{} {}'.format(*wcs.wcs.crpix), crval='{} {}'.format(*wcs.wcs.crval), cd_matrix='{} {} {} {}'.format( wcs.wcs.cd[0, 0], wcs.wcs.cd[0, 1], wcs.wcs.cd[1, 0], wcs.wcs.cd[1, 1]), reference_frame=wcs.wcs.radesys, regionSTCS=regionSTCS, ) # metadata['inst'] = None # recommended # metadata['mjd_obs'] = None # recommended # metadata['equinox'] = wcs.wcs.equinox return metadata
def show(self, major='BMAJ', minor='BMIN', angle='BPA', corner='bottom left', frame=False, borderpad=0.4, pad=0.5, **kwargs): """ Display the beam shape and size for the primary image. By default, this method will search for the BMAJ, BMIN, and BPA keywords in the FITS header to set the major and minor axes and the position angle on the sky. Parameters ---------- major : float, quantity or unit, optional Major axis of the beam in degrees or an angular quantity (overrides BMAJ if present) minor : float, quantity or unit, optional Minor axis of the beam in degrees or an angular quantity (overrides BMIN if present) angle : float, quantity or unit, optional Position angle of the beam on the sky in degrees or an angular quantity (overrides BPA if present) in the anticlockwise direction. corner : int, optional The beam location. Acceptable values are 'left', 'right', 'top', 'bottom', 'top left', 'top right', 'bottom left' (default), and 'bottom right'. frame : str, optional Whether to display a frame behind the beam (default is False) kwargs Additional arguments are passed to the matplotlib Ellipse class. See the matplotlib documentation for more details. """ if isinstance(major, str): major = self._header[major] if isinstance(minor, str): minor = self._header[minor] if isinstance(angle, str): angle = self._header[angle] if isinstance(major, u.Quantity): major = major.to(u.degree).value elif isinstance(major, u.Unit): major = major.to(u.degree) if isinstance(minor, u.Quantity): minor = minor.to(u.degree).value elif isinstance(minor, u.Unit): minor = minor.to(u.degree) if isinstance(angle, u.Quantity): angle = angle.to(u.degree).value elif isinstance(angle, u.Unit): angle = angle.to(u.degree) if self._wcs.is_celestial: pix_scale = proj_plane_pixel_scales(self._wcs) sx = pix_scale[self._dimensions[0]] sy = pix_scale[self._dimensions[1]] degrees_per_pixel = np.sqrt(sx * sy) else: raise ValueError("Cannot show beam when WCS is not celestial") self._base_settings['minor'] = minor self._base_settings['major'] = major self._base_settings['angle'] = angle self._base_settings['corner'] = corner self._base_settings['frame'] = frame self._base_settings['borderpad'] = borderpad self._base_settings['pad'] = pad minor /= degrees_per_pixel major /= degrees_per_pixel try: self._beam.remove() except Exception: pass if isinstance(corner, str): corner = corners[corner] self._beam = AnchoredEllipse(self._ax.transData, width=minor, height=major, angle=angle, loc=corner, pad=pad, borderpad=borderpad, frameon=frame) self._ax.add_artist(self._beam) self.set(**kwargs)
def show(self, length, label=None, corner='bottom right', frame=False, borderpad=0.4, pad=0.5, **kwargs): """ Overlay a scale bar on the image. Parameters ---------- length : float, or quantity The length of the scalebar in degrees, an angular quantity, or angular unit label : str, optional Label to place below the scalebar corner : int, optional Where to place the scalebar. Acceptable values are:, 'left', 'right', 'top', 'bottom', 'top left', 'top right', 'bottom left' (default), 'bottom right' frame : str, optional Whether to display a frame behind the scalebar (default is False) kwargs Additional arguments are passed to the matplotlib Rectangle and Text classes. See the matplotlib documentation for more details. In cases where the same argument exists for the two objects, the argument is passed to both the Text and Rectangle instance. """ self._length = length self._base_settings['corner'] = corner self._base_settings['frame'] = frame self._base_settings['borderpad'] = borderpad self._base_settings['pad'] = pad if isinstance(length, u.Quantity): length = length.to(u.degree).value elif isinstance(length, u.Unit): length = length.to(u.degree) if self._wcs.is_celestial: pix_scale = proj_plane_pixel_scales(self._wcs) sx = pix_scale[self._dimensions[0]] sy = pix_scale[self._dimensions[1]] degrees_per_pixel = np.sqrt(sx * sy) else: raise ValueError("Cannot show scalebar when WCS is not celestial") length = length / degrees_per_pixel try: self._scalebar.remove() except Exception: pass if isinstance(corner, str): corner = corners[corner] self._scalebar = AnchoredSizeBar(self._ax.transData, length, label, corner, pad=pad, borderpad=borderpad, sep=5, frameon=frame) self._ax.add_artist(self._scalebar) self.set(**kwargs)
#listfiles = glob.glob('/Volumes/VINCE/dwarfs/combined_LSBVCC/*.fits*') #listfiles = pd.read_csv('listfiles.csv').values.flatten() for f in tqdm(listfiles): hdu = fits.open(f) # Load the multi-extension fits file for i in range(0,len(hdu)): band = hdu[i].name if band == 'G': # Look only for G-band observations header = hdu[i].header w = WCS(hdu[i].header) # Define class parameters pscale = str(utils.proj_plane_pixel_scales(w)[0] * 3600)[0:5] gain = str(header['GAIN']) seeing = str(header['FINALIQ']) sizeX = str(header['NAXIS2']) sizeY = str(header['NAXIS1']) saturate = str(header['SATURATE']) output = f.split('.fits')[0] + '_{}'.format(band) image = f + '[{}]'.format(i) # Inizialite the class c = model2D(image, output, gain, pscale, seeing, saturate, sizeX, sizeY, model2D.params, isofactor = 3.) # Create median subtracted image _ = c.get_median()
(1 / cube.beam.sr.to(u.deg ** 2)) * \ (cube.header["CDELT2"] * u.deg)**2 if verbose: print("Total sum at native resolution.") print("Arecibo: {0}, VLA + Arecibo: {1}".format(ar.value, vla.value)) # print("Combined VLA + Arecibo: {}".format(vla_combined.value)) arec_sums[i] = ar.value vla_sums[i] = vla.value # vla_combined_sums[i] = vla_combined.value # Now convolve VLA to match Arecibo # This should match the previous sum pixscale = proj_plane_pixel_scales(arecibo.wcs)[0] beam_kern = arecibo.beam.as_kernel(pixscale) # conv_channel = convolve_fft(cube[i].value, beam_kern) # vla_conv = np.nansum(conv_channel) * \ # (1 / arecibo.beam.sr.to(u.deg ** 2)) * \ # (arecibo.header["CDELT2"] * u.deg)**2 # if verbose: # print("Total sum at common resolution.") # print("Arecibo: {0}, VLA + Arecibo: {1}".format(ar.value, vla_conv.value)) # print("VLA + Arecibo should be scaled by:" # " {}".format(vla_conv.value / ar.value))
def __init__(self, data, position, size, wcs=None, mode='trim', fill_value=np.nan, copy=False): """ Parameters ---------- data : `~numpy.ndarray` The 2D data array from which to extract the cutout array. position : tuple or `~astropy.coordinates.SkyCoord` The position of the cutout array's center with respect to the ``data`` array. The position can be specified either as a ``(x, y)`` tuple of pixel coordinates or a `~astropy.coordinates.SkyCoord`, in which case ``wcs`` is a required input. size : int, array-like, `~astropy.units.Quantity` The size of the cutout array along each axis. If ``size`` is a scalar number or a scalar `~astropy.units.Quantity`, then a square cutout of ``size`` will be created. If ``size`` has two elements, they should be in ``(ny, nx)`` order. Scalar numbers in ``size`` are assumed to be in units of pixels. ``size`` can also be a `~astropy.units.Quantity` object or contain `~astropy.units.Quantity` objects. Such `~astropy.units.Quantity` objects must be in pixel or angular units. For all cases, ``size`` will be converted to an integer number of pixels, rounding the the nearest integer. See the ``mode`` keyword for additional details on the final cutout size. .. note:: If ``size`` is in angular units, the cutout size is converted to pixels using the pixel scales along each axis of the image at the ``CRPIX`` location. Projection and other non-linear distortions are not taken into account. wcs : `~astropy.wcs.WCS`, optional A WCS object associated with the input ``data`` array. If ``wcs`` is not `None`, then the returned cutout object will contain a copy of the updated WCS for the cutout data array. mode : {'trim', 'partial', 'strict'}, optional The mode used for creating the cutout data array. For the ``'partial'`` and ``'trim'`` modes, a partial overlap of the cutout array and the input ``data`` array is sufficient. For the ``'strict'`` mode, the cutout array has to be fully contained within the ``data`` array, otherwise an `~astropy.nddata.utils.PartialOverlapError` is raised. In all modes, non-overlapping arrays will raise a `~astropy.nddata.utils.NoOverlapError`. In ``'partial'`` mode, positions in the cutout array that do not overlap with the ``data`` array will be filled with ``fill_value``. In ``'trim'`` mode only the overlapping elements are returned, thus the resulting cutout array may be smaller than the requested ``shape``. fill_value : number, optional If ``mode='partial'``, the value to fill pixels in the cutout array that do not overlap with the input ``data``. ``fill_value`` must have the same ``dtype`` as the input ``data`` array. copy : bool, optional If `False` (default), then the cutout data will be a view into the original ``data`` array. If `True`, then the cutout data will hold a copy of the original ``data`` array. Returns ------- result : `~astropy.nddata.utils.Cutout2D` A cutout object containing the 2D cutout data array and the updated WCS, if ``wcs`` is input. Examples -------- >>> import numpy as np >>> from astropy.nddata.utils import Cutout2D >>> from astropy import units as u >>> data = np.arange(20.).reshape(5, 4) >>> cutout1 = Cutout2D(data, (2, 2), (3, 3)) >>> print(cutout1.data) [[ 5. 6. 7.] [ 9. 10. 11.] [ 13. 14. 15.]] >>> print(cutout1.center_original) (2.0, 2.0) >>> print(cutout1.center_cutout) (1.0, 1.0) >>> print(cutout1.origin_original) (1, 1) >>> cutout2 = Cutout2D(data, (2, 2), 3) >>> print(cutout2.data) [[ 5. 6. 7.] [ 9. 10. 11.] [ 13. 14. 15.]] >>> size = u.Quantity([3, 3], u.pixel) >>> cutout3 = Cutout2D(data, (0, 0), size) >>> print(cutout3.data) [[ 0. 1.] [ 4. 5.]] >>> cutout4 = Cutout2D(data, (0, 0), (3 * u.pixel, 3)) >>> print(cutout4.data) [[ 0. 1.] [ 4. 5.]] >>> cutout5 = Cutout2D(data, (0, 0), (3, 3), mode='partial') >>> print(cutout5.data) [[ nan nan nan] [ nan 0. 1.] [ nan 4. 5.]] """ if isinstance(position, SkyCoord): if wcs is None: raise ValueError('wcs must be input if position is a ' 'SkyCoord') position = skycoord_to_pixel(position, wcs, mode='all') # (x, y) if np.isscalar(size): size = np.repeat(size, 2) # special handling for a scalar Quantity if isinstance(size, u.Quantity): size = np.atleast_1d(size) if len(size) == 1: size = np.repeat(size, 2) if len(size) > 2: raise ValueError('size must have at most two elements') shape = np.zeros(2).astype(int) pixel_scales = None # ``size`` can have a mixture of int and Quantity (and even units), # so evaluate each axis separately for axis, side in enumerate(size): if not isinstance(side, u.Quantity): shape[axis] = np.int(np.round(size[axis])) # pixels else: if side.unit == u.pixel: shape[axis] = np.int(np.round(side.value)) elif side.unit.physical_type == 'angle': if wcs is None: raise ValueError('wcs must be input if any element ' 'of size has angular units') if pixel_scales is None: pixel_scales = u.Quantity( proj_plane_pixel_scales(wcs), wcs.wcs.cunit[axis]) shape[axis] = np.int(np.round( (side / pixel_scales[axis]).decompose())) else: raise ValueError('shape can contain Quantities with only ' 'pixel or angular units') data = np.asanyarray(data) # reverse position because extract_array and overlap_slices # use (y, x), but keep the input position pos_yx = position[::-1] cutout_data, input_position_cutout = extract_array( data, tuple(shape), pos_yx, mode=mode, fill_value=fill_value, return_position=True) if copy: cutout_data = np.copy(cutout_data) self.data = cutout_data self.input_position_cutout = input_position_cutout[::-1] # (x, y) slices_original, slices_cutout = overlap_slices( data.shape, shape, pos_yx, mode=mode) self.slices_original = slices_original self.slices_cutout = slices_cutout self.shape = self.data.shape self.input_position_original = position self.shape_input = shape ((self.xmin_original, self.xmax_original), (self.ymin_original, self.ymax_original)) = self.bbox_original ((self.xmin_cutout, self.xmax_cutout), (self.ymin_cutout, self.ymax_cutout)) = self.bbox_cutout # the true origin pixel of the cutout array, including any # filled cutout values self._origin_original_true = ( self.origin_original[0] - self.slices_cutout[1].start, self.origin_original[1] - self.slices_cutout[0].start) if wcs is not None: self.wcs = deepcopy(wcs) self.wcs.wcs.crpix -= self._origin_original_true else: self.wcs = None
""" Iterative m.a.d. based sigma with positive outlier rejection. """ noise = mad_std(data, axis=axis) for _ in range(iterations): ind = (np.abs(data) <= thresh * noise).nonzero() noise = mad_std(data[ind], axis=axis) return noise names = ['ic348', 'ngc1333', 'ophA'] for name in names: cube = SpectralCube.read('{}.13co.fits'.format(name)) pixscale = proj_plane_pixel_scales(cube.wcs)[0] # cube = cube.with_mask(cube > 3 * scale * u.Jy) # # Want to smooth the mask edges mask = cube.mask.include() # Set smoothing parameters and # consecutive channels. smooth_chans = int(round_up_to_odd(200. / 66.)) # 10 consecutive channels must be above the MAD level to be real emission. num_chans = 7 peak_snr = 4.5 # Cutoff level min_snr = 3
params["PA_err"] = float(contents[39].split()[-1]) params["eps"] = float(contents[40].split()[-3]) params["eps_err"] = float(contents[40].split()[-1]) params["inc"] = float(contents[41].split()[-3]) params["inc_err"] = float(contents[41].split()[-1]) # Both x and y are on the same line params["xcent"] = float(contents[42].split()[-6]) params["xcent_err"] = float(contents[42].split()[-4][:-1]) params["ycent"] = float(contents[42].split()[-3]) params["ycent_err"] = float(contents[42].split()[-1]) # Now convert xcent and ycent to RA and Dec. params["RAcent"], params["Deccent"] = \ mywcs.celestial.wcs_pix2world(params["xcent"], params["ycent"], 0) # Add angular uncertainties in deg. pix_scale = proj_plane_pixel_scales(mywcs.celestial) params["RAcent_err"] = pix_scale[0] * params["xcent_err"] params["Deccent_err"] = pix_scale[1] * params["ycent_err"] params["Vsys"] = float(contents[44].split()[-3]) params["Vsys_err"] = float(contents[44].split()[-1]) params["points_used"] = float(contents[56].split()[-1]) params["iterations"] = float(contents[57].split()[-1]) params["chi^2"] = float(contents[58].split()[-1]) params["DOF"] = float(contents[59].split()[-1]) # Column names are always on line 62, and data starts on 64 colnames = contents[62].split() data = []
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
def test_pixscale_cd(): mywcs = WCS(naxis=2) mywcs.wcs.cd = [[-0.1, 0], [0, 0.2]] mywcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] assert_almost_equal(proj_plane_pixel_scales(mywcs), (0.1, 0.2))
TMASSfile = os.path.join(TMASSdir, '_'.join([thisTarget, thisWaveband]) + '.fits') TMASSimg = AstroImage(TMASSfile) PSFproperties = TMASSimg.get_PSF() TMASS_PSFwidth = np.sqrt(PSFproperties[0]*PSFproperties[1]) # Grab image dimensions ny, nx = TMASSimg.arr.shape yc, xc = np.array(TMASSimg.arr.shape)//2 # Grab the image center coordinates TMASSwcs = WCS(TMASSimg.header) RA_cen, Dec_cen = TMASSwcs.wcs_pix2world(xc, yc, 0) TMASS_pl_sc = proj_plane_pixel_scales(TMASSwcs) TMASS_pl_sc = np.sqrt(TMASS_pl_sc[0]*TMASS_pl_sc[1])*3600.0 # Compute distances from image center yy, xx = np.mgrid[0:ny, 0:nx] dist = np.sqrt((xx - xc)**2 + (yy - yc)**2) dist *= TMASS_pl_sc # Check if there are specified "good angles" for this target. If # there are, then build a mask to make sure radial profiles are # constructed using only user-specified regions of the nebula. if thisTarget in angleDict.keys(): # This target has a specified "good angle" range. TMASS_angleMap = (np.rad2deg(np.arctan2((yy - yc), (xx - xc))) - 90.0 + 360.0) % 360.0 minAng, maxAng = np.min(angleDict[thisTarget]), np.max(angleDict[thisTarget])
def __init__(self, data, position, size, wcs=None, mode='trim', fill_value=np.nan, copy=False): if isinstance(position, SkyCoord): if wcs is None: raise ValueError('wcs must be input if position is a ' 'SkyCoord') position = skycoord_to_pixel(position, wcs, mode='all') # (x, y) if np.isscalar(size): size = np.repeat(size, 2) # special handling for a scalar Quantity if isinstance(size, u.Quantity): size = np.atleast_1d(size) if len(size) == 1: size = np.repeat(size, 2) if len(size) > 2: raise ValueError('size must have at most two elements') shape = np.zeros(2).astype(int) pixel_scales = None # ``size`` can have a mixture of int and Quantity (and even units), # so evaluate each axis separately for axis, side in enumerate(size): if not isinstance(side, u.Quantity): shape[axis] = int(np.round(size[axis])) # pixels else: if side.unit == u.pixel: shape[axis] = int(np.round(side.value)) elif side.unit.physical_type == 'angle': if wcs is None: raise ValueError('wcs must be input if any element ' 'of size has angular units') if pixel_scales is None: pixel_scales = u.Quantity( proj_plane_pixel_scales(wcs), wcs.wcs.cunit[axis]) shape[axis] = int(np.round( (side / pixel_scales[axis]).decompose())) else: raise ValueError('shape can contain Quantities with only ' 'pixel or angular units') data = np.asanyarray(data) # reverse position because extract_array and overlap_slices # use (y, x), but keep the input position pos_yx = position[::-1] cutout_data, input_position_cutout = extract_array( data, tuple(shape), pos_yx, mode=mode, fill_value=fill_value, return_position=True) if copy: cutout_data = np.copy(cutout_data) self.data = cutout_data self.input_position_cutout = input_position_cutout[::-1] # (x, y) slices_original, slices_cutout = overlap_slices( data.shape, shape, pos_yx, mode=mode) self.slices_original = slices_original self.slices_cutout = slices_cutout self.shape = self.data.shape self.input_position_original = position self.shape_input = shape ((self.ymin_original, self.ymax_original), (self.xmin_original, self.xmax_original)) = self.bbox_original ((self.ymin_cutout, self.ymax_cutout), (self.xmin_cutout, self.xmax_cutout)) = self.bbox_cutout # the true origin pixel of the cutout array, including any # filled cutout values self._origin_original_true = ( self.origin_original[0] - self.slices_cutout[1].start, self.origin_original[1] - self.slices_cutout[0].start) if wcs is not None: self.wcs = deepcopy(wcs) self.wcs.wcs.crpix -= self._origin_original_true self.wcs.array_shape = self.data.shape if wcs.sip is not None: self.wcs.sip = Sip(wcs.sip.a, wcs.sip.b, wcs.sip.ap, wcs.sip.bp, wcs.sip.crpix - self._origin_original_true) else: self.wcs = None
def make_signal_mask(cube, smooth_chans=200. / 66., min_chan=7, peak_snr=5., min_snr=3.5, edge_thresh=1.5, verbose=False): ''' Create a robust signal mask by requiring spatial and spectral connectivity. ''' import astropy.units as u from astropy.convolution import Box1DKernel from signal_id import Noise from scipy import ndimage as nd from astropy.wcs.utils import proj_plane_pixel_scales from astropy.utils.console import ProgressBar import skimage.morphology as mo import numpy as np from radio_beam import Beam from itertools import groupby, chain from operator import itemgetter import matplotlib.pyplot as p pixscale = proj_plane_pixel_scales(cube.wcs)[0] # # Want to smooth the mask edges mask = cube.mask.include().copy() # Set smoothing parameters and # consecutive channels. smooth_chans = int(round_up_to_odd(smooth_chans)) # consecutive channels to be real emission. num_chans = min_chan # Smooth the cube, then create a noise model spec_kernel = Box1DKernel(smooth_chans) smooth_cube = cube.spectral_smooth(spec_kernel) noise = Noise(smooth_cube) noise.estimate_noise(spectral_flat=True) noise.get_scale_cube() snr = noise.snr.copy() snr[np.isnan(snr)] = 0.0 posns = np.where(snr.max(axis=0) >= min_snr) bad_pos = np.where(snr.max(axis=0) < min_snr) mask[:, bad_pos[0], bad_pos[1]] = False for i, j in ProgressBar(zip(*posns)): # Look for all pixels above min_snr good_posns = np.where(snr[:, i, j] > min_snr)[0] # Reject if the total is less than connectivity requirement if good_posns.size < num_chans: mask[:, i, j] = False continue # Find connected pixels sequences = [] for k, g in groupby(enumerate(good_posns), lambda (i, x): i - x): sequences.append(map(itemgetter(1), g)) # Check length and peak. Require a minimum of 3 pixels above the noise # to grow from. sequences = [seq for seq in sequences if len(seq) >= 3 and np.nanmax(snr[:, i, j][seq]) >= peak_snr] # Continue if no good sequences found if len(sequences) == 0: mask[:, i, j] = False continue # Now take each valid sequence and expand the edges until the smoothed # spectrum approaches zero. edges = [[seq[0], seq[-1]] for seq in sequences] for n, edge in enumerate(edges): # Lower side if n == 0: start_posn = edge[0] stop_posn = 0 else: start_posn = edge[0] - edges[n - 1][0] stop_posn = edges[n - 1][0] for pt in np.arange(start_posn, stop_posn, -1): # if smoothed[pt] <= mad * edge_thresh: if snr[:, i, j][pt] <= edge_thresh: break sequences[n].insert(0, pt) # Upper side start_posn = edge[1] if n == len(edges) - 1: stop_posn = cube.shape[0] else: stop_posn = edges[n + 1][0] for pt in np.arange(start_posn, stop_posn, 1): # if smoothed[pt] <= mad * edge_thresh: if snr[:, i, j][pt] <= edge_thresh: break sequences[n].insert(0, pt) # Final check for the min peak level and ensure all meet the # spectral connectivity requirement sequences = [seq for seq in sequences if len(seq) >= num_chans and np.nanmax(snr[:, i, j][seq]) >= peak_snr] if len(sequences) == 0: mask[:, i, j] = False continue bad_posns = \ list(set(np.arange(cube.shape[0])) - set(list(chain(*sequences)))) mask[:, i, j][bad_posns] = False if verbose: p.subplot(121) p.plot(cube.spectral_axis.value, noise.snr[:, i, j]) min_val = cube.spectral_axis.value[np.where(mask[:, i, j])[0][-1]] max_val = cube.spectral_axis.value[np.where(mask[:, i, j])[0][0]] p.vlines(min_val, 0, np.nanmax(noise.snr[:, i, j])) p.vlines(max_val, 0, np.nanmax(noise.snr[:, i, j])) p.plot(cube.spectral_axis.value, noise.snr[:, i, j] * mask[:, i, j], 'bD') p.subplot(122) p.plot(cube.spectral_axis.value, cube[:, i, j], label='Cube') p.plot(cube.spectral_axis.value, smooth_cube[:, i, j], label='Smooth Cube') p.axvline(min_val) p.axvline(max_val) p.plot(cube.spectral_axis.value, smooth_cube[:, i, j] * mask[:, i, j], 'bD') p.draw() raw_input("Next spectrum?") p.clf()