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