1
2 """module for producing astronomical plots
3
4 (c) 2007-2012 Matt Hilton
5
6 U{http://astlib.sourceforge.net}
7
8 This module provides the matplotlib powered ImagePlot class, which is designed to be flexible.
9 ImagePlots can have RA, Dec. coordinate axes, contour overlays, and have objects marked in them,
10 using WCS coordinates. RGB plots are supported too.
11
12 @var DEC_TICK_STEPS: Defines the possible coordinate label steps on the delination axis in
13 sexagesimal mode. Dictionary format: {'deg', 'unit'}
14 @type DEC_TICK_STEPS: dictionary list
15
16 @var RA_TICK_STEPS: Defines the possible coordinate label steps on the right ascension axis in
17 sexagesimal mode. Dictionary format: {'deg', 'unit'}
18 @type RA_TICK_STEPS: dictionary list
19
20 @var DECIMAL_TICK_STEPS: Defines the possible coordinate label steps on both coordinate axes in
21 decimal degrees mode.
22 @type DECIMAL_TICK_STEPS: list
23
24 @var DEG: Variable to stand in for the degrees symbol.
25 @type DEG: string
26
27 @var PRIME: Variable to stand in for the prime symbol.
28 @type PRIME: string
29
30 @var DOUBLE_PRIME: Variable to stand in for the double prime symbol.
31 @type DOUBLE_PRIME: string
32
33 """
34
35 import math
36 import astImages
37 import astWCS
38 import astCoords
39 import numpy
40 import pyfits
41 from scipy import interpolate
42 import pylab
43 import matplotlib.patches as patches
44 import sys
45
46 DEC_TICK_STEPS=[{'deg': 1.0/60.0/60.0, 'unit': "s"},
47 {'deg': 2.0/60.0/60.0, 'unit': "s"},
48 {'deg': 5.0/60.0/60.0, 'unit': "s"},
49 {'deg': 10.0/60.0/60.0, 'unit': "s"},
50 {'deg': 30.0/60.0/60.0, 'unit': "s"},
51 {'deg': 1.0/60.0, 'unit': "m"},
52 {'deg': 2.0/60.0, 'unit': "m"},
53 {'deg': 5.0/60.0, 'unit': "m"},
54 {'deg': 15.0/60.0, 'unit': "m"},
55 {'deg': 30.0/60.0, 'unit': "m"},
56 {'deg': 1.0, 'unit': "d"},
57 {'deg': 2.0, 'unit': "d"},
58 {'deg': 4.0, 'unit': "d"},
59 {'deg': 5.0, 'unit': "d"},
60 {'deg': 10.0, 'unit': "d"},
61 {'deg': 20.0, 'unit': "d"},
62 {'deg': 30.0, 'unit': "d"}]
63
64 RA_TICK_STEPS=[ {'deg': (0.5/60.0/60.0/24.0)*360.0, 'unit': "s"},
65 {'deg': (1.0/60.0/60.0/24.0)*360.0, 'unit': "s"},
66 {'deg': (2.0/60.0/60.0/24.0)*360.0, 'unit': "s"},
67 {'deg': (4.0/60.0/60.0/24.0)*360.0, 'unit': "s"},
68 {'deg': (5.0/60.0/60.0/24.0)*360.0, 'unit': "s"},
69 {'deg': (10.0/60.0/60.0/24.0)*360.0, 'unit': "s"},
70 {'deg': (20.0/60.0/60.0/24.0)*360.0, 'unit': "s"},
71 {'deg': (30.0/60.0/60.0/24.0)*360.0, 'unit': "s"},
72 {'deg': (1.0/60.0/24.0)*360.0, 'unit': "m"},
73 {'deg': (2.0/60.0/24.0)*360.0, 'unit': "m"},
74 {'deg': (5.0/60.0/24.0)*360.0, 'unit': "m"},
75 {'deg': (10.0/60.0/24.0)*360.0, 'unit': "m"},
76 {'deg': (20.0/60.0/24.0)*360.0, 'unit': "m"},
77 {'deg': (30.0/60.0/24.0)*360.0, 'unit': "m"},
78 {'deg': (1.0/24.0)*360.0, 'unit': "h"},
79 {'deg': (3.0/24.0)*360.0, 'unit': "h"},
80 {'deg': (6.0/24.0)*360.0, 'unit': "h"},
81 {'deg': (12.0/24.0)*360.0, 'unit': "h"}]
82
83 DECIMAL_TICK_STEPS=[0.001, 0.0025, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 2.5, 5.0, 10.0, 30.0, 90.0]
84
85 DEG = u"\N{DEGREE SIGN}"
86 PRIME = "$^\prime$"
87 DOUBLE_PRIME = "$^{\prime\prime}$"
88
89
91 """This class describes a matplotlib image plot containing an astronomical image with an
92 associated WCS.
93
94 Objects within the image boundaries can be marked by passing their WCS coordinates to
95 L{ImagePlot.addPlotObjects}.
96
97 Other images can be overlaid using L{ImagePlot.addContourOverlay}.
98
99 For images rotated with North at the top, East at the left (as can be done using
100 L{astImages.clipRotatedImageSectionWCS} or L{astImages.resampleToTanProjection}, WCS coordinate
101 axes can be plotted, with tick marks set appropriately for the image size. Otherwise, a compass
102 can be plotted showing the directions of North and East in the image.
103
104 RGB images are also supported.
105
106 The plot can of course be tweaked further after creation using matplotlib/pylab commands.
107
108 """
109 - def __init__(self, imageData, imageWCS, axes = [0.1,0.1,0.8,0.8], \
110 cutLevels = ["smart", 99.5], colorMapName = "gray", title = None, axesLabels = "sexagesimal", \
111 axesFontFamily="serif", axesFontSize=12.0, RATickSteps="auto", decTickSteps="auto",
112 colorBar = False):
113 """Makes an ImagePlot from the given image array and astWCS. For coordinate axes to work, the
114 image and WCS should have been rotated such that East is at the left, North is at the top
115 (see e.g. L{astImages.clipRotatedImageSectionWCS}, or L{astImages.resampleToTanProjection}).
116
117 If imageData is given as a list in the format [r, g, b], a color RGB plot will be made. However,
118 in this case the cutLevels must be specified manually for each component as a list -
119 i.e. cutLevels = [[r min, r max], [g min, g max], [b min, b max]]. In this case of course, the
120 colorMap will be ignored. All r, g, b image arrays must have the same dimensions.
121
122 Set axesLabels = None to make a plot without coordinate axes plotted.
123
124 The axes can be marked in either sexagesimal or decimal celestial coordinates. If RATickSteps
125 or decTickSteps are set to "auto", the appropriate axis scales will be determined automatically
126 from the size of the image array and associated WCS. The tick step sizes can be overidden.
127 If the coordinate axes are in sexagesimal format a dictionary in the format {'deg', 'unit'} is
128 needed (see L{RA_TICK_STEPS} and L{DEC_TICK_STEPS} for examples). If the coordinate axes are in
129 decimal format, the tick step size is specified simply in RA, dec decimal degrees.
130
131 @type imageData: numpy array or list
132 @param imageData: image data array or list of numpy arrays [r, g, b]
133 @type imageWCS: astWCS.WCS
134 @param imageWCS: astWCS.WCS object
135 @type axes: list
136 @param axes: specifies where in the current figure to draw the finder chart (see pylab.axes)
137 @type cutLevels: list
138 @param cutLevels: sets the image scaling - available options:
139 - pixel values: cutLevels=[low value, high value].
140 - histogram equalisation: cutLevels=["histEq", number of bins ( e.g. 1024)]
141 - relative: cutLevels=["relative", cut per cent level (e.g. 99.5)]
142 - smart: cutLevels=["smart", cut per cent level (e.g. 99.5)]
143 ["smart", 99.5] seems to provide good scaling over a range of different images.
144 Note that for RGB images, cut levels must be specified manually i.e. as a list:
145 [[r min, rmax], [g min, g max], [b min, b max]]
146 @type colorMapName: string
147 @param colorMapName: name of a standard matplotlib colormap, e.g. "hot", "cool", "gray"
148 etc. (do "help(pylab.colormaps)" in the Python interpreter to see available options)
149 @type title: string
150 @param title: optional title for the plot
151 @type axesLabels: string
152 @param axesLabels: either "sexagesimal" (for H:M:S, D:M:S), "decimal" (for decimal degrees)
153 or None (for no coordinate axes labels)
154 @type axesFontFamily: string
155 @param axesFontFamily: matplotlib fontfamily, e.g. 'serif', 'sans-serif' etc.
156 @type axesFontSize: float
157 @param axesFontSize: font size of axes labels and titles (in points)
158 @type colorBar: bool
159 @param colorBar: if True, plot a vertical color bar at the side of the image indicating the intensity
160 scale.
161
162 """
163
164 self.RADeg, self.decDeg=imageWCS.getCentreWCSCoords()
165 self.wcs=imageWCS
166
167
168 if type(imageData) == list:
169 if len(imageData) == 3:
170 if len(cutLevels) == 3:
171 r=astImages.normalise(imageData[0], cutLevels[0])
172 g=astImages.normalise(imageData[1], cutLevels[1])
173 b=astImages.normalise(imageData[2], cutLevels[2])
174 rgb=numpy.array([r.transpose(), g.transpose(), b.transpose()])
175 rgb=rgb.transpose()
176 self.data=rgb
177 self.rgbImage=True
178 else:
179 raise Exception, "tried to create a RGB array, but cutLevels is not a list of 3 lists"
180
181 else:
182 raise Exception, "tried to create a RGB array but imageData is not a list of 3 arrays"
183 else:
184 self.data=imageData
185 self.rgbImage=False
186
187 self.axes=pylab.axes(axes)
188 self.cutLevels=cutLevels
189 self.colorMapName=colorMapName
190 self.title=title
191 self.axesLabels=axesLabels
192 self.colorBar=colorBar
193 self.axesFontSize=axesFontSize
194 self.axesFontFamily=axesFontFamily
195
196 self.flipXAxis=False
197 self.flipYAxis=False
198
199 if self.axesLabels != None:
200
201
202 if self.axesLabels == "sexagesimal":
203 if RATickSteps != "auto":
204 if type(RATickSteps) != dict or "deg" not in RATickSteps.keys() \
205 or "unit" not in RATickSteps.keys():
206 raise Exception, "RATickSteps needs to be in format {'deg', 'unit'} for sexagesimal axes labels"
207 if decTickSteps != "auto":
208 if type(decTickSteps) != dict or "deg" not in decTickSteps.keys() \
209 or "unit" not in decTickSteps.keys():
210 raise Exception, "decTickSteps needs to be in format {'deg', 'unit'} for sexagesimal axes labels"
211 elif self.axesLabels == "decimal":
212 if RATickSteps != "auto":
213 if type(RATickSteps) != float:
214 raise Exception, "RATickSteps needs to be a float (if not 'auto') for decimal axes labels"
215 if decTickSteps != "auto":
216 if type(decTickSteps) != float:
217 raise Exception, "decTickSteps needs to be a float (if not 'auto') for decimal axes labels"
218 self.RATickSteps=RATickSteps
219 self.decTickSteps=decTickSteps
220
221 self.calcWCSAxisLabels(axesLabels = self.axesLabels)
222
223
224 self.plotObjects=[]
225
226
227 self.contourOverlays=[]
228
229 self.draw()
230
231
233 """Redraws the ImagePlot.
234
235 """
236
237 pylab.axes(self.axes)
238 pylab.cla()
239
240 if self.title != None:
241 pylab.title(self.title)
242 try:
243 colorMap=pylab.cm.get_cmap(self.colorMapName)
244 except AssertionError:
245 raise Exception, self.colorMapName+"is not a defined matplotlib colormap."
246
247 if self.rgbImage == False:
248 self.cutImage=astImages.intensityCutImage(self.data, self.cutLevels)
249 if self.cutLevels[0]=="histEq":
250 pylab.imshow(self.cutImage['image'], interpolation="bilinear", origin='lower', cmap=colorMap)
251 else:
252 pylab.imshow(self.cutImage['image'], interpolation="bilinear", norm=self.cutImage['norm'], \
253 origin='lower', cmap=colorMap)
254 else:
255 pylab.imshow(self.data, interpolation="bilinear", origin='lower')
256
257 if self.colorBar == True:
258 pylab.colorbar(shrink=0.8)
259
260 for c in self.contourOverlays:
261 pylab.contour(c['contourData']['scaledImage'], c['contourData']['contourLevels'],
262 colors=c['color'], linewidths=c['width'])
263
264 for p in self.plotObjects:
265 for x, y, l in zip(p['x'], p['y'], p['objLabels']):
266 if p['symbol'] == "circle":
267 c=patches.Circle((x, y), radius=p['sizePix']/2.0, fill=False, edgecolor=p['color'],
268 linewidth=p['width'])
269 self.axes.add_patch(c)
270 elif p['symbol'] == "box":
271 c=patches.Rectangle((x-p['sizePix']/2, y-p['sizePix']/2), p['sizePix'], p['sizePix'],
272 fill=False, edgecolor=p['color'], linewidth=p['width'])
273 self.axes.add_patch(c)
274 elif p['symbol'] == "cross":
275 pylab.plot([x-p['sizePix']/2, x+p['sizePix']/2], [y, y], linestyle='-',
276 linewidth=p['width'], color= p['color'])
277 pylab.plot([x, x], [y-p['sizePix']/2, y+p['sizePix']/2], linestyle='-',
278 linewidth=p['width'], color= p['color'])
279 elif p['symbol'] == "diamond":
280 c=patches.RegularPolygon([x, y], 4, radius=p['sizePix']/2, orientation=0,
281 edgecolor=p['color'], fill=False, linewidth=p['width'])
282 self.axes.add_patch(c)
283 if l != None:
284 pylab.text(x, y+p['sizePix']/1.5, l, horizontalalignment='center', \
285 fontsize=p['objLabelSize'], color=p['color'])
286
287 if p['symbol'] == "compass":
288 x=p['x'][0]
289 y=p['y'][0]
290 ra=p['RA'][0]
291 dec=p['dec'][0]
292
293 westPoint,eastPoint,southPoint,northPoint=astCoords.calcRADecSearchBox(ra, dec, p['sizeArcSec']/3600.0/2.0)
294 northPix=self.wcs.wcs2pix(ra, northPoint)
295 eastPix=self.wcs.wcs2pix(eastPoint, dec)
296
297 edx=eastPix[0]-x
298 edy=eastPix[1]-y
299 ndx=northPix[0]-x
300 ndy=northPix[1]-y
301 nArrow=patches.Arrow(x, y, ndx, ndy, edgecolor=p['color'], facecolor=p['color'], width=p['width'])
302 eArrow=patches.Arrow(x, y, edx, edy, edgecolor=p['color'], facecolor=p['color'], width=p['width'])
303 self.axes.add_patch(nArrow)
304 self.axes.add_patch(eArrow)
305 pylab.text(x+ndx+ndx*0.2, y+ndy+ndy*0.2, "N", horizontalalignment='center',
306 verticalalignment='center', fontsize=p['objLabelSize'], color=p['color'])
307 pylab.text(x+edx+edx*0.2, y+edy+edy*0.2, "E", horizontalalignment='center',
308 verticalalignment='center', fontsize=p['objLabelSize'], color=p['color'])
309
310 if p['symbol'] == "scaleBar":
311 x=p['x'][0]
312 y=p['y'][0]
313 ra=p['RA'][0]
314 dec=p['dec'][0]
315
316 westPoint,eastPoint,southPoint,northPoint=astCoords.calcRADecSearchBox(ra, dec, p['sizeArcSec']/3600.0/2.0)
317 northPix=self.wcs.wcs2pix(ra, northPoint)
318 eastPix=self.wcs.wcs2pix(eastPoint, dec)
319 edx=eastPix[0]-x
320 edy=eastPix[1]-y
321 ndx=northPix[0]-x
322 ndy=northPix[1]-y
323
324 eArrow=patches.Arrow(x, y, edx, edy, edgecolor=p['color'], facecolor=p['color'], width=p['width'])
325 wArrow=patches.Arrow(x, y, -edx, edy, edgecolor=p['color'], facecolor=p['color'], width=p['width'])
326
327 self.axes.add_patch(eArrow)
328 self.axes.add_patch(wArrow)
329
330
331 scaleLabel=None
332 if p['sizeArcSec'] < 60.0:
333 scaleLabel="%.0f %s" % (p['sizeArcSec'], DOUBLE_PRIME)
334 elif p['sizeArcSec'] >= 60.0 and p['sizeArcSec'] < 3600.0:
335 scaleLabel="%.0f %s" % (p['sizeArcSec']/60.0, PRIME)
336 else:
337 scaleLabel="%.0f %s" % (p['sizeArcSec']/3600.0, DEG)
338
339 pylab.text(x, y+0.025*self.data.shape[1], scaleLabel, horizontalalignment='center',
340 verticalalignment='center', fontsize=p['objLabelSize'], color=p['color'])
341
342 if self.axesLabels != None:
343 pylab.xticks(self.ticsRA[0], self.ticsRA[1], weight='normal', family=self.axesFontFamily, \
344 fontsize=self.axesFontSize)
345 pylab.yticks(self.ticsDec[0], self.ticsDec[1], weight='normal', family=self.axesFontFamily, \
346 fontsize=self.axesFontSize)
347 pylab.xlabel(self.RAAxisLabel, family=self.axesFontFamily, fontsize=self.axesFontSize)
348 pylab.ylabel(self.decAxisLabel, family=self.axesFontFamily, fontsize=self.axesFontSize)
349 else:
350 pylab.xticks([], [])
351 pylab.yticks([], [])
352 pylab.xlabel("")
353 pylab.ylabel("")
354
355 if self.flipXAxis == False:
356 pylab.xlim(0, self.data.shape[1]-1)
357 else:
358 pylab.xlim(self.data.shape[1]-1, 0)
359 if self.flipYAxis == False:
360 pylab.ylim(0, self.data.shape[0]-1)
361 else:
362 pylab.ylim(self.data.shape[0]-1, 0)
363
364
365 - def addContourOverlay(self, contourImageData, contourWCS, tag, levels = ["linear", "min", "max", 5],
366 width = 1, color = "white", smooth = 0, highAccuracy = False):
367 """Adds image data to the ImagePlot as a contour overlay. The contours can be removed using
368 L{removeContourOverlay}. If a contour overlay already exists with this tag, it will be replaced.
369
370 @type contourImageData: numpy array
371 @param contourImageData: image data array from which contours are to be generated
372 @type contourWCS: astWCS.WCS
373 @param contourWCS: astWCS.WCS object for the image to be contoured
374 @type tag: string
375 @param tag: identifying tag for this set of contours
376 @type levels: list
377 @param levels: sets the contour levels - available options:
378 - values: contourLevels=[list of values specifying each level]
379 - linear spacing: contourLevels=['linear', min level value, max level value, number
380 of levels] - can use "min", "max" to automatically set min, max levels from image data
381 - log spacing: contourLevels=['log', min level value, max level value, number of
382 levels] - can use "min", "max" to automatically set min, max levels from image data
383 @type width: int
384 @param width: width of the overlaid contours
385 @type color: string
386 @param color: color of the overlaid contours, specified by the name of a standard
387 matplotlib color, e.g., "black", "white", "cyan"
388 etc. (do "help(pylab.colors)" in the Python interpreter to see available options)
389 @type smooth: float
390 @param smooth: standard deviation (in arcsec) of Gaussian filter for
391 pre-smoothing of contour image data (set to 0 for no smoothing)
392 @type highAccuracy: bool
393 @param highAccuracy: if True, sample every corresponding pixel in each image; otherwise, sample
394 every nth pixel, where n = the ratio of the image scales.
395
396 """
397
398 if self.rgbImage == True:
399 backgroundData=self.data[:,:,0]
400 else:
401 backgroundData=self.data
402 contourData=astImages.generateContourOverlay(backgroundData, self.wcs, contourImageData, \
403 contourWCS, levels, smooth, highAccuracy = highAccuracy)
404
405 alreadyGot=False
406 for c in self.contourOverlays:
407 if c['tag'] == tag:
408 c['contourData']=contourData
409 c['tag']=tag
410 c['color']=color
411 c['width']=width
412 alreadyGot=True
413
414 if alreadyGot == False:
415 self.contourOverlays.append({'contourData': contourData, 'tag': tag, 'color': color, \
416 'width': width})
417 self.draw()
418
419
421 """Removes the contourOverlay from the ImagePlot corresponding to the tag.
422
423 @type tag: string
424 @param tag: tag for contour overlay in ImagePlot.contourOverlays to be removed
425
426 """
427
428 index=0
429 for p in self.contourOverlays:
430 if p['tag'] == tag:
431 self.plotObjects.remove(self.plotObjects[index])
432 index=index+1
433 self.draw()
434
435
436 - def addPlotObjects(self, objRAs, objDecs, tag, symbol="circle", size=4.0, width=1.0, color="yellow",
437 objLabels = None, objLabelSize = 12.0):
438 """Add objects with RA, dec coords objRAs, objDecs to the ImagePlot. Only objects that fall within
439 the image boundaries will be plotted.
440
441 symbol specifies the type of symbol with which to mark the object in the image. The following
442 values are allowed:
443 - "circle"
444 - "box"
445 - "cross"
446 - "diamond"
447
448 size specifies the diameter in arcsec of the symbol (if plotSymbol == "circle"), or the width
449 of the box in arcsec (if plotSymbol == "box")
450
451 width specifies the thickness of the symbol lines in pixels
452
453 color can be any valid matplotlib color (e.g. "red", "green", etc.)
454
455 The objects can be removed from the plot by using removePlotObjects(), and then calling
456 draw(). If the ImagePlot already has a set of plotObjects with the same tag, they will be
457 replaced.
458
459 @type objRAs: numpy array or list
460 @param objRAs: object RA coords in decimal degrees
461 @type objDecs: numpy array or list
462 @param objDecs: corresponding object Dec. coords in decimal degrees
463 @type tag: string
464 @param tag: identifying tag for this set of objects
465 @type symbol: string
466 @param symbol: either "circle", "box", "cross", or "diamond"
467 @type size: float
468 @param size: size of symbols to plot (radius in arcsec, or width of box)
469 @type width: float
470 @param width: width of symbols in pixels
471 @type color: string
472 @param color: any valid matplotlib color string, e.g. "red", "green" etc.
473 @type objLabels: list
474 @param objLabels: text labels to plot next to objects in figure
475 @type objLabelSize: float
476 @param objLabelSize: size of font used for object labels (in points)
477
478 """
479
480 pixCoords=self.wcs.wcs2pix(objRAs, objDecs)
481
482 xMax=self.data.shape[1]
483 yMax=self.data.shape[0]
484
485 if objLabels == None:
486 objLabels=[None]*len(objRAs)
487
488 xInPlot=[]
489 yInPlot=[]
490 RAInPlot=[]
491 decInPlot=[]
492 labelInPlot=[]
493 for p, r, d, l in zip(pixCoords, objRAs, objDecs, objLabels):
494 if p[0] >= 0 and p[0] < xMax and p[1] >= 0 and p[1] < yMax:
495 xInPlot.append(p[0])
496 yInPlot.append(p[1])
497 RAInPlot.append(r)
498 decInPlot.append(d)
499 labelInPlot.append(l)
500
501 xInPlot=numpy.array(xInPlot)
502 yInPlot=numpy.array(yInPlot)
503 RAInPlot=numpy.array(RAInPlot)
504 decInPlot=numpy.array(decInPlot)
505
506
507 sizePix=(size/3600.0)/self.wcs.getPixelSizeDeg()
508
509 alreadyGot=False
510 for p in self.plotObjects:
511 if p['tag'] == tag:
512 p['x']=xInPlot
513 p['y']=yInPlot
514 p['RA']=RAInPlot
515 p['dec']=decInPlot
516 p['tag']=tag
517 p['objLabels']=objLabels
518 p['symbol']=symbol
519 p['sizePix']=sizePix
520 p['sizeArcSec']=size
521 p['width']=width
522 p['color']=color
523 p['objLabelSize']=objLabelSize
524 alreadyGot=True
525
526 if alreadyGot == False:
527 self.plotObjects.append({'x': xInPlot, 'y': yInPlot, 'RA': RAInPlot, 'dec': decInPlot,
528 'tag': tag, 'objLabels': labelInPlot, 'symbol': symbol,
529 'sizePix': sizePix, 'width': width, 'color': color,
530 'objLabelSize': objLabelSize, 'sizeArcSec': size})
531 self.draw()
532
533
535 """Removes the plotObjects from the ImagePlot corresponding to the tag. The plot must be redrawn
536 for the change to take effect.
537
538 @type tag: string
539 @param tag: tag for set of objects in ImagePlot.plotObjects to be removed
540
541 """
542
543 index=0
544 for p in self.plotObjects:
545 if p['tag'] == tag:
546 self.plotObjects.remove(self.plotObjects[index])
547 index=index+1
548 self.draw()
549
550
551 - def addCompass(self, location, sizeArcSec, color = "white", fontSize = 12, \
552 width = 20.0):
553 """Adds a compass to the ImagePlot at the given location ('N', 'NE', 'E', 'SE', 'S',
554 'SW', 'W', or 'NW'). Note these aren't directions on the WCS coordinate grid, they are
555 relative positions on the plot - so N is top centre, NE is top right, SW is bottom right etc..
556 Alternatively, pixel coordinates (x, y) in the image can be given.
557
558 @type location: string or tuple
559 @param location: location in the plot where the compass is drawn:
560 - string: N, NE, E, SE, S, SW, W or NW
561 - tuple: (x, y)
562 @type sizeArcSec: float
563 @param sizeArcSec: length of the compass arrows on the plot in arc seconds
564 @type color: string
565 @param color: any valid matplotlib color string
566 @type fontSize: float
567 @param fontSize: size of font used to label N and E, in points
568 @type width: float
569 @param width: width of arrows used to mark compass
570
571 """
572
573 if type(location) == str:
574 cRADeg, cDecDeg=self.wcs.getCentreWCSCoords()
575 RAMin, RAMax, decMin, decMax=self.wcs.getImageMinMaxWCSCoords()
576 westPoint,eastPoint,southPoint,northPoint=astCoords.calcRADecSearchBox(cRADeg, cDecDeg, sizeArcSec/3600.0/2.0)
577 sizeRADeg=eastPoint-westPoint
578 sizeDecDeg=northPoint-southPoint
579 xSizePix=(sizeArcSec/3600.0)/self.wcs.getXPixelSizeDeg()
580 ySizePix=(sizeArcSec/3600.0)/self.wcs.getYPixelSizeDeg()
581 X=self.data.shape[1]
582 Y=self.data.shape[0]
583 xBufferPix=0.5*xSizePix
584 yBufferPix=0.5*ySizePix
585 cx, cy=self.wcs.wcs2pix(cRADeg, cDecDeg)
586 foundLocation=False
587 x=cy
588 y=cx
589 if self.wcs.isFlipped() == False:
590 if location.find("N") != -1:
591 y=Y-2*yBufferPix
592 foundLocation=True
593 if location.find("S") != -1:
594 y=yBufferPix
595 foundLocation=True
596 if location.find("E") != -1:
597 x=xBufferPix*2
598 foundLocation=True
599 if location.find("W") != -1:
600 x=X-xBufferPix
601 foundLocation=True
602 else:
603 if location.find("S") != -1:
604 y=Y-2*yBufferPix
605 foundLocation=True
606 if location.find("N") != -1:
607 y=yBufferPix
608 foundLocation=True
609 if location.find("W") != -1:
610 x=xBufferPix*2
611 foundLocation=True
612 if location.find("E") != -1:
613 x=X-xBufferPix
614 foundLocation=True
615 if foundLocation == False:
616 raise Exception, "didn't understand location string for scale bar (should be e.g. N, S, E, W)."
617 RADeg, decDeg=self.wcs.pix2wcs(x, y)
618 elif type(location) == tuple or type(location) == list:
619 x, y=location
620 RADeg, decDeg=self.wcs.pix2wcs(x, y)
621 else:
622 raise Exception, "didn't understand location for scale bar - should be string or tuple."
623
624 alreadyGot=False
625 for p in self.plotObjects:
626 if p['tag'] == "compass":
627 p['x']=[x]
628 p['y']=[y]
629 p['RA']=[RADeg]
630 p['dec']=[decDeg]
631 p['tag']="compass"
632 p['objLabels']=[None]
633 p['symbol']="compass"
634 p['sizeArcSec']=sizeArcSec
635 p['width']=width
636 p['color']=color
637 p['objLabelSize']=fontSize
638 alreadyGot=True
639
640 if alreadyGot == False:
641 self.plotObjects.append({'x': [x], 'y': [y], 'RA': [RADeg], 'dec': [decDeg],
642 'tag': "compass", 'objLabels': [None], 'symbol': "compass",
643 'width': width, 'color': color,
644 'objLabelSize': fontSize, 'sizeArcSec': sizeArcSec})
645 self.draw()
646
647
648 - def addScaleBar(self, location, sizeArcSec, color = "white", fontSize = 12, \
649 width = 20.0):
650 """Adds a scale bar to the ImagePlot at the given location ('N', 'NE', 'E', 'SE', 'S',
651 'SW', 'W', or 'NW'). Note these aren't directions on the WCS coordinate grid, they are
652 relative positions on the plot - so N is top centre, NE is top right, SW is bottom right etc..
653 Alternatively, pixel coordinates (x, y) in the image can be given.
654
655 @type location: string or tuple
656 @param location: location in the plot where the compass is drawn:
657 - string: N, NE, E, SE, S, SW, W or NW
658 - tuple: (x, y)
659 @type sizeArcSec: float
660 @param sizeArcSec: scale length to indicate on the plot in arc seconds
661 @type color: string
662 @param color: any valid matplotlib color string
663 @type fontSize: float
664 @param fontSize: size of font used to label N and E, in points
665 @type width: float
666 @param width: width of arrow used to mark scale
667
668 """
669
670
671 if type(location) == str:
672 cRADeg, cDecDeg=self.wcs.getCentreWCSCoords()
673 RAMin, RAMax, decMin, decMax=self.wcs.getImageMinMaxWCSCoords()
674 westPoint,eastPoint,southPoint,northPoint=astCoords.calcRADecSearchBox(cRADeg, cDecDeg, sizeArcSec/3600.0/2.0)
675 sizeRADeg=eastPoint-westPoint
676 sizeDecDeg=northPoint-southPoint
677 xSizePix=(sizeArcSec/3600.0)/self.wcs.getXPixelSizeDeg()
678 ySizePix=(sizeArcSec/3600.0)/self.wcs.getYPixelSizeDeg()
679 X=self.data.shape[1]
680 Y=self.data.shape[0]
681 xBufferPix=0.6*ySizePix
682 yBufferPix=0.05*Y
683 cx, cy=self.wcs.wcs2pix(cRADeg, cDecDeg)
684 foundLocation=False
685 x=cy
686 y=cx
687 if self.wcs.isFlipped() == False:
688 if location.find("N") != -1:
689 y=Y-1.5*yBufferPix
690 foundLocation=True
691 if location.find("S") != -1:
692 y=yBufferPix
693 foundLocation=True
694 if location.find("E") != -1:
695 x=xBufferPix
696 foundLocation=True
697 if location.find("W") != -1:
698 x=X-xBufferPix
699 foundLocation=True
700 else:
701 if location.find("S") != -1:
702 y=Y-1.5*yBufferPix
703 foundLocation=True
704 if location.find("N") != -1:
705 y=yBufferPix
706 foundLocation=True
707 if location.find("W") != -1:
708 x=xBufferPix
709 foundLocation=True
710 if location.find("E") != -1:
711 x=X-xBufferPix
712 foundLocation=True
713 if foundLocation == False:
714 raise Exception, "didn't understand location string for scale bar (should be e.g. N, S, E, W)."
715 RADeg, decDeg=self.wcs.pix2wcs(x, y)
716 elif type(location) == tuple or type(location) == list:
717 x, y=location
718 RADeg, decDeg=self.wcs.pix2wcs(x, y)
719 else:
720 raise Exception, "didn't understand location for scale bar - should be string or tuple."
721
722 alreadyGot=False
723 for p in self.plotObjects:
724 if p['tag'] == "scaleBar":
725 p['x']=[x]
726 p['y']=[y]
727 p['RA']=[RADeg]
728 p['dec']=[decDeg]
729 p['tag']="scaleBar"
730 p['objLabels']=[None]
731 p['symbol']="scaleBar"
732 p['sizeArcSec']=sizeArcSec
733 p['width']=width
734 p['color']=color
735 p['objLabelSize']=fontSize
736 alreadyGot=True
737
738 if alreadyGot == False:
739 self.plotObjects.append({'x': [x], 'y': [y], 'RA': [RADeg], 'dec': [decDeg],
740 'tag': "scaleBar", 'objLabels': [None], 'symbol': "scaleBar",
741 'width': width, 'color': color,
742 'objLabelSize': fontSize, 'sizeArcSec': sizeArcSec})
743 self.draw()
744
745
747 """This function calculates the positions of coordinate labels for the RA and Dec axes of the
748 ImagePlot. The tick steps are calculated automatically unless self.RATickSteps,
749 self.decTickSteps are set to values other than "auto" (see L{ImagePlot.__init__}).
750
751 The ImagePlot must be redrawn for changes to be applied.
752
753 @type axesLabels: string
754 @param axesLabels: either "sexagesimal" (for H:M:S, D:M:S), "decimal" (for decimal degrees),
755 or None for no coordinate axes labels
756
757 """
758
759
760 equinox=self.wcs.getEquinox()
761 if equinox<1984:
762 equinoxLabel="B"+str(int(equinox))
763 else:
764 equinoxLabel="J"+str(int(equinox))
765
766 self.axesLabels=axesLabels
767
768 ticsDict=self.getTickSteps()
769
770
771 if self.RATickSteps != "auto":
772 ticsDict['major']['RA']=self.RATickSteps
773 if self.decTickSteps != "auto":
774 ticsDict['major']['dec']=self.decTickSteps
775
776 RALocs=[]
777 decLocs=[]
778 RALabels=[]
779 decLabels=[]
780 key="major"
781
782 if self.axesLabels == "sexagesimal":
783 self.RAAxisLabel="R.A. ("+equinoxLabel+")"
784 self.decAxisLabel="Dec. ("+equinoxLabel+")"
785 RADegStep=ticsDict[key]['RA']['deg']
786 decDegStep=ticsDict[key]['dec']['deg']
787 elif self.axesLabels == "decimal":
788 self.RAAxisLabel="R.A. Degrees ("+equinoxLabel+")"
789 self.decAxisLabel="Dec. Degrees ("+equinoxLabel+")"
790 RADegStep=ticsDict[key]['RA']
791 decDegStep=ticsDict[key]['dec']
792 else:
793 raise Exception, "axesLabels must be either 'sexagesimal' or 'decimal'"
794
795 xArray=numpy.arange(0, self.data.shape[1], 1)
796 yArray=numpy.arange(0, self.data.shape[0], 1)
797 xWCS=self.wcs.pix2wcs(xArray, numpy.zeros(xArray.shape[0], dtype=float))
798 yWCS=self.wcs.pix2wcs(numpy.zeros(yArray.shape[0], dtype=float), yArray)
799 xWCS=numpy.array(xWCS)
800 yWCS=numpy.array(yWCS)
801 ras=xWCS[:,0]
802 decs=yWCS[:,1]
803 RAEdges=numpy.array([ras[0], ras[-1]])
804 RAMin=RAEdges.min()
805 RAMax=RAEdges.max()
806 decMin=decs.min()
807 decMax=decs.max()
808
809
810 midRAPix, midDecPix=self.wcs.wcs2pix((RAEdges[1]+RAEdges[0])/2.0, (decMax+decMin)/2.0)
811 if midRAPix < 0 or midRAPix > self.wcs.header['NAXIS1']:
812 wrappedRA=True
813 else:
814 wrappedRA=False
815
816
817 if ras[1] < ras[0]:
818 self.flipXAxis=False
819 ra2x=interpolate.interp1d(ras[::-1], xArray[::-1], kind='linear')
820 else:
821 self.flipXAxis=True
822 ra2x=interpolate.interp1d(ras, xArray, kind='linear')
823 if decs[1] < decs[0]:
824 self.flipYAxis=True
825 dec2y=interpolate.interp1d(decs[::-1], yArray[::-1], kind='linear')
826 else:
827 self.flipYAxis=False
828 dec2y=interpolate.interp1d(decs, yArray, kind='linear')
829
830 if wrappedRA == False:
831 RAPlotMin=RADegStep*math.modf(RAMin/RADegStep)[1]
832 RAPlotMax=RADegStep*math.modf(RAMax/RADegStep)[1]
833 if RAPlotMin < RAMin:
834 RAPlotMin=RAPlotMin+RADegStep
835 if RAPlotMax >= RAMax:
836 RAPlotMax=RAPlotMax-RADegStep
837 RADegs=numpy.arange(RAPlotMin, RAPlotMax+0.0001, RADegStep)
838 else:
839 RAPlotMin=RADegStep*math.modf(RAMin/RADegStep)[1]
840 RAPlotMax=RADegStep*math.modf(RAMax/RADegStep)[1]
841 if RAPlotMin > RAMin:
842 RAPlotMin=RAPlotMin-RADegStep
843 if RAPlotMax <= RAMax:
844 RAPlotMax=RAPlotMax+RADegStep
845 for i in range(ras.shape[0]):
846 if ras[i] >= RAMax and ras[i] <= 360.0:
847 ras[i]=ras[i]-360.0
848 if ras[1] < ras[0]:
849 ra2x=interpolate.interp1d(ras[::-1], xArray[::-1], kind='linear')
850 else:
851 ra2x=interpolate.interp1d(ras, xArray, kind='linear')
852 RADegs=numpy.arange(RAPlotMin, RAPlotMax-360.0-0.0001, -RADegStep)
853
854 decPlotMin=decDegStep*math.modf(decMin/decDegStep)[1]
855 decPlotMax=decDegStep*math.modf(decMax/decDegStep)[1]
856 if decPlotMin < decMin:
857 decPlotMin=decPlotMin+decDegStep
858 if decPlotMax >= decMax:
859 decPlotMax=decPlotMax-decDegStep
860 decDegs=numpy.arange(decPlotMin, decPlotMax+0.0001, decDegStep)
861
862 if key == "major":
863 if axesLabels == "sexagesimal":
864 for r in RADegs:
865 if r < 0:
866 r=r+360.0
867 h, m, s=astCoords.decimal2hms(r, ":").split(":")
868 hInt=int(round(float(h)))
869 if ticsDict[key]['RA']['unit'] == 'h' and (60.0-float(m)) < 0.01:
870 hInt=hInt+1
871 if hInt < 10:
872 hString="0"+str(hInt)
873 else:
874 hString=str(hInt)
875 mInt=int(round(float(m)))
876 if ticsDict[key]['RA']['unit'] == 'm' and (60.0-float(s)) < 0.01:
877 mInt=mInt+1
878 if mInt < 10:
879 mString="0"+str(mInt)
880 else:
881 mString=str(mInt)
882 sInt=int(round(float(s)))
883 if sInt < 10:
884 sString="0"+str(sInt)
885 else:
886 sString=str(sInt)
887 if ticsDict[key]['RA']['unit'] == 'h':
888 rString=hString+"$^h$"
889 elif ticsDict[key]['RA']['unit'] == 'm':
890 rString=hString+"$^h$"+mString+"$^m$"
891 else:
892 rString=hString+"$^h$"+mString+"$^m$"+sString+"$^s$"
893 RALabels.append(rString)
894 for D in decDegs:
895 d, m, s=astCoords.decimal2dms(D, ":").split(":")
896 dInt=int(round(float(d)))
897 if ticsDict[key]['dec']['unit'] == 'd' and (60.0-float(m)) < 0.01:
898 dInt=dInt+1
899 if dInt < 10 and dInt >= 0 and D > 0:
900 dString="+0"+str(dInt)
901 elif dInt > -10 and dInt <= 0 and D < 0:
902 dString="-0"+str(abs(dInt))
903 elif dInt >= 10:
904 dString="+"+str(dInt)
905 else:
906 dString=str(dInt)
907 mInt=int(round(float(m)))
908 if ticsDict[key]['dec']['unit'] == 'm' and (60.0-float(s)) < 0.01:
909 mInt=mInt+1
910 if mInt < 10:
911 mString="0"+str(mInt)
912 else:
913 mString=str(mInt)
914 sInt=int(round(float(s)))
915 if sInt < 10:
916 sString="0"+str(sInt)
917 else:
918 sString=str(sInt)
919 if ticsDict[key]['dec']['unit'] == 'd':
920 dString=dString+DEG
921 elif ticsDict[key]['dec']['unit'] == 'm':
922 dString=dString+DEG+mString+PRIME
923 else:
924 dString=dString+DEG+mString+PRIME+sString+DOUBLE_PRIME
925 decLabels.append(dString)
926 elif axesLabels == "decimal":
927 if wrappedRA == False:
928 RALabels=RALabels+RADegs.tolist()
929 else:
930 nonNegativeLabels=[]
931 for r in RADegs:
932 if r < 0:
933 r=r+360.0
934 nonNegativeLabels.append(r)
935 RALabels=RALabels+nonNegativeLabels
936 decLabels=decLabels+decDegs.tolist()
937
938
939 dps=[]
940 for r in RALabels:
941 dps.append(len(str(r).split(".")[-1]))
942 dpNumRA=int(math.ceil(numpy.array(dps).mean()))
943 for i in range(len(RALabels)):
944 fString="%."+str(dpNumRA)+"f"
945 RALabels[i]=fString % (RALabels[i])
946 dps=[]
947 for d in decLabels:
948 dps.append(len(str(d).split(".")[-1]))
949 dpNumDec=int(math.ceil(numpy.array(dps).mean()))
950 for i in range(len(decLabels)):
951 fString="%."+str(dpNumDec)+"f"
952 decLabels[i]=fString % (decLabels[i])
953
954 if key == 'minor':
955 RALabels=RALabels+RADegs.shape[0]*['']
956 decLabels=decLabels+decDegs.shape[0]*['']
957
958 RALocs=RALocs+ra2x(RADegs).tolist()
959 decLocs=decLocs+dec2y(decDegs).tolist()
960
961 self.ticsRA=[RALocs, RALabels]
962 self.ticsDec=[decLocs, decLabels]
963
964
965 - def save(self, fileName):
966 """Saves the ImagePlot in any format that matplotlib can understand, as determined from the
967 fileName extension.
968
969 @type fileName: string
970 @param fileName: path where plot will be written
971
972 """
973
974 pylab.draw()
975 pylab.savefig(fileName)
976
977
979 """Chooses the appropriate WCS coordinate tick steps for the plot based on its size.
980 Whether the ticks are decimal or sexagesimal is set by self.axesLabels.
981
982 Note: minor ticks not used at the moment.
983
984 @rtype: dictionary
985 @return: tick step sizes for major, minor plot ticks, in format {'major', 'minor'}
986
987 """
988
989
990 xArray=numpy.arange(0, self.data.shape[1], 1)
991 yArray=numpy.arange(0, self.data.shape[0], 1)
992 xWCS=self.wcs.pix2wcs(xArray, numpy.zeros(xArray.shape[0], dtype=float))
993 yWCS=self.wcs.pix2wcs(numpy.zeros(yArray.shape[0], dtype=float), yArray)
994 xWCS=numpy.array(xWCS)
995 yWCS=numpy.array(yWCS)
996 ras=xWCS[:,0]
997 decs=yWCS[:,1]
998 RAEdges=numpy.array([ras[0], ras[-1]])
999 RAMin=RAEdges.min()
1000 RAMax=RAEdges.max()
1001 decMin=decs.min()
1002 decMax=decs.max()
1003
1004
1005 midRAPix, midDecPix=self.wcs.wcs2pix((RAEdges[1]+RAEdges[0])/2.0, (decMax+decMin)/2.0)
1006 if midRAPix < 0 or midRAPix > self.wcs.header['NAXIS1']:
1007 wrappedRA=True
1008 else:
1009 wrappedRA=False
1010 if wrappedRA == False:
1011 RAWidthDeg=RAMax-RAMin
1012 else:
1013 RAWidthDeg=(360.0-RAMax)+RAMin
1014 decHeightDeg=decMax-decMin
1015
1016 ticsDict={}
1017 ticsDict['major']={}
1018 ticsDict['minor']={}
1019 if self.axesLabels == "sexagesimal":
1020
1021 matchIndex = 0
1022 for i in range(len(RA_TICK_STEPS)):
1023 if RAWidthDeg/2.5 > RA_TICK_STEPS[i]['deg']:
1024 matchIndex = i
1025
1026 ticsDict['major']['RA']=RA_TICK_STEPS[matchIndex]
1027 ticsDict['minor']['RA']=RA_TICK_STEPS[matchIndex-1]
1028
1029 matchIndex = 0
1030 for i in range(len(DEC_TICK_STEPS)):
1031 if decHeightDeg/2.5 > DEC_TICK_STEPS[i]['deg']:
1032 matchIndex = i
1033
1034 ticsDict['major']['dec']=DEC_TICK_STEPS[matchIndex]
1035 ticsDict['minor']['dec']=DEC_TICK_STEPS[matchIndex-1]
1036
1037 return ticsDict
1038
1039 elif self.axesLabels == "decimal":
1040
1041 matchIndex = 0
1042 for i in range(len(DECIMAL_TICK_STEPS)):
1043 if RAWidthDeg/2.5 > DECIMAL_TICK_STEPS[i]:
1044 matchIndex = i
1045
1046 ticsDict['major']['RA']=DECIMAL_TICK_STEPS[matchIndex]
1047 ticsDict['minor']['RA']=DECIMAL_TICK_STEPS[matchIndex-1]
1048
1049 matchIndex = 0
1050 for i in range(len(DECIMAL_TICK_STEPS)):
1051 if decHeightDeg/2.5 > DECIMAL_TICK_STEPS[i]:
1052 matchIndex = i
1053
1054 ticsDict['major']['dec']=DECIMAL_TICK_STEPS[matchIndex]
1055 ticsDict['minor']['dec']=DECIMAL_TICK_STEPS[matchIndex-1]
1056
1057 return ticsDict
1058
1059 else:
1060 raise Exception, "axesLabels must be either 'sexagesimal' or 'decimal'"
1061