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