Package astLib :: Module astImages
[hide private]
[frames] | no frames]

Source Code for Module astLib.astImages

  1  # -*- coding: utf-8 -*- 
  2  """module for simple .fits image tasks (rotation, clipping out sections, making .pngs etc.) 
  3   
  4  (c) 2007-2009 Matt Hilton  
  5   
  6  U{http://astlib.sourceforge.net} 
  7   
  8  Some routines in this module will fail if, e.g., asked to clip a section from a .fits image at a 
  9  position not found within the image (as determined using the WCS). Where this occurs, the function 
 10  will return None. An error message will be printed to the console when this happens if 
 11  astImages.REPORT_ERRORS=True (the default). Testing if an astImages function returns None can be 
 12  used to handle errors in scripts.  
 13   
 14  """ 
 15   
 16  REPORT_ERRORS=True 
 17   
 18  import os 
 19  import sys 
 20  import math 
 21  from astLib import astWCS 
 22  import pyfits 
 23  try: 
 24      from scipy import ndimage 
 25      from scipy import interpolate 
 26  except: 
 27      print "WARNING: astImages: failed to import scipy.ndimage - some functions will not work." 
 28  import numpy 
 29  try: 
 30      import matplotlib 
 31      from matplotlib import pylab 
 32      matplotlib.interactive(False) 
 33  except: 
 34      print "WARNING: astImages: failed to import matplotlib - some functions will not work." 
 35  try: 
 36      import Image 
 37  except: 
 38      print "WARNING: astImages: failed to import Image - some functions will not work." 
 39   
 40  #--------------------------------------------------------------------------------------------------- 
41 -def clipImageSectionWCS(imageData, imageWCS, RADeg, decDeg, clipSizeDeg, returnWCS = True):
42 """Clips a square or rectangular section from an image array at the given celestial coordinates. 43 An updated WCS for the clipped section is optionally returned. 44 45 @type imageData: numpy array 46 @param imageData: image data array 47 @type imageWCS: astWCS.WCS 48 @param imageWCS: astWCS.WCS object 49 @type RADeg: float 50 @param RADeg: coordinate in decimal degrees 51 @type decDeg: float 52 @param decDeg: coordinate in decimal degrees 53 @type clipSizeDeg: float or list in format [widthDeg, heightDeg] 54 @param clipSizeDeg: if float, size of square clipped section in decimal degrees; if list, 55 size of clipped section in degrees in x, y axes of image respectively 56 @type returnWCS: bool 57 @param returnWCS: if True, return an updated WCS for the clipped section 58 @rtype: dictionary 59 @return: clipped image section (numpy array), updated astWCS WCS object for 60 clipped image section, in format {'data', 'wcs'}. 61 62 """ 63 64 imHeight=imageData.shape[0] 65 imWidth=imageData.shape[1] 66 imScale=imageWCS.getPixelSizeDeg() 67 68 if type(clipSizeDeg) == float: 69 xHalfClipSizeDeg=clipSizeDeg/2.0 70 xHalfSizePix=xHalfClipSizeDeg/imScale 71 yHalfClipSizeDeg=xHalfClipSizeDeg 72 yHalfSizePix=xHalfSizePix 73 elif type(clipSizeDeg) == list or type(clipSizeDeg) == tuple: 74 xHalfClipSizeDeg=clipSizeDeg[0]/2.0 75 yHalfClipSizeDeg=clipSizeDeg[1]/2.0 76 xHalfSizePix=xHalfClipSizeDeg/imScale 77 yHalfSizePix=yHalfClipSizeDeg/imScale 78 else: 79 raise Exception, "did not understand clipSizeDeg: should be float, or [widthDeg, heightDeg]" 80 81 cPixCoords=imageWCS.wcs2pix(RADeg, decDeg) 82 83 cTopLeft=[cPixCoords[0]+xHalfSizePix, cPixCoords[1]+yHalfSizePix] 84 cBottomRight=[cPixCoords[0]-xHalfSizePix, cPixCoords[1]-yHalfSizePix] 85 86 X=[int(round(cTopLeft[0])),int(round(cBottomRight[0]))] 87 Y=[int(round(cTopLeft[1])),int(round(cBottomRight[1]))] 88 89 X.sort() 90 Y.sort() 91 92 if X[0] < 0: 93 X[0]=0 94 if X[1] > imWidth: 95 X[1]=imWidth 96 if Y[0] < 0: 97 Y[0]=0 98 if Y[1] > imHeight: 99 Y[1]=imHeight 100 101 clippedData=imageData[Y[0]:Y[1],X[0]:X[1]] 102 103 # Update WCS 104 if returnWCS == True: 105 try: 106 oldCRPIX1=imageWCS.header['CRPIX1'] 107 oldCRPIX2=imageWCS.header['CRPIX2'] 108 clippedWCS=imageWCS.copy() 109 clippedWCS.header.update('NAXIS1', clippedData.shape[1]) 110 clippedWCS.header.update('NAXIS2', clippedData.shape[0]) 111 clippedWCS.header.update('CRPIX1', oldCRPIX1-X[0]) 112 clippedWCS.header.update('CRPIX2', oldCRPIX2-Y[0]) 113 clippedWCS.updateFromHeader() 114 115 except KeyError: 116 117 if REPORT_ERRORS == True: 118 119 print "WARNING: astImages.clipImageSectionWCS() : no CRPIX1, CRPIX2 keywords found - not updating clipped image WCS." 120 121 clippedData=imageData[Y[0]:Y[1],X[0]:X[1]] 122 clippedWCS=imageWCS.copy() 123 else: 124 clippedWCS=None 125 126 return {'data': clippedData, 'wcs': clippedWCS}
127 128 #---------------------------------------------------------------------------------------------------
129 -def clipImageSectionPix(imageData, XCoord, YCoord, clipSizePix):
130 """Clips a square or rectangular section from an image array at the given pixel coordinates. 131 132 @type imageData: numpy array 133 @param imageData: image data array 134 @type XCoord: float 135 @param XCoord: coordinate in pixels 136 @type YCoord: float 137 @param YCoord: coordinate in pixels 138 @type clipSizePix: float or list in format [widthPix, heightPix] 139 @param clipSizePix: if float, size of square clipped section in pixels; if list, 140 size of clipped section in pixels in x, y axes of output image respectively 141 @rtype: numpy array 142 @return: clipped image section 143 144 """ 145 146 imHeight=imageData.shape[0] 147 imWidth=imageData.shape[1] 148 149 if type(clipSizePix) == float or type(clipSizePix) == int: 150 xHalfClipSizePix=int(round(clipSizePix/2.0)) 151 yHalfClipSizePix=xHalfClipSizePix 152 elif type(clipSizePix) == list or type(clipSizePix) == tuple: 153 xHalfClipSizePix=int(round(clipSizePix[0]/2.0)) 154 yHalfClipSizePix=int(round(clipSizePix[1]/2.0)) 155 else: 156 raise Exception, "did not understand clipSizePix: should be float, or [widthPix, heightPix]" 157 158 cTopLeft=[XCoord+xHalfClipSizePix, YCoord+yHalfClipSizePix] 159 cBottomRight=[XCoord-xHalfClipSizePix, YCoord-yHalfClipSizePix] 160 161 X=[int(round(cTopLeft[0])),int(round(cBottomRight[0]))] 162 Y=[int(round(cTopLeft[1])),int(round(cBottomRight[1]))] 163 164 X.sort() 165 Y.sort() 166 167 if X[0] < 0: 168 X[0]=0 169 if X[1] > imWidth: 170 X[1]=imWidth 171 if Y[0] < 0: 172 Y[0]=0 173 if Y[1] > imHeight: 174 Y[1]=imHeight 175 176 return imageData[Y[0]:Y[1],X[0]:X[1]]
177 178 #---------------------------------------------------------------------------------------------------
179 -def clipRotatedImageSectionWCS(imageData, imageWCS, RADeg, decDeg, clipSizeDeg, returnWCS = True):
180 """Clips a square or rectangular section from an image array at the given celestial coordinates. 181 The resulting clip is rotated and/or flipped such that North is at the top, and East appears at 182 the left. An updated WCS for the clipped section is also returned. Note that the alignment 183 of the rotated WCS is currently not perfect - however, it is probably good enough in most 184 cases for use with L{ImagePlot} for plotting purposes. 185 186 @type imageData: numpy array 187 @param imageData: image data array 188 @type imageWCS: astWCS.WCS 189 @param imageWCS: astWCS.WCS object 190 @type RADeg: float 191 @param RADeg: coordinate in decimal degrees 192 @type decDeg: float 193 @param decDeg: coordinate in decimal degrees 194 @type clipSizeDeg: float 195 @param clipSizeDeg: if float, size of square clipped section in decimal degrees; if list, 196 size of clipped section in degrees in RA, dec. axes of output rotated image respectively 197 @rtype: numpy array 198 @return: clipped image section (numpy array), updated astWCS WCS object for 199 clipped image section, in format {'data', 'wcs'}. 200 201 @note: Returns 'None' if the requested position is not found within the image. If the image 202 WCS does not have keywords of the form CD1_1 etc., the output WCS will not be rotated. 203 204 """ 205 206 halfImageSize=imageWCS.getHalfSizeDeg() 207 imageCentre=imageWCS.getCentreWCSCoords() 208 imScale=imageWCS.getPixelSizeDeg() 209 210 if type(clipSizeDeg) == float: 211 xHalfClipSizeDeg=clipSizeDeg/2.0 212 yHalfClipSizeDeg=xHalfClipSizeDeg 213 elif type(clipSizeDeg) == list or type(clipSizeDeg) == tuple: 214 xHalfClipSizeDeg=clipSizeDeg[0]/2.0 215 yHalfClipSizeDeg=clipSizeDeg[1]/2.0 216 else: 217 raise Exception, "did not understand clipSizeDeg: should be float, or [widthDeg, heightDeg]" 218 219 diagonalHalfSizeDeg=math.sqrt((xHalfClipSizeDeg*xHalfClipSizeDeg) \ 220 +(yHalfClipSizeDeg*yHalfClipSizeDeg)) 221 222 diagonalHalfSizePix=diagonalHalfSizeDeg/imScale 223 224 if RADeg>imageCentre[0]-halfImageSize[0] and RADeg<imageCentre[0]+halfImageSize[0] \ 225 and decDeg>imageCentre[1]-halfImageSize[1] and decDeg<imageCentre[1]+halfImageSize[1]: 226 227 imageDiagonalClip=clipImageSectionWCS(imageData, imageWCS, RADeg, 228 decDeg, diagonalHalfSizeDeg*2.0) 229 diagonalClip=imageDiagonalClip['data'] 230 diagonalWCS=imageDiagonalClip['wcs'] 231 232 rotDeg=diagonalWCS.getRotationDeg() 233 imageRotated=ndimage.rotate(diagonalClip, rotDeg) 234 if diagonalWCS.isFlipped() == 1: 235 imageRotated=pylab.fliplr(imageRotated) 236 237 # Handle WCS rotation 238 rotatedWCS=diagonalWCS.copy() 239 rotRadians=math.radians(rotDeg) 240 241 if returnWCS == True: 242 try: 243 244 CD11=rotatedWCS.header['CD1_1'] 245 CD21=rotatedWCS.header['CD2_1'] 246 CD12=rotatedWCS.header['CD1_2'] 247 CD22=rotatedWCS.header['CD2_2'] 248 if rotatedWCS.isFlipped() == 1: 249 CD11=CD11*-1 250 CD12=CD12*-1 251 CDMatrix=numpy.array([[CD11, CD12], [CD21, CD22]], dtype=numpy.float64) 252 253 rotRadians=rotRadians 254 rot11=math.cos(rotRadians) 255 rot12=math.sin(rotRadians) 256 rot21=-math.sin(rotRadians) 257 rot22=math.cos(rotRadians) 258 rotMatrix=numpy.array([[rot11, rot12], [rot21, rot22]], dtype=numpy.float64) 259 newCDMatrix=numpy.dot(rotMatrix, CDMatrix) 260 261 P1=diagonalWCS.header['CRPIX1'] 262 P2=diagonalWCS.header['CRPIX2'] 263 V1=diagonalWCS.header['CRVAL1'] 264 V2=diagonalWCS.header['CRVAL2'] 265 266 PMatrix=numpy.zeros((2,), dtype = numpy.float64) 267 PMatrix[0]=P1 268 PMatrix[1]=P2 269 270 # BELOW IS HOW TO WORK OUT THE NEW REF PIXEL 271 CMatrix=numpy.array([imageRotated.shape[1]/2.0, imageRotated.shape[0]/2.0]) 272 centreCoords=diagonalWCS.getCentreWCSCoords() 273 alphaRad=math.radians(centreCoords[0]) 274 deltaRad=math.radians(centreCoords[1]) 275 thetaRad=math.asin(math.sin(deltaRad)*math.sin(math.radians(V2)) + \ 276 math.cos(deltaRad)*math.cos(math.radians(V2))*math.cos(alphaRad-math.radians(V1))) 277 phiRad=math.atan2(-math.cos(deltaRad)*math.sin(alphaRad-math.radians(V1)), \ 278 math.sin(deltaRad)*math.cos(math.radians(V2)) - \ 279 math.cos(deltaRad)*math.sin(math.radians(V2))*math.cos(alphaRad-math.radians(V1))) + \ 280 math.pi 281 RTheta=(180.0/math.pi)*(1.0/math.tan(thetaRad)) 282 283 xy=numpy.zeros((2,), dtype=numpy.float64) 284 xy[0]=RTheta*math.sin(phiRad) 285 xy[1]=-RTheta*math.cos(phiRad) 286 newPMatrix=CMatrix - numpy.dot(numpy.linalg.inv(newCDMatrix), xy) 287 288 # But there's a small offset to CRPIX due to the rotatedImage being rounded to an integer 289 # number of pixels (not sure this helps much) 290 #d=numpy.dot(rotMatrix, [diagonalClip.shape[1], diagonalClip.shape[0]]) 291 #offset=abs(d)-numpy.array(imageRotated.shape) 292 293 rotatedWCS.header.update('NAXIS1', imageRotated.shape[1]) 294 rotatedWCS.header.update('NAXIS2', imageRotated.shape[0]) 295 rotatedWCS.header.update('CRPIX1', newPMatrix[0]) 296 rotatedWCS.header.update('CRPIX2', newPMatrix[1]) 297 rotatedWCS.header.update('CRVAL1', V1) 298 rotatedWCS.header.update('CRVAL2', V2) 299 rotatedWCS.header.update('CD1_1', newCDMatrix[0][0]) 300 rotatedWCS.header.update('CD2_1', newCDMatrix[1][0]) 301 rotatedWCS.header.update('CD1_2', newCDMatrix[0][1]) 302 rotatedWCS.header.update('CD2_2', newCDMatrix[1][1]) 303 rotatedWCS.updateFromHeader() 304 305 except KeyError: 306 307 if REPORT_ERRORS == True: 308 print "WARNING: astImages.clipRotatedImageSectionWCS() : no CDi_j keywords found - not rotating WCS." 309 310 imageRotated=diagonalClip 311 rotatedWCS=diagonalWCS 312 313 imageRotatedClip=clipImageSectionWCS(imageRotated, rotatedWCS, RADeg, decDeg, clipSizeDeg) 314 315 if returnWCS == True: 316 return {'data': imageRotatedClip['data'], 'wcs': imageRotatedClip['wcs']} 317 else: 318 return {'data': imageRotatedClip['data'], 'wcs': None} 319 320 else: 321 322 if REPORT_ERRORS==True: 323 print """ERROR: astImages.clipRotatedImageSectionWCS() : 324 RADeg, decDeg are not within imageData.""" 325 326 return None
327 328 #---------------------------------------------------------------------------------------------------
329 -def scaleImage(imageData, imageWCS, scaleFactor):
330 """Scales image array and WCS by the given scale factor. 331 332 @type imageData: numpy array 333 @param imageData: image data array 334 @type imageWCS: astWCS.WCS 335 @param imageWCS: astWCS.WCS object 336 @type scaleFactor: float 337 @param scaleFactor: factor to resize image by 338 @rtype: dictionary 339 @return: image data (numpy array), updated astWCS WCS object for image, in format {'data', 'wcs'}. 340 341 """ 342 343 scaledData=ndimage.zoom(imageData, scaleFactor) 344 345 # Take care of offset due to rounding in scaling image to integer pixel dimensions 346 properDimensions=numpy.array(imageData.shape)*scaleFactor 347 offset=properDimensions-numpy.array(scaledData.shape) 348 349 # Rescale WCS 350 try: 351 oldCRPIX1=imageWCS.header['CRPIX1'] 352 oldCRPIX2=imageWCS.header['CRPIX2'] 353 CD11=imageWCS.header['CD1_1'] 354 CD21=imageWCS.header['CD2_1'] 355 CD12=imageWCS.header['CD1_2'] 356 CD22=imageWCS.header['CD2_2'] 357 358 scaledWCS=imageWCS.copy() 359 scaledWCS.header.update('NAXIS1', scaledData.shape[1]) 360 scaledWCS.header.update('NAXIS2', scaledData.shape[0]) 361 scaledWCS.header.update('CRPIX1', oldCRPIX1*scaleFactor+offset[1]) 362 scaledWCS.header.update('CRPIX2', oldCRPIX2*scaleFactor+offset[0]) 363 scaledWCS.header.update('CD1_1', CD11/scaleFactor) 364 scaledWCS.header.update('CD2_1', CD21/scaleFactor) 365 scaledWCS.header.update('CD1_2', CD12/scaleFactor) 366 scaledWCS.header.update('CD2_2', CD22/scaleFactor) 367 scaledWCS.updateFromHeader() 368 369 except KeyError: 370 371 if REPORT_ERRORS == True: 372 373 print "WARNING: astImages.rescaleImage() : no CDij, keywords found - not updating WCS." 374 scaledWCS=imageWCS.copy() 375 376 return {'data': scaledData, 'wcs': scaledWCS}
377 378 #---------------------------------------------------------------------------------------------------
379 -def intensityCutImage(imageData, cutLevels):
380 """Creates a matplotlib.pylab plot of an image array with the specified cuts in intensity 381 applied. This routine is used by L{saveBitmap} and L{saveContourOverlayBitmap}, which both 382 produce output as .png, .jpg, etc. images. 383 384 @type imageData: numpy array 385 @param imageData: image data array 386 @type cutLevels: list 387 @param cutLevels: sets the image scaling - available options: 388 - pixel values: cutLevels=[low value, high value]. 389 - histogram equalisation: cutLevels=["histEq", number of bins ( e.g. 1024)] 390 - relative: cutLevels=["relative", cut per cent level (e.g. 99.5)] 391 - smart: cutLevels=["smart", cut per cent level (e.g. 99.5)] 392 ["smart", 99.5] seems to provide good scaling over a range of different images. 393 @rtype: dictionary 394 @return: image section (numpy.array), matplotlib image normalisation (matplotlib.colors.Normalize), in the format {'image', 'norm'}. 395 396 @note: If cutLevels[0] == "histEq", then only {'image'} is returned. 397 398 """ 399 400 oImWidth=imageData.shape[1] 401 oImHeight=imageData.shape[0] 402 403 # Optional histogram equalisation 404 if cutLevels[0]=="histEq": 405 406 imageData=histEq(imageData, cutLevels[1]) 407 anorm=pylab.normalize(imageData.min(), imageData.max()) 408 409 elif cutLevels[0]=="relative": 410 411 # this turns image data into 1D array then sorts 412 sorted=numpy.sort(numpy.ravel(imageData)) 413 maxValue=sorted.max() 414 minValue=sorted.min() 415 416 # want to discard the top and bottom specified 417 topCutIndex=len(sorted-1) \ 418 -int(math.floor(float((100.0-cutLevels[1])/100.0)*len(sorted-1))) 419 bottomCutIndex=int(math.ceil(float((100.0-cutLevels[1])/100.0)*len(sorted-1))) 420 topCut=sorted[topCutIndex] 421 bottomCut=sorted[bottomCutIndex] 422 anorm=pylab.normalize(bottomCut, topCut) 423 424 elif cutLevels[0]=="smart": 425 426 # this turns image data into 1Darray then sorts 427 sorted=numpy.sort(numpy.ravel(imageData)) 428 maxValue=sorted.max() 429 minValue=sorted.min() 430 numBins=10000 # 0.01 per cent accuracy 431 binWidth=(maxValue-minValue)/float(numBins) 432 histogram=ndimage.histogram(sorted, minValue, maxValue, numBins) 433 434 # Find the bin with the most pixels in it, set that as our minimum 435 # Then search through the bins until we get to a bin with more/or the same number of 436 # pixels in it than the previous one. 437 # We take that to be the maximum. 438 # This means that we avoid the traps of big, bright, saturated stars that cause 439 # problems for relative scaling 440 backgroundValue=histogram.max() 441 foundBackgroundBin=False 442 foundTopBin=False 443 lastBin=-10000 444 for i in range(len(histogram)): 445 446 if histogram[i]>=lastBin and foundBackgroundBin==True: 447 448 # Added a fudge here to stop us picking for top bin a bin within 449 # 10 percent of the background pixel value 450 if (minValue+(binWidth*i))>bottomBinValue*1.1: 451 topBinValue=minValue+(binWidth*i) 452 foundTopBin=True 453 break 454 455 if histogram[i]==backgroundValue and foundBackgroundBin==False: 456 bottomBinValue=minValue+(binWidth*i) 457 foundBackgroundBin=True 458 459 lastBin=histogram[i] 460 461 if foundTopBin==False: 462 topBinValue=maxValue 463 464 #Now we apply relative scaling to this 465 smartClipped=numpy.clip(sorted, bottomBinValue, topBinValue) 466 topCutIndex=len(smartClipped-1) \ 467 -int(math.floor(float((100.0-cutLevels[1])/100.0)*len(smartClipped-1))) 468 bottomCutIndex=int(math.ceil(float((100.0-cutLevels[1])/100.0)*len(smartClipped-1))) 469 topCut=smartClipped[topCutIndex] 470 bottomCut=smartClipped[bottomCutIndex] 471 anorm=pylab.normalize(bottomCut, topCut) 472 else: 473 474 # Normalise using given cut levels 475 anorm=pylab.normalize(cutLevels[0], cutLevels[1]) 476 477 if cutLevels[0]=="histEq": 478 return {'image': imageData.copy()} 479 else: 480 return {'image': imageData.copy(), 'norm': anorm}
481 482 #---------------------------------------------------------------------------------------------------
483 -def resampleToWCS(im1Data, im1WCS, im2Data, im2WCS, preserveFlux = False):
484 """Resamples data corresponding to second image (with data im2Data, WCS im2WCS) onto the WCS 485 of the first image (im1Data, im1WCS). The output, resampled image is of the pixel same 486 dimensions of the first image. No interpolation is performed - output is NOT suitable 487 for photometry (this routine is for assisting in plotting). 488 489 @type im1Data: numpy array 490 @param im1Data: image data array for first image 491 @type im1WCS: astWCS.WCS 492 @param im1WCS: astWCS.WCS object corresponding to im1Data 493 @type im2Data: numpy array 494 @param im2Data: image data array for second image (to be resampled to match first image) 495 @type im2WCS: astWCS.WCS 496 @param im2WCS: astWCS.WCS object corresponding to im2Data 497 @type preserveFlux: bool 498 @param preserveFlux: if True, scales the values in the resampled image to preserve flux 499 @rtype: dictionary 500 @return: numpy image data array and associated WCS in format {'data', 'wcs'} 501 502 """ 503 504 resampledData=numpy.zeros(im1Data.shape) 505 506 for x in range(im1Data.shape[1]): 507 for y in range(im1Data.shape[0]): 508 RA, dec=im1WCS.pix2wcs(x, y) 509 x2, y2=im2WCS.wcs2pix(RA, dec) 510 x2=int(round(x2)) 511 y2=int(round(y2)) 512 if x2 >= 0 and x2 < im2Data.shape[1] and y2 >= 0 and y2 < im2Data.shape[0]: 513 resampledData[y][x]=im2Data[y2][x2] 514 515 # Preserve flux in the resampled image 516 if preserveFlux == True: 517 pixAreaRatio=im2WCS.getPixelSizeDeg()**2/im1WCS.getPixelSizeDeg()**2 518 resampledData=resampledData/pixAreaRatio 519 520 # Note: should really just copy im1WCS keywords into im2WCS and return that 521 # Only a problem if we're using this for anything other than plotting 522 return {'data': resampledData, 'wcs': im1WCS.copy()}
523 524 #---------------------------------------------------------------------------------------------------
525 -def generateContourOverlay(backgroundImageData, backgroundImageWCS, contourImageData, contourImageWCS, \ 526 contourLevels, contourSmoothFactor = 0):
527 """Rescales an image array to be used as a contour overlay to have the same dimensions as the 528 background image, and generates a set of contour levels suitable for plotting by e.g. 529 L{saveContourOverlayBitmap}. The image array from which the contours are to be generated will be 530 resampled to the same dimensions as the background image data, and can be optionally smoothed 531 using a Gaussian filter. The sigma of the Gaussian filter (contourSmoothFactor) is specified in 532 arcsec. 533 534 @type backgroundImageData: numpy array 535 @param backgroundImageData: background image data array 536 @type backgroundImageWCS: astWCS.WCS 537 @param backgroundImageWCS: astWCS.WCS object of the background image data array 538 @type contourImageData: numpy array 539 @param contourImageData: image data array from which contours are to be generated 540 @type contourImageWCS: astWCS.WCS 541 @param contourImageWCS: astWCS.WCS object corresponding to contourImageData 542 @type contourLevels: list 543 @param contourLevels: sets the contour levels - available options: 544 - values: contourLevels=[list of values specifying each level] 545 - linear spacing: contourLevels=['linear', min level value, max level value, number 546 of levels] 547 - log spacing: contourLevels=['log', min level value, max level value, number of 548 levels] 549 @type contourSmoothFactor: float 550 @param contourSmoothFactor: standard deviation (in pixels) of Gaussian filter for 551 pre-smoothing of contour image data (set to 0 for no smoothing) 552 553 """ 554 555 # For compromise between speed and accuracy, scale a copy of the background 556 # image down to a scale that is one pixel = 1/5 of a pixel in the contour image 557 scaleFactor=backgroundImageWCS.getPixelSizeDeg()/(contourImageWCS.getPixelSizeDeg()/5.0) 558 scaledBackground=scaleImage(backgroundImageData, backgroundImageWCS, scaleFactor) 559 scaled=resampleToWCS(scaledBackground['data'], scaledBackground['wcs'], 560 contourImageData, contourImageWCS) 561 scaledContourData=scaled['data'] 562 scaledContourWCS=scaled['wcs'] 563 564 if contourSmoothFactor > 0: 565 sigmaPix=(contourSmoothFactor/3600.0)/scaledContourWCS.getPixelSizeDeg() 566 scaledContourData=ndimage.gaussian_filter(scaledContourData, sigmaPix) 567 568 # Various ways of setting the contour levels 569 # If just a list is passed in, use those instead 570 if contourLevels[0] == "linear": 571 xMin=float(contourLevels[1]) 572 xMax=float(contourLevels[2]) 573 nLevels=contourLevels[3] 574 xStep=(xMax-xMin)/(nLevels-1) 575 cLevels=[] 576 for j in range(nLevels+1): 577 level=xMin+j*xStep 578 cLevels.append(level) 579 580 elif contourLevels[0] == "log": 581 xMin=float(contourLevels[1]) 582 xMax=float(contourLevels[2]) 583 if xMin <= 0.0: 584 raise Exception, "minimum contour level set to <= 0 and log scaling chosen." 585 xLogMin=math.log10(xMin) 586 xLogMax=math.log10(xMax) 587 nLevels=contourLevels[3] 588 xLogStep=(xLogMax-xLogMin)/(nLevels-1) 589 cLevels=[] 590 prevLevel=0 591 for j in range(nLevels+1): 592 level=math.pow(10, xLogMin+j*xLogStep) 593 cLevels.append(level) 594 595 else: 596 cLevels=contourLevels 597 598 # Now blow the contour image data back up to the size of the original image 599 scaledBack=scaleImage(scaledContourData, scaledContourWCS, 1.0/scaleFactor) 600 601 return {'scaledImage': scaledBack['data'], 'contourLevels': cLevels}
602 603 #---------------------------------------------------------------------------------------------------
604 -def saveBitmap(outputFileName, imageData, cutLevels, size, colorMapName):
605 """Makes a bitmap image from an image array; the image format is specified by the 606 filename extension. (e.g. ".jpg" =JPEG, ".png"=PNG). 607 608 @type outputFileName: string 609 @param outputFileName: filename of output bitmap image 610 @type imageData: numpy array 611 @param imageData: image data array 612 @type cutLevels: list 613 @param cutLevels: sets the image scaling - available options: 614 - pixel values: cutLevels=[low value, high value]. 615 - histogram equalisation: cutLevels=["histEq", number of bins ( e.g. 1024)] 616 - relative: cutLevels=["relative", cut per cent level (e.g. 99.5)] 617 - smart: cutLevels=["smart", cut per cent level (e.g. 99.5)] 618 ["smart", 99.5] seems to provide good scaling over a range of different images. 619 @type size: int 620 @param size: size of output image in pixels 621 @type colorMapName: string 622 @param colorMapName: name of a standard matplotlib colormap, e.g. "hot", "cool", "gray" 623 etc. (do "help(pylab.colormaps)" in the Python interpreter to see available options) 624 625 """ 626 627 cut=intensityCutImage(imageData, cutLevels) 628 629 # Make plot 630 aspectR=float(cut['image'].shape[0])/float(cut['image'].shape[1]) 631 pylab.figure(figsize=(10,10*aspectR)) 632 pylab.axes([0,0,1,1]) 633 634 try: 635 colorMap=pylab.cm.get_cmap(colorMapName) 636 except AssertionError: 637 raise Exception, colorMapName+" is not a defined matplotlib colormap." 638 639 if cutLevels[0]=="histEq": 640 pylab.imshow(cut['image'], interpolation="bilinear", origin='lower', cmap=colorMap) 641 642 else: 643 pylab.imshow(cut['image'], interpolation="bilinear", norm=cut['norm'], origin='lower', 644 cmap=colorMap) 645 646 pylab.axis("off") 647 648 pylab.savefig("out_astImages.png") 649 pylab.close("all") 650 651 im=Image.open("out_astImages.png") 652 im.thumbnail((int(size),int(size))) 653 im.save(outputFileName) 654 655 os.remove("out_astImages.png")
656 657 #---------------------------------------------------------------------------------------------------
658 -def saveContourOverlayBitmap(outputFileName, backgroundImageData, backgroundImageWCS, cutLevels, \ 659 size, colorMapName, contourImageData, contourImageWCS, \ 660 contourSmoothFactor, contourLevels, contourColor, contourWidth):
661 """Makes a bitmap image from an image array, with a set of contours generated from a 662 second image array overlaid. The image format is specified by the file extension 663 (e.g. ".jpg"=JPEG, ".png"=PNG). The image array from which the contours are to be generated 664 can optionally be pre-smoothed using a Gaussian filter. 665 666 @type outputFileName: string 667 @param outputFileName: filename of output bitmap image 668 @type backgroundImageData: numpy array 669 @param backgroundImageData: background image data array 670 @type backgroundImageWCS: astWCS.WCS 671 @param backgroundImageWCS: astWCS.WCS object of the background image data array 672 @type cutLevels: list 673 @param cutLevels: sets the image scaling - available options: 674 - pixel values: cutLevels=[low value, high value]. 675 - histogram equalisation: cutLevels=["histEq", number of bins ( e.g. 1024)] 676 - relative: cutLevels=["relative", cut per cent level (e.g. 99.5)] 677 - smart: cutLevels=["smart", cut per cent level (e.g. 99.5)] 678 ["smart", 99.5] seems to provide good scaling over a range of different images. 679 @type size: int 680 @param size: size of output image in pixels 681 @type colorMapName: string 682 @param colorMapName: name of a standard matplotlib colormap, e.g. "hot", "cool", "gray" 683 etc. (do "help(pylab.colormaps)" in the Python interpreter to see available options) 684 @type contourImageData: numpy array 685 @param contourImageData: image data array from which contours are to be generated 686 @type contourImageWCS: astWCS.WCS 687 @param contourImageWCS: astWCS.WCS object corresponding to contourImageData 688 @type contourSmoothFactor: float 689 @param contourSmoothFactor: standard deviation (in pixels) of Gaussian filter for 690 pre-smoothing of contour image data (set to 0 for no smoothing) 691 @type contourLevels: list 692 @param contourLevels: sets the contour levels - available options: 693 - values: contourLevels=[list of values specifying each level] 694 - linear spacing: contourLevels=['linear', min level value, max level value, number 695 of levels] 696 - log spacing: contourLevels=['log', min level value, max level value, number of 697 levels] 698 @type contourColor: string 699 @param contourColor: color of the overlaid contours, specified by the name of a standard 700 matplotlib color, e.g., "black", "white", "cyan" 701 etc. (do "help(pylab.colors)" in the Python interpreter to see available options) 702 @type contourWidth: int 703 @param contourWidth: width of the overlaid contours 704 705 """ 706 707 cut=intensityCutImage(backgroundImageData, cutLevels) 708 709 # Make plot of just the background image 710 aspectR=float(cut['image'].shape[0])/float(cut['image'].shape[1]) 711 pylab.figure(figsize=(10,10*aspectR)) 712 pylab.axes([0,0,1,1]) 713 714 try: 715 colorMap=pylab.cm.get_cmap(colorMapName) 716 except AssertionError: 717 raise Exception, colorMapName+" is not a defined matplotlib colormap." 718 719 if cutLevels[0]=="histEq": 720 pylab.imshow(cut['image'], interpolation="bilinear", origin='lower', cmap=colorMap) 721 722 else: 723 pylab.imshow(cut['image'], interpolation="bilinear", norm=cut['norm'], origin='lower', 724 cmap=colorMap) 725 726 pylab.axis("off") 727 728 # Add the contours 729 contourData=generateContourOverlay(backgroundImageData, backgroundImageWCS, contourImageData, \ 730 contourImageWCS, contourLevels, contourSmoothFactor) 731 732 pylab.contour(contourData['scaledImage'], contourData['contourLevels'], colors=contourColor, 733 linewidths=contourWidth) 734 735 pylab.savefig("out_astImages.png") 736 pylab.close("all") 737 738 im=Image.open("out_astImages.png") 739 im.thumbnail((int(size),int(size))) 740 im.save(outputFileName) 741 742 os.remove("out_astImages.png")
743 744 #---------------------------------------------------------------------------------------------------
745 -def saveFITS(outputFileName, imageData, imageWCS = None):
746 """Writes an image array to a new .fits file. 747 748 @type outputFileName: string 749 @param outputFileName: filename of output FITS image 750 @type imageData: numpy array 751 @param imageData: image data array 752 @type imageWCS: astWCS.WCS object 753 @param imageWCS: image WCS object 754 755 @note: If imageWCS=None, the FITS image will be written with a rudimentary header containing 756 no meta data. 757 758 """ 759 760 if os.path.exists(outputFileName): 761 os.remove(outputFileName) 762 763 newImg=pyfits.HDUList() 764 765 if imageWCS!=None: 766 hdu=pyfits.PrimaryHDU(None, imageWCS.header) 767 else: 768 hdu=pyfits.PrimaryHDU(None, None) 769 770 hdu.data=imageData 771 newImg.append(hdu) 772 newImg.writeto(outputFileName) 773 newImg.close()
774 775 #---------------------------------------------------------------------------------------------------
776 -def histEq(inputArray, numBins):
777 """Performs histogram equalisation of the input numpy array. 778 779 @type inputArray: numpy array 780 @param inputArray: image data array 781 @type numBins: int 782 @param numBins: number of bins in which to perform the operation (e.g. 1024) 783 @rtype: numpy array 784 @return: image data array 785 786 """ 787 788 imageData=inputArray 789 790 # histogram equalisation: we want an equal number of pixels in each intensity range 791 sortedDataIntensities=numpy.sort(numpy.ravel(imageData)) 792 median=numpy.median(sortedDataIntensities) 793 794 # Make cumulative histogram of data values, simple min-max used to set bin sizes and range 795 dataCumHist=numpy.zeros(numBins) 796 minIntensity=sortedDataIntensities.min() 797 maxIntensity=sortedDataIntensities.max() 798 histRange=maxIntensity-minIntensity 799 binWidth=histRange/float(numBins-1) 800 for i in range(len(sortedDataIntensities)): 801 binNumber=int(math.ceil((sortedDataIntensities[i]-minIntensity)/binWidth)) 802 addArray=numpy.zeros(numBins) 803 onesArray=numpy.ones(numBins-binNumber) 804 onesRange=range(binNumber, numBins) 805 numpy.put(addArray, onesRange, onesArray) 806 dataCumHist=dataCumHist+addArray 807 808 # Make ideal cumulative histogram 809 idealValue=dataCumHist.max()/float(numBins) 810 idealCumHist=numpy.arange(idealValue, dataCumHist.max()+idealValue, idealValue) 811 812 # Map the data to the ideal 813 for y in range(imageData.shape[0]): 814 for x in range(imageData.shape[1]): 815 # Get index corresponding to dataIntensity 816 intensityBin=int(math.ceil((imageData[y][x]-minIntensity)/binWidth)) 817 818 # Guard against rounding errors (happens rarely I think) 819 if intensityBin<0: 820 intensityBin=0 821 if intensityBin>len(dataCumHist)-1: 822 intensityBin=len(dataCumHist)-1 823 824 # Get the cumulative frequency corresponding intensity level in the data 825 dataCumFreq=dataCumHist[intensityBin] 826 827 # Get the index of the corresponding ideal cumulative frequency 828 idealBin=numpy.searchsorted(idealCumHist, dataCumFreq) 829 idealIntensity=(idealBin*binWidth)+minIntensity 830 imageData[y][x]=idealIntensity 831 832 return imageData
833 834 #---------------------------------------------------------------------------------------------------
835 -def normalise(inputArray, clipMinMax):
836 """Clips the inputArray in intensity and normalises the array such that minimum and maximum 837 values are 0, 1. Clip in intensity is specified by clipMinMax, a list in the format 838 [clipMin, clipMax] 839 840 Used for normalising image arrays so that they can be turned into RGB arrays that matplotlib 841 can plot (see L{astPlots.ImagePlot}). 842 843 @type inputArray: numpy array 844 @param inputArray: image data array 845 @type clipMinMax: list 846 @param clipMinMax: [minimum value of clipped array, maximum value of clipped array] 847 @rtype: numpy array 848 @return: normalised array with minimum value 0, maximum value 1 849 850 """ 851 clipped=inputArray.clip(clipMinMax[0], clipMinMax[1]) 852 slope=1.0/(clipMinMax[1]-clipMinMax[0]) 853 intercept=-clipMinMax[0]*slope 854 clipped=clipped*slope+intercept 855 856 return clipped
857 858 #--------------------------------------------------------------------------------------------------- 859