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