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

Source Code for Module astLib.astImages

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