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