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-2011 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   
  36  #--------------------------------------------------------------------------------------------------- 
37 -def clipImageSectionWCS(imageData, imageWCS, RADeg, decDeg, clipSizeDeg, returnWCS = True):
38 """Clips a square or rectangular section from an image array at the given celestial coordinates. 39 An updated WCS for the clipped section is optionally returned, as well as the x, y pixel 40 coordinates in the original image corresponding to the clipped section. 41 42 Note that the clip size is specified in degrees on the sky. For projections that have varying 43 real pixel scale across the map (e.g. CEA), use L{clipUsingRADecCoords} instead. 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, and coordinates of clipped section in imageData in format 61 {'data', 'wcs', 'clippedSection'}. 62 63 """ 64 65 imHeight=imageData.shape[0] 66 imWidth=imageData.shape[1] 67 xImScale=imageWCS.getXPixelSizeDeg() 68 yImScale=imageWCS.getYPixelSizeDeg() 69 70 if type(clipSizeDeg) == float: 71 xHalfClipSizeDeg=clipSizeDeg/2.0 72 yHalfClipSizeDeg=xHalfClipSizeDeg 73 elif type(clipSizeDeg) == list or type(clipSizeDeg) == tuple: 74 xHalfClipSizeDeg=clipSizeDeg[0]/2.0 75 yHalfClipSizeDeg=clipSizeDeg[1]/2.0 76 else: 77 raise Exception, "did not understand clipSizeDeg: should be float, or [widthDeg, heightDeg]" 78 79 xHalfSizePix=xHalfClipSizeDeg/xImScale 80 yHalfSizePix=yHalfClipSizeDeg/yImScale 81 82 cPixCoords=imageWCS.wcs2pix(RADeg, decDeg) 83 84 cTopLeft=[cPixCoords[0]+xHalfSizePix, cPixCoords[1]+yHalfSizePix] 85 cBottomRight=[cPixCoords[0]-xHalfSizePix, cPixCoords[1]-yHalfSizePix] 86 87 X=[int(round(cTopLeft[0])),int(round(cBottomRight[0]))] 88 Y=[int(round(cTopLeft[1])),int(round(cBottomRight[1]))] 89 90 X.sort() 91 Y.sort() 92 93 if X[0] < 0: 94 X[0]=0 95 if X[1] > imWidth: 96 X[1]=imWidth 97 if Y[0] < 0: 98 Y[0]=0 99 if Y[1] > imHeight: 100 Y[1]=imHeight 101 102 clippedData=imageData[Y[0]:Y[1],X[0]:X[1]] 103 104 # Update WCS 105 if returnWCS == True: 106 try: 107 oldCRPIX1=imageWCS.header['CRPIX1'] 108 oldCRPIX2=imageWCS.header['CRPIX2'] 109 clippedWCS=imageWCS.copy() 110 clippedWCS.header.update('NAXIS1', clippedData.shape[1]) 111 clippedWCS.header.update('NAXIS2', clippedData.shape[0]) 112 clippedWCS.header.update('CRPIX1', oldCRPIX1-X[0]) 113 clippedWCS.header.update('CRPIX2', oldCRPIX2-Y[0]) 114 clippedWCS.updateFromHeader() 115 116 except KeyError: 117 118 if REPORT_ERRORS == True: 119 120 print "WARNING: astImages.clipImageSectionWCS() : no CRPIX1, CRPIX2 keywords found - not updating clipped image WCS." 121 122 clippedData=imageData[Y[0]:Y[1],X[0]:X[1]] 123 clippedWCS=imageWCS.copy() 124 else: 125 clippedWCS=None 126 127 return {'data': clippedData, 'wcs': clippedWCS, 'clippedSection': [X[0], X[1], Y[0], Y[1]]}
128 129 #---------------------------------------------------------------------------------------------------
130 -def clipImageSectionPix(imageData, XCoord, YCoord, clipSizePix):
131 """Clips a square or rectangular section from an image array at the given pixel coordinates. 132 133 @type imageData: numpy array 134 @param imageData: image data array 135 @type XCoord: float 136 @param XCoord: coordinate in pixels 137 @type YCoord: float 138 @param YCoord: coordinate in pixels 139 @type clipSizePix: float or list in format [widthPix, heightPix] 140 @param clipSizePix: if float, size of square clipped section in pixels; if list, 141 size of clipped section in pixels in x, y axes of output image respectively 142 @rtype: numpy array 143 @return: clipped image section 144 145 """ 146 147 imHeight=imageData.shape[0] 148 imWidth=imageData.shape[1] 149 150 if type(clipSizePix) == float or type(clipSizePix) == int: 151 xHalfClipSizePix=int(round(clipSizePix/2.0)) 152 yHalfClipSizePix=xHalfClipSizePix 153 elif type(clipSizePix) == list or type(clipSizePix) == tuple: 154 xHalfClipSizePix=int(round(clipSizePix[0]/2.0)) 155 yHalfClipSizePix=int(round(clipSizePix[1]/2.0)) 156 else: 157 raise Exception, "did not understand clipSizePix: should be float, or [widthPix, heightPix]" 158 159 cTopLeft=[XCoord+xHalfClipSizePix, YCoord+yHalfClipSizePix] 160 cBottomRight=[XCoord-xHalfClipSizePix, YCoord-yHalfClipSizePix] 161 162 X=[int(round(cTopLeft[0])),int(round(cBottomRight[0]))] 163 Y=[int(round(cTopLeft[1])),int(round(cBottomRight[1]))] 164 165 X.sort() 166 Y.sort() 167 168 if X[0] < 0: 169 X[0]=0 170 if X[1] > imWidth: 171 X[1]=imWidth 172 if Y[0] < 0: 173 Y[0]=0 174 if Y[1] > imHeight: 175 Y[1]=imHeight 176 177 return imageData[Y[0]:Y[1],X[0]:X[1]]
178 179 #---------------------------------------------------------------------------------------------------
180 -def clipRotatedImageSectionWCS(imageData, imageWCS, RADeg, decDeg, clipSizeDeg, returnWCS = True):
181 """Clips a square or rectangular section from an image array at the given celestial coordinates. 182 The resulting clip is rotated and/or flipped such that North is at the top, and East appears at 183 the left. An updated WCS for the clipped section is also returned. Note that the alignment 184 of the rotated WCS is currently not perfect - however, it is probably good enough in most 185 cases for use with L{ImagePlot} for plotting purposes. 186 187 Note that the clip size is specified in degrees on the sky. For projections that have varying 188 real pixel scale across the map (e.g. CEA), use L{clipUsingRADecCoords} instead. 189 190 @type imageData: numpy array 191 @param imageData: image data array 192 @type imageWCS: astWCS.WCS 193 @param imageWCS: astWCS.WCS object 194 @type RADeg: float 195 @param RADeg: coordinate in decimal degrees 196 @type decDeg: float 197 @param decDeg: coordinate in decimal degrees 198 @type clipSizeDeg: float 199 @param clipSizeDeg: if float, size of square clipped section in decimal degrees; if list, 200 size of clipped section in degrees in RA, dec. axes of output rotated image respectively 201 @type returnWCS: bool 202 @param returnWCS: if True, return an updated WCS for the clipped section 203 @rtype: dictionary 204 @return: clipped image section (numpy array), updated astWCS WCS object for 205 clipped image section, in format {'data', 'wcs'}. 206 207 @note: Returns 'None' if the requested position is not found within the image. If the image 208 WCS does not have keywords of the form CD1_1 etc., the output WCS will not be rotated. 209 210 """ 211 212 halfImageSize=imageWCS.getHalfSizeDeg() 213 imageCentre=imageWCS.getCentreWCSCoords() 214 imScale=imageWCS.getPixelSizeDeg() 215 216 if type(clipSizeDeg) == float: 217 xHalfClipSizeDeg=clipSizeDeg/2.0 218 yHalfClipSizeDeg=xHalfClipSizeDeg 219 elif type(clipSizeDeg) == list or type(clipSizeDeg) == tuple: 220 xHalfClipSizeDeg=clipSizeDeg[0]/2.0 221 yHalfClipSizeDeg=clipSizeDeg[1]/2.0 222 else: 223 raise Exception, "did not understand clipSizeDeg: should be float, or [widthDeg, heightDeg]" 224 225 diagonalHalfSizeDeg=math.sqrt((xHalfClipSizeDeg*xHalfClipSizeDeg) \ 226 +(yHalfClipSizeDeg*yHalfClipSizeDeg)) 227 228 diagonalHalfSizePix=diagonalHalfSizeDeg/imScale 229 230 if RADeg>imageCentre[0]-halfImageSize[0] and RADeg<imageCentre[0]+halfImageSize[0] \ 231 and decDeg>imageCentre[1]-halfImageSize[1] and decDeg<imageCentre[1]+halfImageSize[1]: 232 233 imageDiagonalClip=clipImageSectionWCS(imageData, imageWCS, RADeg, 234 decDeg, diagonalHalfSizeDeg*2.0) 235 diagonalClip=imageDiagonalClip['data'] 236 diagonalWCS=imageDiagonalClip['wcs'] 237 238 rotDeg=diagonalWCS.getRotationDeg() 239 imageRotated=ndimage.rotate(diagonalClip, rotDeg) 240 if diagonalWCS.isFlipped() == 1: 241 imageRotated=pylab.fliplr(imageRotated) 242 243 # Handle WCS rotation 244 rotatedWCS=diagonalWCS.copy() 245 rotRadians=math.radians(rotDeg) 246 247 if returnWCS == True: 248 try: 249 250 CD11=rotatedWCS.header['CD1_1'] 251 CD21=rotatedWCS.header['CD2_1'] 252 CD12=rotatedWCS.header['CD1_2'] 253 CD22=rotatedWCS.header['CD2_2'] 254 if rotatedWCS.isFlipped() == 1: 255 CD11=CD11*-1 256 CD12=CD12*-1 257 CDMatrix=numpy.array([[CD11, CD12], [CD21, CD22]], dtype=numpy.float64) 258 259 rotRadians=rotRadians 260 rot11=math.cos(rotRadians) 261 rot12=math.sin(rotRadians) 262 rot21=-math.sin(rotRadians) 263 rot22=math.cos(rotRadians) 264 rotMatrix=numpy.array([[rot11, rot12], [rot21, rot22]], dtype=numpy.float64) 265 newCDMatrix=numpy.dot(rotMatrix, CDMatrix) 266 267 P1=diagonalWCS.header['CRPIX1'] 268 P2=diagonalWCS.header['CRPIX2'] 269 V1=diagonalWCS.header['CRVAL1'] 270 V2=diagonalWCS.header['CRVAL2'] 271 272 PMatrix=numpy.zeros((2,), dtype = numpy.float64) 273 PMatrix[0]=P1 274 PMatrix[1]=P2 275 276 # BELOW IS HOW TO WORK OUT THE NEW REF PIXEL 277 CMatrix=numpy.array([imageRotated.shape[1]/2.0, imageRotated.shape[0]/2.0]) 278 centreCoords=diagonalWCS.getCentreWCSCoords() 279 alphaRad=math.radians(centreCoords[0]) 280 deltaRad=math.radians(centreCoords[1]) 281 thetaRad=math.asin(math.sin(deltaRad)*math.sin(math.radians(V2)) + \ 282 math.cos(deltaRad)*math.cos(math.radians(V2))*math.cos(alphaRad-math.radians(V1))) 283 phiRad=math.atan2(-math.cos(deltaRad)*math.sin(alphaRad-math.radians(V1)), \ 284 math.sin(deltaRad)*math.cos(math.radians(V2)) - \ 285 math.cos(deltaRad)*math.sin(math.radians(V2))*math.cos(alphaRad-math.radians(V1))) + \ 286 math.pi 287 RTheta=(180.0/math.pi)*(1.0/math.tan(thetaRad)) 288 289 xy=numpy.zeros((2,), dtype=numpy.float64) 290 xy[0]=RTheta*math.sin(phiRad) 291 xy[1]=-RTheta*math.cos(phiRad) 292 newPMatrix=CMatrix - numpy.dot(numpy.linalg.inv(newCDMatrix), xy) 293 294 # But there's a small offset to CRPIX due to the rotatedImage being rounded to an integer 295 # number of pixels (not sure this helps much) 296 #d=numpy.dot(rotMatrix, [diagonalClip.shape[1], diagonalClip.shape[0]]) 297 #offset=abs(d)-numpy.array(imageRotated.shape) 298 299 rotatedWCS.header.update('NAXIS1', imageRotated.shape[1]) 300 rotatedWCS.header.update('NAXIS2', imageRotated.shape[0]) 301 rotatedWCS.header.update('CRPIX1', newPMatrix[0]) 302 rotatedWCS.header.update('CRPIX2', newPMatrix[1]) 303 rotatedWCS.header.update('CRVAL1', V1) 304 rotatedWCS.header.update('CRVAL2', V2) 305 rotatedWCS.header.update('CD1_1', newCDMatrix[0][0]) 306 rotatedWCS.header.update('CD2_1', newCDMatrix[1][0]) 307 rotatedWCS.header.update('CD1_2', newCDMatrix[0][1]) 308 rotatedWCS.header.update('CD2_2', newCDMatrix[1][1]) 309 rotatedWCS.updateFromHeader() 310 311 except KeyError: 312 313 if REPORT_ERRORS == True: 314 print "WARNING: astImages.clipRotatedImageSectionWCS() : no CDi_j keywords found - not rotating WCS." 315 316 imageRotated=diagonalClip 317 rotatedWCS=diagonalWCS 318 319 imageRotatedClip=clipImageSectionWCS(imageRotated, rotatedWCS, RADeg, decDeg, clipSizeDeg) 320 321 if returnWCS == True: 322 return {'data': imageRotatedClip['data'], 'wcs': imageRotatedClip['wcs']} 323 else: 324 return {'data': imageRotatedClip['data'], 'wcs': None} 325 326 else: 327 328 if REPORT_ERRORS==True: 329 print """ERROR: astImages.clipRotatedImageSectionWCS() : 330 RADeg, decDeg are not within imageData.""" 331 332 return None
333 334 #---------------------------------------------------------------------------------------------------
335 -def clipUsingRADecCoords(imageData, imageWCS, RAMin, RAMax, decMin, decMax, returnWCS = True):
336 """Clips a section from an image array at the pixel coordinates corresponding to the given 337 celestial coordinates. 338 339 @type imageData: numpy array 340 @param imageData: image data array 341 @type imageWCS: astWCS.WCS 342 @param imageWCS: astWCS.WCS object 343 @type RAMin: float 344 @param RAMin: minimum RA coordinate in decimal degrees 345 @type RAMax: float 346 @param RAMax: maximum RA coordinate in decimal degrees 347 @type decMin: float 348 @param decMin: minimum dec coordinate in decimal degrees 349 @type decMax: float 350 @param decMax: maximum dec coordinate in decimal degrees 351 @type returnWCS: bool 352 @param returnWCS: if True, return an updated WCS for the clipped section 353 @rtype: dictionary 354 @return: clipped image section (numpy array), updated astWCS WCS object for 355 clipped image section, and corresponding pixel coordinates in imageData in format 356 {'data', 'wcs', 'clippedSection'}. 357 358 @note: Returns 'None' if the requested position is not found within the image. 359 360 """ 361 362 imHeight=imageData.shape[0] 363 imWidth=imageData.shape[1] 364 365 xMin, yMin=imageWCS.wcs2pix(RAMin, decMin) 366 xMax, yMax=imageWCS.wcs2pix(RAMax, decMax) 367 xMin=int(round(xMin)) 368 xMax=int(round(xMax)) 369 yMin=int(round(yMin)) 370 yMax=int(round(yMax)) 371 X=[xMin, xMax] 372 X.sort() 373 Y=[yMin, yMax] 374 Y.sort() 375 376 if X[0] < 0: 377 X[0]=0 378 if X[1] > imWidth: 379 X[1]=imWidth 380 if Y[0] < 0: 381 Y[0]=0 382 if Y[1] > imHeight: 383 Y[1]=imHeight 384 385 clippedData=imageData[Y[0]:Y[1],X[0]:X[1]] 386 387 # Update WCS 388 if returnWCS == True: 389 try: 390 oldCRPIX1=imageWCS.header['CRPIX1'] 391 oldCRPIX2=imageWCS.header['CRPIX2'] 392 clippedWCS=imageWCS.copy() 393 clippedWCS.header.update('NAXIS1', clippedData.shape[1]) 394 clippedWCS.header.update('NAXIS2', clippedData.shape[0]) 395 clippedWCS.header.update('CRPIX1', oldCRPIX1-X[0]) 396 clippedWCS.header.update('CRPIX2', oldCRPIX2-Y[0]) 397 clippedWCS.updateFromHeader() 398 399 except KeyError: 400 401 if REPORT_ERRORS == True: 402 403 print "WARNING: astImages.clipUsingRADecCoords() : no CRPIX1, CRPIX2 keywords found - not updating clipped image WCS." 404 405 clippedData=imageData[Y[0]:Y[1],X[0]:X[1]] 406 clippedWCS=imageWCS.copy() 407 else: 408 clippedWCS=None 409 410 return {'data': clippedData, 'wcs': clippedWCS, 'clippedSection': [X[0], X[1], Y[0], Y[1]]}
411 412 #---------------------------------------------------------------------------------------------------
413 -def scaleImage(imageData, imageWCS, scaleFactor):
414 """Scales image array and WCS by the given scale factor. 415 416 @type imageData: numpy array 417 @param imageData: image data array 418 @type imageWCS: astWCS.WCS 419 @param imageWCS: astWCS.WCS object 420 @type scaleFactor: float or list or tuple 421 @param scaleFactor: factor to resize image by - if tuple or list, in format 422 [x scale factor, y scale factor] 423 @rtype: dictionary 424 @return: image data (numpy array), updated astWCS WCS object for image, in format {'data', 'wcs'}. 425 426 """ 427 428 if type(scaleFactor) == int or type(scaleFactor) == float: 429 scaleFactor=[float(scaleFactor), float(scaleFactor)] 430 scaledData=ndimage.zoom(imageData, scaleFactor) 431 432 # Take care of offset due to rounding in scaling image to integer pixel dimensions 433 properDimensions=numpy.array(imageData.shape)*scaleFactor 434 offset=properDimensions-numpy.array(scaledData.shape) 435 436 # Rescale WCS 437 try: 438 oldCRPIX1=imageWCS.header['CRPIX1'] 439 oldCRPIX2=imageWCS.header['CRPIX2'] 440 CD11=imageWCS.header['CD1_1'] 441 CD21=imageWCS.header['CD2_1'] 442 CD12=imageWCS.header['CD1_2'] 443 CD22=imageWCS.header['CD2_2'] 444 except KeyError: 445 # Try the older FITS header format 446 try: 447 oldCRPIX1=imageWCS.header['CRPIX1'] 448 oldCRPIX2=imageWCS.header['CRPIX2'] 449 CD11=imageWCS.header['CDELT1'] 450 CD21=0 451 CD12=0 452 CD22=imageWCS.header['CDELT2'] 453 except KeyError: 454 if REPORT_ERRORS == True: 455 print "WARNING: astImages.rescaleImage() : no CDij or CDELT keywords found - not updating WCS." 456 scaledWCS=imageWCS.copy() 457 return {'data': scaledData, 'wcs': scaledWCS} 458 459 CDMatrix=numpy.array([[CD11, CD12], [CD21, CD22]], dtype=numpy.float64) 460 scaleFactorMatrix=numpy.array([[1.0/scaleFactor[0], 0], [0, 1.0/scaleFactor[1]]]) 461 scaledCDMatrix=numpy.dot(scaleFactorMatrix, CDMatrix) 462 463 scaledWCS=imageWCS.copy() 464 scaledWCS.header.update('NAXIS1', scaledData.shape[1]) 465 scaledWCS.header.update('NAXIS2', scaledData.shape[0]) 466 scaledWCS.header.update('CRPIX1', oldCRPIX1*scaleFactor[0]+offset[1]) 467 scaledWCS.header.update('CRPIX2', oldCRPIX2*scaleFactor[1]+offset[0]) 468 scaledWCS.header.update('CD1_1', scaledCDMatrix[0][0]) 469 scaledWCS.header.update('CD2_1', scaledCDMatrix[1][0]) 470 scaledWCS.header.update('CD1_2', scaledCDMatrix[0][1]) 471 scaledWCS.header.update('CD2_2', scaledCDMatrix[1][1]) 472 scaledWCS.updateFromHeader() 473 474 return {'data': scaledData, 'wcs': scaledWCS}
475 476 #---------------------------------------------------------------------------------------------------
477 -def intensityCutImage(imageData, cutLevels):
478 """Creates a matplotlib.pylab plot of an image array with the specified cuts in intensity 479 applied. This routine is used by L{saveBitmap} and L{saveContourOverlayBitmap}, which both 480 produce output as .png, .jpg, etc. images. 481 482 @type imageData: numpy array 483 @param imageData: image data array 484 @type cutLevels: list 485 @param cutLevels: sets the image scaling - available options: 486 - pixel values: cutLevels=[low value, high value]. 487 - histogram equalisation: cutLevels=["histEq", number of bins ( e.g. 1024)] 488 - relative: cutLevels=["relative", cut per cent level (e.g. 99.5)] 489 - smart: cutLevels=["smart", cut per cent level (e.g. 99.5)] 490 ["smart", 99.5] seems to provide good scaling over a range of different images. 491 @rtype: dictionary 492 @return: image section (numpy.array), matplotlib image normalisation (matplotlib.colors.Normalize), in the format {'image', 'norm'}. 493 494 @note: If cutLevels[0] == "histEq", then only {'image'} is returned. 495 496 """ 497 498 oImWidth=imageData.shape[1] 499 oImHeight=imageData.shape[0] 500 501 # Optional histogram equalisation 502 if cutLevels[0]=="histEq": 503 504 imageData=histEq(imageData, cutLevels[1]) 505 anorm=pylab.normalize(imageData.min(), imageData.max()) 506 507 elif cutLevels[0]=="relative": 508 509 # this turns image data into 1D array then sorts 510 sorted=numpy.sort(numpy.ravel(imageData)) 511 maxValue=sorted.max() 512 minValue=sorted.min() 513 514 # want to discard the top and bottom specified 515 topCutIndex=len(sorted-1) \ 516 -int(math.floor(float((100.0-cutLevels[1])/100.0)*len(sorted-1))) 517 bottomCutIndex=int(math.ceil(float((100.0-cutLevels[1])/100.0)*len(sorted-1))) 518 topCut=sorted[topCutIndex] 519 bottomCut=sorted[bottomCutIndex] 520 anorm=pylab.normalize(bottomCut, topCut) 521 522 elif cutLevels[0]=="smart": 523 524 # this turns image data into 1Darray then sorts 525 sorted=numpy.sort(numpy.ravel(imageData)) 526 maxValue=sorted.max() 527 minValue=sorted.min() 528 numBins=10000 # 0.01 per cent accuracy 529 binWidth=(maxValue-minValue)/float(numBins) 530 histogram=ndimage.histogram(sorted, minValue, maxValue, numBins) 531 532 # Find the bin with the most pixels in it, set that as our minimum 533 # Then search through the bins until we get to a bin with more/or the same number of 534 # pixels in it than the previous one. 535 # We take that to be the maximum. 536 # This means that we avoid the traps of big, bright, saturated stars that cause 537 # problems for relative scaling 538 backgroundValue=histogram.max() 539 foundBackgroundBin=False 540 foundTopBin=False 541 lastBin=-10000 542 for i in range(len(histogram)): 543 544 if histogram[i]>=lastBin and foundBackgroundBin==True: 545 546 # Added a fudge here to stop us picking for top bin a bin within 547 # 10 percent of the background pixel value 548 if (minValue+(binWidth*i))>bottomBinValue*1.1: 549 topBinValue=minValue+(binWidth*i) 550 foundTopBin=True 551 break 552 553 if histogram[i]==backgroundValue and foundBackgroundBin==False: 554 bottomBinValue=minValue+(binWidth*i) 555 foundBackgroundBin=True 556 557 lastBin=histogram[i] 558 559 if foundTopBin==False: 560 topBinValue=maxValue 561 562 #Now we apply relative scaling to this 563 smartClipped=numpy.clip(sorted, bottomBinValue, topBinValue) 564 topCutIndex=len(smartClipped-1) \ 565 -int(math.floor(float((100.0-cutLevels[1])/100.0)*len(smartClipped-1))) 566 bottomCutIndex=int(math.ceil(float((100.0-cutLevels[1])/100.0)*len(smartClipped-1))) 567 topCut=smartClipped[topCutIndex] 568 bottomCut=smartClipped[bottomCutIndex] 569 anorm=pylab.normalize(bottomCut, topCut) 570 else: 571 572 # Normalise using given cut levels 573 anorm=pylab.normalize(cutLevels[0], cutLevels[1]) 574 575 if cutLevels[0]=="histEq": 576 return {'image': imageData.copy()} 577 else: 578 return {'image': imageData.copy(), 'norm': anorm}
579 580 #---------------------------------------------------------------------------------------------------
581 -def resampleToTanProjection(imageData, imageWCS, outputPixDimensions=[600, 600]):
582 """Resamples an image and WCS to a tangent plane projection. Purely for plotting purposes 583 (e.g., ensuring RA, dec. coordinate axes perpendicular). 584 585 @type imageData: numpy array 586 @param imageData: image data array 587 @type imageWCS: astWCS.WCS 588 @param imageWCS: astWCS.WCS object 589 @type outputPixDimensions: list 590 @param outputPixDimensions: [width, height] of output image in pixels 591 @rtype: dictionary 592 @return: image data (numpy array), updated astWCS WCS object for image, in format {'data', 'wcs'}. 593 594 """ 595 596 RADeg, decDeg=imageWCS.getCentreWCSCoords() 597 xPixelScale=imageWCS.getXPixelSizeDeg() 598 yPixelScale=imageWCS.getYPixelSizeDeg() 599 xSizeDeg, ySizeDeg=imageWCS.getFullSizeSkyDeg() 600 xSizePix=int(round(outputPixDimensions[0])) 601 ySizePix=int(round(outputPixDimensions[1])) 602 xRefPix=xSizePix/2.0 603 yRefPix=ySizePix/2.0 604 xOutPixScale=xSizeDeg/xSizePix 605 yOutPixScale=ySizeDeg/ySizePix 606 cardList=pyfits.CardList() 607 cardList.append(pyfits.Card('NAXIS', 2)) 608 cardList.append(pyfits.Card('NAXIS1', xSizePix)) 609 cardList.append(pyfits.Card('NAXIS2', ySizePix)) 610 cardList.append(pyfits.Card('CTYPE1', 'RA---TAN')) 611 cardList.append(pyfits.Card('CTYPE2', 'DEC--TAN')) 612 cardList.append(pyfits.Card('CRVAL1', RADeg)) 613 cardList.append(pyfits.Card('CRVAL2', decDeg)) 614 cardList.append(pyfits.Card('CRPIX1', xRefPix+1)) 615 cardList.append(pyfits.Card('CRPIX2', yRefPix+1)) 616 cardList.append(pyfits.Card('CDELT1', -xOutPixScale)) 617 cardList.append(pyfits.Card('CDELT2', xOutPixScale)) # Makes more sense to use same pix scale 618 cardList.append(pyfits.Card('CUNIT1', 'DEG')) 619 cardList.append(pyfits.Card('CUNIT2', 'DEG')) 620 newHead=pyfits.Header(cards=cardList) 621 newWCS=astWCS.WCS(newHead, mode='pyfits') 622 newImage=numpy.zeros([ySizePix, xSizePix]) 623 624 tanImage=resampleToWCS(newImage, newWCS, imageData, imageWCS, highAccuracy=True, 625 onlyOverlapping=False) 626 627 return tanImage
628 629 #---------------------------------------------------------------------------------------------------
630 -def resampleToWCS(im1Data, im1WCS, im2Data, im2WCS, highAccuracy = False, onlyOverlapping = True):
631 """Resamples data corresponding to second image (with data im2Data, WCS im2WCS) onto the WCS 632 of the first image (im1Data, im1WCS). The output, resampled image is of the pixel same 633 dimensions of the first image. This routine is for assisting in plotting - performing 634 photometry on the output is not recommended. 635 636 Set highAccuracy == True to sample every corresponding pixel in each image; otherwise only 637 every nth pixel (where n is the ratio of the image scales) will be sampled, with values 638 in between being set using a linear interpolation (much faster). 639 640 Set onlyOverlapping == True to speed up resampling by only resampling the overlapping 641 area defined by both image WCSs. 642 643 @type im1Data: numpy array 644 @param im1Data: image data array for first image 645 @type im1WCS: astWCS.WCS 646 @param im1WCS: astWCS.WCS object corresponding to im1Data 647 @type im2Data: numpy array 648 @param im2Data: image data array for second image (to be resampled to match first image) 649 @type im2WCS: astWCS.WCS 650 @param im2WCS: astWCS.WCS object corresponding to im2Data 651 @type highAccuracy: bool 652 @param highAccuracy: if True, sample every corresponding pixel in each image; otherwise, sample 653 every nth pixel, where n = the ratio of the image scales. 654 @type onlyOverlapping: bool 655 @param onlyOverlapping: if True, only consider the overlapping area defined by both image WCSs 656 (speeds things up) 657 @rtype: dictionary 658 @return: numpy image data array and associated WCS in format {'data', 'wcs'} 659 660 """ 661 662 resampledData=numpy.zeros(im1Data.shape) 663 664 # Find overlap - speed things up 665 # But have a border so as not to require the overlap to be perfect 666 # There's also no point in oversampling image 1 if it's much higher res than image 2 667 xPixRatio=(im2WCS.getXPixelSizeDeg()/im1WCS.getXPixelSizeDeg())/2.0 668 yPixRatio=(im2WCS.getYPixelSizeDeg()/im1WCS.getYPixelSizeDeg())/2.0 669 xBorder=xPixRatio*10.0 670 yBorder=yPixRatio*10.0 671 if highAccuracy == False: 672 if xPixRatio > 1: 673 xPixStep=int(math.ceil(xPixRatio)) 674 else: 675 xPixStep=1 676 if yPixRatio > 1: 677 yPixStep=int(math.ceil(yPixRatio)) 678 else: 679 yPixStep=1 680 else: 681 xPixStep=1 682 yPixStep=1 683 684 if onlyOverlapping == True: 685 overlap=astWCS.findWCSOverlap(im1WCS, im2WCS) 686 xOverlap=[overlap['wcs1Pix'][0], overlap['wcs1Pix'][1]] 687 yOverlap=[overlap['wcs1Pix'][2], overlap['wcs1Pix'][3]] 688 xOverlap.sort() 689 yOverlap.sort() 690 xMin=int(math.floor(xOverlap[0]-xBorder)) 691 xMax=int(math.ceil(xOverlap[1]+xBorder)) 692 yMin=int(math.floor(yOverlap[0]-yBorder)) 693 yMax=int(math.ceil(yOverlap[1]+yBorder)) 694 xRemainder=(xMax-xMin) % xPixStep 695 yRemainder=(yMax-yMin) % yPixStep 696 if xRemainder != 0: 697 xMax=xMax+xRemainder 698 if yRemainder != 0: 699 yMax=yMax+yRemainder 700 # Check that we're still within the image boundaries, to be on the safe side 701 if xMin < 0: 702 xMin=0 703 if xMax > im1Data.shape[1]: 704 xMax=im1Data.shape[1] 705 if yMin < 0: 706 yMin=0 707 if yMax > im1Data.shape[0]: 708 yMax=im1Data.shape[0] 709 else: 710 xMin=0 711 xMax=im1Data.shape[1] 712 yMin=0 713 yMax=im1Data.shape[0] 714 715 for x in range(xMin, xMax, xPixStep): 716 for y in range(yMin, yMax, yPixStep): 717 RA, dec=im1WCS.pix2wcs(x, y) 718 x2, y2=im2WCS.wcs2pix(RA, dec) 719 x2=int(round(x2)) 720 y2=int(round(y2)) 721 if x2 >= 0 and x2 < im2Data.shape[1] and y2 >= 0 and y2 < im2Data.shape[0]: 722 resampledData[y][x]=im2Data[y2][x2] 723 724 # linear interpolation 725 if highAccuracy == False: 726 for row in range(resampledData.shape[0]): 727 vals=resampledData[row, numpy.arange(xMin, xMax, xPixStep)] 728 index2data=interpolate.interp1d(numpy.arange(0, vals.shape[0], 1), vals) 729 interpedVals=index2data(numpy.arange(0, vals.shape[0]-1, 1.0/xPixStep)) 730 resampledData[row, xMin:xMin+interpedVals.shape[0]]=interpedVals 731 for col in range(resampledData.shape[1]): 732 vals=resampledData[numpy.arange(yMin, yMax, yPixStep), col] 733 index2data=interpolate.interp1d(numpy.arange(0, vals.shape[0], 1), vals) 734 interpedVals=index2data(numpy.arange(0, vals.shape[0]-1, 1.0/yPixStep)) 735 resampledData[yMin:yMin+interpedVals.shape[0], col]=interpedVals 736 737 # Note: should really just copy im1WCS keywords into im2WCS and return that 738 # Only a problem if we're using this for anything other than plotting 739 return {'data': resampledData, 'wcs': im1WCS.copy()}
740 741 #---------------------------------------------------------------------------------------------------
742 -def generateContourOverlay(backgroundImageData, backgroundImageWCS, contourImageData, contourImageWCS, \ 743 contourLevels, contourSmoothFactor = 0, highAccuracy = False):
744 """Rescales an image array to be used as a contour overlay to have the same dimensions as the 745 background image, and generates a set of contour levels. The image array from which the contours 746 are to be generated will be resampled to the same dimensions as the background image data, and 747 can be optionally smoothed using a Gaussian filter. The sigma of the Gaussian filter 748 (contourSmoothFactor) is specified in arcsec. 749 750 @type backgroundImageData: numpy array 751 @param backgroundImageData: background image data array 752 @type backgroundImageWCS: astWCS.WCS 753 @param backgroundImageWCS: astWCS.WCS object of the background image data array 754 @type contourImageData: numpy array 755 @param contourImageData: image data array from which contours are to be generated 756 @type contourImageWCS: astWCS.WCS 757 @param contourImageWCS: astWCS.WCS object corresponding to contourImageData 758 @type contourLevels: list 759 @param contourLevels: sets the contour levels - available options: 760 - values: contourLevels=[list of values specifying each level] 761 - linear spacing: contourLevels=['linear', min level value, max level value, number 762 of levels] - can use "min", "max" to automatically set min, max levels from image data 763 - log spacing: contourLevels=['log', min level value, max level value, number of 764 levels] - can use "min", "max" to automatically set min, max levels from image data 765 @type contourSmoothFactor: float 766 @param contourSmoothFactor: standard deviation (in arcsec) of Gaussian filter for 767 pre-smoothing of contour image data (set to 0 for no smoothing) 768 @type highAccuracy: bool 769 @param highAccuracy: if True, sample every corresponding pixel in each image; otherwise, sample 770 every nth pixel, where n = the ratio of the image scales. 771 772 """ 773 774 # For compromise between speed and accuracy, scale a copy of the background 775 # image down to a scale that is one pixel = 1/5 of a pixel in the contour image 776 # But only do this if it has CDij keywords as we know how to scale those 777 if backgroundImageWCS.header.has_key("CD1_1") == True: 778 xScaleFactor=backgroundImageWCS.getXPixelSizeDeg()/(contourImageWCS.getXPixelSizeDeg()/5.0) 779 yScaleFactor=backgroundImageWCS.getYPixelSizeDeg()/(contourImageWCS.getYPixelSizeDeg()/5.0) 780 scaledBackground=scaleImage(backgroundImageData, backgroundImageWCS, (xScaleFactor, yScaleFactor)) 781 scaled=resampleToWCS(scaledBackground['data'], scaledBackground['wcs'], 782 contourImageData, contourImageWCS, highAccuracy = highAccuracy) 783 scaledContourData=scaled['data'] 784 scaledContourWCS=scaled['wcs'] 785 scaledBackground=True 786 else: 787 scaled=resampleToWCS(backgroundImageData, backgroundImageWCS, 788 contourImageData, contourImageWCS, highAccuracy = highAccuracy) 789 scaledContourData=scaled['data'] 790 scaledContourWCS=scaled['wcs'] 791 scaledBackground=False 792 793 if contourSmoothFactor > 0: 794 sigmaPix=(contourSmoothFactor/3600.0)/scaledContourWCS.getPixelSizeDeg() 795 scaledContourData=ndimage.gaussian_filter(scaledContourData, sigmaPix) 796 797 # Various ways of setting the contour levels 798 # If just a list is passed in, use those instead 799 if contourLevels[0] == "linear": 800 if contourLevels[1] == "min": 801 xMin=contourImageData.flatten().min() 802 else: 803 xMin=float(contourLevels[1]) 804 if contourLevels[2] == "max": 805 xMax=contourImageData.flatten().max() 806 else: 807 xMax=float(contourLevels[2]) 808 nLevels=contourLevels[3] 809 xStep=(xMax-xMin)/(nLevels-1) 810 cLevels=[] 811 for j in range(nLevels+1): 812 level=xMin+j*xStep 813 cLevels.append(level) 814 815 elif contourLevels[0] == "log": 816 if contourLevels[1] == "min": 817 xMin=contourImageData.flatten().min() 818 else: 819 xMin=float(contourLevels[1]) 820 if contourLevels[2] == "max": 821 xMax=contourImageData.flatten().max() 822 else: 823 xMax=float(contourLevels[2]) 824 if xMin <= 0.0: 825 raise Exception, "minimum contour level set to <= 0 and log scaling chosen." 826 xLogMin=math.log10(xMin) 827 xLogMax=math.log10(xMax) 828 nLevels=contourLevels[3] 829 xLogStep=(xLogMax-xLogMin)/(nLevels-1) 830 cLevels=[] 831 prevLevel=0 832 for j in range(nLevels+1): 833 level=math.pow(10, xLogMin+j*xLogStep) 834 cLevels.append(level) 835 836 else: 837 cLevels=contourLevels 838 839 # Now blow the contour image data back up to the size of the original image 840 if scaledBackground == True: 841 scaledBack=scaleImage(scaledContourData, scaledContourWCS, (1.0/xScaleFactor, 1.0/yScaleFactor))['data'] 842 else: 843 scaledBack=scaledContourData 844 845 return {'scaledImage': scaledBack, 'contourLevels': cLevels}
846 847 #---------------------------------------------------------------------------------------------------
848 -def saveBitmap(outputFileName, imageData, cutLevels, size, colorMapName):
849 """Makes a bitmap image from an image array; the image format is specified by the 850 filename extension. (e.g. ".jpg" =JPEG, ".png"=PNG). 851 852 @type outputFileName: string 853 @param outputFileName: filename of output bitmap image 854 @type imageData: numpy array 855 @param imageData: image data array 856 @type cutLevels: list 857 @param cutLevels: sets the image scaling - available options: 858 - pixel values: cutLevels=[low value, high value]. 859 - histogram equalisation: cutLevels=["histEq", number of bins ( e.g. 1024)] 860 - relative: cutLevels=["relative", cut per cent level (e.g. 99.5)] 861 - smart: cutLevels=["smart", cut per cent level (e.g. 99.5)] 862 ["smart", 99.5] seems to provide good scaling over a range of different images. 863 @type size: int 864 @param size: size of output image in pixels 865 @type colorMapName: string 866 @param colorMapName: name of a standard matplotlib colormap, e.g. "hot", "cool", "gray" 867 etc. (do "help(pylab.colormaps)" in the Python interpreter to see available options) 868 869 """ 870 871 cut=intensityCutImage(imageData, cutLevels) 872 873 # Make plot 874 aspectR=float(cut['image'].shape[0])/float(cut['image'].shape[1]) 875 pylab.figure(figsize=(10,10*aspectR)) 876 pylab.axes([0,0,1,1]) 877 878 try: 879 colorMap=pylab.cm.get_cmap(colorMapName) 880 except AssertionError: 881 raise Exception, colorMapName+" is not a defined matplotlib colormap." 882 883 if cutLevels[0]=="histEq": 884 pylab.imshow(cut['image'], interpolation="bilinear", origin='lower', cmap=colorMap) 885 886 else: 887 pylab.imshow(cut['image'], interpolation="bilinear", norm=cut['norm'], origin='lower', 888 cmap=colorMap) 889 890 pylab.axis("off") 891 892 pylab.savefig("out_astImages.png") 893 pylab.close("all") 894 895 try: 896 from PIL import Image 897 except: 898 raise Exception, "astImages.saveBitmap requires the Python Imaging Library to be installed." 899 im=Image.open("out_astImages.png") 900 im.thumbnail((int(size),int(size))) 901 im.save(outputFileName) 902 903 os.remove("out_astImages.png")
904 905 #---------------------------------------------------------------------------------------------------
906 -def saveContourOverlayBitmap(outputFileName, backgroundImageData, backgroundImageWCS, cutLevels, \ 907 size, colorMapName, contourImageData, contourImageWCS, \ 908 contourSmoothFactor, contourLevels, contourColor, contourWidth):
909 """Makes a bitmap image from an image array, with a set of contours generated from a 910 second image array overlaid. The image format is specified by the file extension 911 (e.g. ".jpg"=JPEG, ".png"=PNG). The image array from which the contours are to be generated 912 can optionally be pre-smoothed using a Gaussian filter. 913 914 @type outputFileName: string 915 @param outputFileName: filename of output bitmap image 916 @type backgroundImageData: numpy array 917 @param backgroundImageData: background image data array 918 @type backgroundImageWCS: astWCS.WCS 919 @param backgroundImageWCS: astWCS.WCS object of the background image data array 920 @type cutLevels: list 921 @param cutLevels: sets the image scaling - available options: 922 - pixel values: cutLevels=[low value, high value]. 923 - histogram equalisation: cutLevels=["histEq", number of bins ( e.g. 1024)] 924 - relative: cutLevels=["relative", cut per cent level (e.g. 99.5)] 925 - smart: cutLevels=["smart", cut per cent level (e.g. 99.5)] 926 ["smart", 99.5] seems to provide good scaling over a range of different images. 927 @type size: int 928 @param size: size of output image in pixels 929 @type colorMapName: string 930 @param colorMapName: name of a standard matplotlib colormap, e.g. "hot", "cool", "gray" 931 etc. (do "help(pylab.colormaps)" in the Python interpreter to see available options) 932 @type contourImageData: numpy array 933 @param contourImageData: image data array from which contours are to be generated 934 @type contourImageWCS: astWCS.WCS 935 @param contourImageWCS: astWCS.WCS object corresponding to contourImageData 936 @type contourSmoothFactor: float 937 @param contourSmoothFactor: standard deviation (in pixels) of Gaussian filter for 938 pre-smoothing of contour image data (set to 0 for no smoothing) 939 @type contourLevels: list 940 @param contourLevels: sets the contour levels - available options: 941 - values: contourLevels=[list of values specifying each level] 942 - linear spacing: contourLevels=['linear', min level value, max level value, number 943 of levels] - can use "min", "max" to automatically set min, max levels from image data 944 - log spacing: contourLevels=['log', min level value, max level value, number of 945 levels] - can use "min", "max" to automatically set min, max levels from image data 946 @type contourColor: string 947 @param contourColor: color of the overlaid contours, specified by the name of a standard 948 matplotlib color, e.g., "black", "white", "cyan" 949 etc. (do "help(pylab.colors)" in the Python interpreter to see available options) 950 @type contourWidth: int 951 @param contourWidth: width of the overlaid contours 952 953 """ 954 955 cut=intensityCutImage(backgroundImageData, cutLevels) 956 957 # Make plot of just the background image 958 aspectR=float(cut['image'].shape[0])/float(cut['image'].shape[1]) 959 pylab.figure(figsize=(10,10*aspectR)) 960 pylab.axes([0,0,1,1]) 961 962 try: 963 colorMap=pylab.cm.get_cmap(colorMapName) 964 except AssertionError: 965 raise Exception, colorMapName+" is not a defined matplotlib colormap." 966 967 if cutLevels[0]=="histEq": 968 pylab.imshow(cut['image'], interpolation="bilinear", origin='lower', cmap=colorMap) 969 970 else: 971 pylab.imshow(cut['image'], interpolation="bilinear", norm=cut['norm'], origin='lower', 972 cmap=colorMap) 973 974 pylab.axis("off") 975 976 # Add the contours 977 contourData=generateContourOverlay(backgroundImageData, backgroundImageWCS, contourImageData, \ 978 contourImageWCS, contourLevels, contourSmoothFactor) 979 980 pylab.contour(contourData['scaledImage'], contourData['contourLevels'], colors=contourColor, 981 linewidths=contourWidth) 982 983 pylab.savefig("out_astImages.png") 984 pylab.close("all") 985 986 try: 987 from PIL import Image 988 except: 989 raise Exception, "astImages.saveContourOverlayBitmap requires the Python Imaging Library to be installed" 990 991 im=Image.open("out_astImages.png") 992 im.thumbnail((int(size),int(size))) 993 im.save(outputFileName) 994 995 os.remove("out_astImages.png")
996 997 #---------------------------------------------------------------------------------------------------
998 -def saveFITS(outputFileName, imageData, imageWCS = None):
999 """Writes an image array to a new .fits file. 1000 1001 @type outputFileName: string 1002 @param outputFileName: filename of output FITS image 1003 @type imageData: numpy array 1004 @param imageData: image data array 1005 @type imageWCS: astWCS.WCS object 1006 @param imageWCS: image WCS object 1007 1008 @note: If imageWCS=None, the FITS image will be written with a rudimentary header containing 1009 no meta data. 1010 1011 """ 1012 1013 if os.path.exists(outputFileName): 1014 os.remove(outputFileName) 1015 1016 newImg=pyfits.HDUList() 1017 1018 if imageWCS!=None: 1019 hdu=pyfits.PrimaryHDU(None, imageWCS.header) 1020 else: 1021 hdu=pyfits.PrimaryHDU(None, None) 1022 1023 hdu.data=imageData 1024 newImg.append(hdu) 1025 newImg.writeto(outputFileName) 1026 newImg.close()
1027 1028 #---------------------------------------------------------------------------------------------------
1029 -def histEq(inputArray, numBins):
1030 """Performs histogram equalisation of the input numpy array. 1031 1032 @type inputArray: numpy array 1033 @param inputArray: image data array 1034 @type numBins: int 1035 @param numBins: number of bins in which to perform the operation (e.g. 1024) 1036 @rtype: numpy array 1037 @return: image data array 1038 1039 """ 1040 1041 imageData=inputArray 1042 1043 # histogram equalisation: we want an equal number of pixels in each intensity range 1044 sortedDataIntensities=numpy.sort(numpy.ravel(imageData)) 1045 median=numpy.median(sortedDataIntensities) 1046 1047 # Make cumulative histogram of data values, simple min-max used to set bin sizes and range 1048 dataCumHist=numpy.zeros(numBins) 1049 minIntensity=sortedDataIntensities.min() 1050 maxIntensity=sortedDataIntensities.max() 1051 histRange=maxIntensity-minIntensity 1052 binWidth=histRange/float(numBins-1) 1053 for i in range(len(sortedDataIntensities)): 1054 binNumber=int(math.ceil((sortedDataIntensities[i]-minIntensity)/binWidth)) 1055 addArray=numpy.zeros(numBins) 1056 onesArray=numpy.ones(numBins-binNumber) 1057 onesRange=range(binNumber, numBins) 1058 numpy.put(addArray, onesRange, onesArray) 1059 dataCumHist=dataCumHist+addArray 1060 1061 # Make ideal cumulative histogram 1062 idealValue=dataCumHist.max()/float(numBins) 1063 idealCumHist=numpy.arange(idealValue, dataCumHist.max()+idealValue, idealValue) 1064 1065 # Map the data to the ideal 1066 for y in range(imageData.shape[0]): 1067 for x in range(imageData.shape[1]): 1068 # Get index corresponding to dataIntensity 1069 intensityBin=int(math.ceil((imageData[y][x]-minIntensity)/binWidth)) 1070 1071 # Guard against rounding errors (happens rarely I think) 1072 if intensityBin<0: 1073 intensityBin=0 1074 if intensityBin>len(dataCumHist)-1: 1075 intensityBin=len(dataCumHist)-1 1076 1077 # Get the cumulative frequency corresponding intensity level in the data 1078 dataCumFreq=dataCumHist[intensityBin] 1079 1080 # Get the index of the corresponding ideal cumulative frequency 1081 idealBin=numpy.searchsorted(idealCumHist, dataCumFreq) 1082 idealIntensity=(idealBin*binWidth)+minIntensity 1083 imageData[y][x]=idealIntensity 1084 1085 return imageData
1086 1087 #---------------------------------------------------------------------------------------------------
1088 -def normalise(inputArray, clipMinMax):
1089 """Clips the inputArray in intensity and normalises the array such that minimum and maximum 1090 values are 0, 1. Clip in intensity is specified by clipMinMax, a list in the format 1091 [clipMin, clipMax] 1092 1093 Used for normalising image arrays so that they can be turned into RGB arrays that matplotlib 1094 can plot (see L{astPlots.ImagePlot}). 1095 1096 @type inputArray: numpy array 1097 @param inputArray: image data array 1098 @type clipMinMax: list 1099 @param clipMinMax: [minimum value of clipped array, maximum value of clipped array] 1100 @rtype: numpy array 1101 @return: normalised array with minimum value 0, maximum value 1 1102 1103 """ 1104 clipped=inputArray.clip(clipMinMax[0], clipMinMax[1]) 1105 slope=1.0/(clipMinMax[1]-clipMinMax[0]) 1106 intercept=-clipMinMax[0]*slope 1107 clipped=clipped*slope+intercept 1108 1109 return clipped
1110