astLib.astImages

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