1
2 """module for producing astronomical plots
3
4 (c) 2007-2009 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 northPoint=dec+p['sizeArcSec']/3600.0
293 eastPoint=ra+p['sizeArcSec']/3600.0
294 sizePix=(p['sizeArcSec']/3600.0)/self.wcs.getPixelSizeDeg()
295 northPix=self.wcs.wcs2pix(ra, northPoint)
296 eastPix=self.wcs.wcs2pix(eastPoint, dec)
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 northPoint=dec+p['sizeArcSec']/3600.0
316 eastPoint=ra+p['sizeArcSec']/3600.0
317 sizePix=(p['sizeArcSec']/3600.0)/self.wcs.getPixelSizeDeg()
318 northPix=self.wcs.wcs2pix(ra, northPoint)
319 eastPix=self.wcs.wcs2pix(eastPoint, dec)
320 edx=eastPix[0]-x
321 edy=eastPix[1]-y
322 ndx=northPix[0]-x
323 ndy=northPix[1]-y
324
325 eArrow=patches.Arrow(x+edx/2, y, edx/2, edy, edgecolor=p['color'], facecolor=p['color'], width=p['width'])
326 wArrow=patches.Arrow(x+edx/2, y, -edx/2, edy, edgecolor=p['color'], facecolor=p['color'], width=p['width'])
327
328
329 self.axes.add_patch(eArrow)
330 self.axes.add_patch(wArrow)
331
332
333 scaleLabel=None
334 if p['sizeArcSec'] < 60.0:
335 scaleLabel="%.0f %s" % (p['sizeArcSec'], DOUBLE_PRIME)
336 elif p['sizeArcSec'] >= 60.0 and p['sizeArcSec'] < 3600.0:
337 scaleLabel="%.0f %s" % (p['sizeArcSec']/60.0, PRIME)
338 else:
339 scaleLabel="%.0f %s" % (p['sizeArcSec']/3600.0, DEG)
340
341 pylab.text(x+edx/2, y+0.025*self.data.shape[1], scaleLabel, horizontalalignment='center',
342 verticalalignment='center', fontsize=p['objLabelSize'], color=p['color'])
343
344
345
346 if self.axesLabels != None:
347 pylab.xticks(self.ticsRA[0], self.ticsRA[1], weight='normal', family=self.axesFontFamily, \
348 fontsize=self.axesFontSize)
349 pylab.yticks(self.ticsDec[0], self.ticsDec[1], weight='normal', family=self.axesFontFamily, \
350 fontsize=self.axesFontSize)
351 pylab.xlabel(self.RAAxisLabel, family=self.axesFontFamily, fontsize=self.axesFontSize)
352 pylab.ylabel(self.decAxisLabel, family=self.axesFontFamily, fontsize=self.axesFontSize)
353 else:
354 pylab.xticks([], [])
355 pylab.yticks([], [])
356 pylab.xlabel("")
357 pylab.ylabel("")
358
359 if self.flipXAxis == False:
360 pylab.xlim(0, self.data.shape[1]-1)
361 else:
362 pylab.xlim(self.data.shape[1]-1, 0)
363 if self.flipYAxis == False:
364 pylab.ylim(0, self.data.shape[0]-1)
365 else:
366 pylab.ylim(self.data.shape[0]-1, 0)
367
368
369 - def addContourOverlay(self, contourImageData, contourWCS, tag, levels = ["linear", "min", "max", 5],
370 width = 1, color = "white", smooth = 0, highAccuracy = False):
371 """Adds image data to the ImagePlot as a contour overlay. The contours can be removed using L{removeContourOverlay}. If a contour overlay already exists with this tag, it will be replaced.
372
373 @type contourImageData: numpy array
374 @param contourImageData: image data array from which contours are to be generated
375 @type contourWCS: astWCS.WCS
376 @param contourWCS: astWCS.WCS object for the image to be contoured
377 @type tag: string
378 @param tag: identifying tag for this set of contours
379 @type levels: list
380 @param levels: sets the contour levels - available options:
381 - values: contourLevels=[list of values specifying each level]
382 - linear spacing: contourLevels=['linear', min level value, max level value, number
383 of levels] - can use "min", "max" to automatically set min, max levels from image data
384 - log spacing: contourLevels=['log', min level value, max level value, number of
385 levels] - can use "min", "max" to automatically set min, max levels from image data
386 @type width: int
387 @param width: width of the overlaid contours
388 @type color: string
389 @param color: color of the overlaid contours, specified by the name of a standard
390 matplotlib color, e.g., "black", "white", "cyan"
391 etc. (do "help(pylab.colors)" in the Python interpreter to see available options)
392 @type smooth: float
393 @param smooth: standard deviation (in arcsec) of Gaussian filter for
394 pre-smoothing of contour image data (set to 0 for no smoothing)
395 @type highAccuracy: bool
396 @param highAccuracy: if True, sample every corresponding pixel in each image; otherwise, sample
397 every nth pixel, where n = the ratio of the image scales.
398
399 """
400
401 if self.rgbImage == True:
402 backgroundData=self.data[:,:,0]
403 else:
404 backgroundData=self.data
405 contourData=astImages.generateContourOverlay(backgroundData, self.wcs, contourImageData, \
406 contourWCS, levels, smooth, highAccuracy = highAccuracy)
407
408 alreadyGot=False
409 for c in self.contourOverlays:
410 if c['tag'] == tag:
411 c['contourData']=contourData
412 c['tag']=tag
413 c['color']=color
414 c['width']=width
415 alreadyGot=True
416
417 if alreadyGot == False:
418 self.contourOverlays.append({'contourData': contourData, 'tag': tag, 'color': color, \
419 'width': width})
420 self.draw()
421
422
424 """Removes the contourOverlay from the ImagePlot corresponding to the tag.
425
426 @type tag: string
427 @param tag: tag for contour overlay in ImagePlot.contourOverlays to be removed
428
429 """
430
431 index=0
432 for p in self.contourOverlays:
433 if p['tag'] == tag:
434 self.plotObjects.remove(self.plotObjects[index])
435 index=index+1
436 self.draw()
437
438
439 - def addPlotObjects(self, objRAs, objDecs, tag, symbol="circle", size=4.0, width=1.0, color="yellow",
440 objLabels = None, objLabelSize = 12.0):
441 """Add objects with RA, dec coords objRAs, objDecs to the ImagePlot. Only objects that fall within
442 the image boundaries will be plotted.
443
444 symbol specifies the type of symbol with which to mark the object in the image. The following
445 values are allowed:
446 - "circle"
447 - "box"
448 - "cross"
449 - "diamond"
450
451 size specifies the diameter in arcsec of the symbol (if plotSymbol == "circle"), or the width
452 of the box in arcsec (if plotSymbol == "box")
453
454 width specifies the thickness of the symbol lines in pixels
455
456 color can be any valid matplotlib color (e.g. "red", "green", etc.)
457
458 The objects can be removed from the plot by using removePlotObjects(), and then calling
459 draw(). If the ImagePlot already has a set of plotObjects with the same tag, they will be
460 replaced.
461
462 @type objRAs: numpy array or list
463 @param objRAs: object RA coords in decimal degrees
464 @type objDecs: numpy array or list
465 @param objDecs: corresponding object Dec. coords in decimal degrees
466 @type tag: string
467 @param tag: identifying tag for this set of objects
468 @type symbol: string
469 @param symbol: either "circle", "box", "cross", or "diamond"
470 @type size: float
471 @param size: size of symbols to plot (radius in arcsec, or width of box)
472 @type width: float
473 @param width: width of symbols in pixels
474 @type color: string
475 @param color: any valid matplotlib color string, e.g. "red", "green" etc.
476 @type objLabels: list
477 @param objLabels: text labels to plot next to objects in figure
478 @type objLabelSize: float
479 @param objLabelSize: size of font used for object labels (in points)
480
481 """
482
483 pixCoords=self.wcs.wcs2pix(objRAs, objDecs)
484
485 xMax=self.data.shape[1]
486 yMax=self.data.shape[0]
487
488 if objLabels == None:
489 objLabels=[None]*len(objRAs)
490
491 xInPlot=[]
492 yInPlot=[]
493 RAInPlot=[]
494 decInPlot=[]
495 labelInPlot=[]
496 for p, r, d, l in zip(pixCoords, objRAs, objDecs, objLabels):
497 if p[0] >= 0 and p[0] < xMax and p[1] >= 0 and p[1] < yMax:
498 xInPlot.append(p[0])
499 yInPlot.append(p[1])
500 RAInPlot.append(r)
501 decInPlot.append(d)
502 labelInPlot.append(l)
503
504 xInPlot=numpy.array(xInPlot)
505 yInPlot=numpy.array(yInPlot)
506 RAInPlot=numpy.array(RAInPlot)
507 decInPlot=numpy.array(decInPlot)
508
509
510 sizePix=(size/3600.0)/self.wcs.getPixelSizeDeg()
511
512 alreadyGot=False
513 for p in self.plotObjects:
514 if p['tag'] == tag:
515 p['x']=xInPlot
516 p['y']=yInPlot
517 p['RA']=RAInPlot
518 p['dec']=decInPlot
519 p['tag']=tag
520 p['objLabels']=objLabels
521 p['symbol']=symbol
522 p['sizePix']=sizePix
523 p['sizeArcSec']=size
524 p['width']=width
525 p['color']=color
526 p['objLabelSize']=objLabelSize
527 alreadyGot=True
528
529 if alreadyGot == False:
530 self.plotObjects.append({'x': xInPlot, 'y': yInPlot, 'RA': RAInPlot, 'dec': decInPlot,
531 'tag': tag, 'objLabels': labelInPlot, 'symbol': symbol,
532 'sizePix': sizePix, 'width': width, 'color': color,
533 'objLabelSize': objLabelSize, 'sizeArcSec': size})
534 self.draw()
535
536
538 """Removes the plotObjects from the ImagePlot corresponding to the tag. The plot must be redrawn
539 for the change to take effect.
540
541 @type tag: string
542 @param tag: tag for set of objects in ImagePlot.plotObjects to be removed
543
544 """
545
546 index=0
547 for p in self.plotObjects:
548 if p['tag'] == tag:
549 self.plotObjects.remove(self.plotObjects[index])
550 index=index+1
551 self.draw()
552
553
554 - def addCompass(self, location, sizeArcSec, color = "white", fontSize = 12, \
555 width = 20.0):
556 """Adds a compass to the ImagePlot at the given location ('N', 'NE', 'E', 'SE', 'S',
557 'SW', 'W', or 'NW'). Note these aren't directions on the WCS coordinate grid, they are
558 relative positions on the plot - so N is top centre, NE is top right, SW is bottom right etc..
559 Alternatively, pixel coordinates (x, y) in the image can be given.
560
561 @type location: string or tuple
562 @param location: location in the plot where the compass is drawn:
563 - string: N, NE, E, SE, S, SW, W or NW
564 - tuple: (x, y)
565 @type sizeArcSec: float
566 @param sizeArcSec: length of the compass arrows on the plot in arc seconds
567 @type color: string
568 @param color: any valid matplotlib color string
569 @type fontSize: float
570 @param fontSize: size of font used to label N and E, in points
571 @type width: float
572 @param width: width of arrows used to mark compass
573
574 """
575
576
577
578 if type(location) == str:
579 RADeg, decDeg=self.wcs.getCentreWCSCoords()
580 x, y=self.wcs.wcs2pix(RADeg, decDeg)
581 halfWidthRADeg, halfHeightDecDeg=self.wcs.getHalfSizeDeg()
582 compassDistRADeg=halfWidthRADeg-1.5*sizeArcSec/3600.0
583 compassDistDecDeg=halfHeightDecDeg-1.5*sizeArcSec/3600.0
584 if self.wcs.isFlipped() == True:
585 compassDistRADeg=compassDistRADeg*-1
586 foundLocation=False
587 if location.find("N") != -1:
588 y=y+compassDistDecDeg/self.wcs.getPixelSizeDeg()
589 foundLocation=True
590 if location.find("S") != -1:
591 y=y-compassDistDecDeg/self.wcs.getPixelSizeDeg()
592 foundLocation=True
593 if location.find("E") != -1:
594 x=x-compassDistRADeg/self.wcs.getPixelSizeDeg()
595 foundLocation=True
596 if location.find("W") != -1:
597 x=x+compassDistRADeg/self.wcs.getPixelSizeDeg()
598 foundLocation=True
599 if foundLocation == False:
600 raise Exception, "didn't understand location string for compass (should be e.g. N, S, E, W)."
601 elif type(location) == tuple:
602 x, y=location
603 else:
604 raise Exception, "didn't understand location for compass - should be string or tuple."
605 RADeg, decDeg=self.wcs.pix2wcs(x, y)
606
607
608 sizePix=(sizeArcSec/3600.0)/self.wcs.getPixelSizeDeg()
609
610 alreadyGot=False
611 for p in self.plotObjects:
612 if p['tag'] == "compass":
613 p['x']=[x]
614 p['y']=[y]
615 p['RA']=[RADeg]
616 p['dec']=[decDeg]
617 p['tag']="compass"
618 p['objLabels']=[None]
619 p['symbol']="compass"
620 p['sizePix']=sizePix
621 p['sizeArcSec']=sizeArcSec
622 p['width']=width
623 p['color']=color
624 p['objLabelSize']=fontSize
625 alreadyGot=True
626
627 if alreadyGot == False:
628 self.plotObjects.append({'x': [x], 'y': [y], 'RA': [RADeg], 'dec': [decDeg],
629 'tag': "compass", 'objLabels': [None], 'symbol': "compass",
630 'sizePix': sizePix, 'width': width, 'color': color,
631 'objLabelSize': fontSize, 'sizeArcSec': sizeArcSec})
632 self.draw()
633
634
635 - def addScaleBar(self, location, sizeArcSec, color = "white", fontSize = 12, \
636 width = 20.0):
637 """Adds a scale bar to the ImagePlot at the given location ('N', 'NE', 'E', 'SE', 'S',
638 'SW', 'W', or 'NW'). Note these aren't directions on the WCS coordinate grid, they are
639 relative positions on the plot - so N is top centre, NE is top right, SW is bottom right etc..
640 Alternatively, pixel coordinates (x, y) in the image can be given.
641
642 @type location: string or tuple
643 @param location: location in the plot where the compass is drawn:
644 - string: N, NE, E, SE, S, SW, W or NW
645 - tuple: (x, y)
646 @type sizeArcSec: float
647 @param sizeArcSec: scale length to indicate on the plot in arc seconds
648 @type color: string
649 @param color: any valid matplotlib color string
650 @type fontSize: float
651 @param fontSize: size of font used to label N and E, in points
652 @type width: float
653 @param width: width of arrow used to mark scale
654
655 """
656
657
658 if type(location) == str:
659 RADeg, decDeg=self.wcs.getCentreWCSCoords()
660 x, y=self.wcs.wcs2pix(RADeg, decDeg)
661 halfWidthRADeg, halfHeightDecDeg=self.wcs.getHalfSizeDeg()
662 compassDistRADeg=halfWidthRADeg-1.2*sizeArcSec/3600.0
663 compassDistDecDeg=halfHeightDecDeg-0.15*halfHeightDecDeg
664 if self.wcs.isFlipped() == True:
665 compassDistRADeg=compassDistRADeg*-1
666 foundLocation=False
667 if location.find("N") != -1:
668 y=y+compassDistDecDeg/self.wcs.getPixelSizeDeg()
669 foundLocation=True
670 if location.find("S") != -1:
671 y=y-compassDistDecDeg/self.wcs.getPixelSizeDeg()
672 foundLocation=True
673 if location.find("E") != -1:
674 x=x-compassDistRADeg/self.wcs.getPixelSizeDeg()
675 foundLocation=True
676 if location.find("W") != -1:
677 x=x+compassDistRADeg/self.wcs.getPixelSizeDeg()
678 foundLocation=True
679 if foundLocation == False:
680 raise Exception, "didn't understand location string for compass (should be e.g. N, S, E, W)."
681 elif type(location) == tuple:
682 x, y=location
683 else:
684 raise Exception, "didn't understand location for compass - should be string or tuple."
685 RADeg, decDeg=self.wcs.pix2wcs(x, y)
686
687
688 sizePix=(sizeArcSec/3600.0)/self.wcs.getPixelSizeDeg()
689
690 alreadyGot=False
691 for p in self.plotObjects:
692 if p['tag'] == "scaleBar":
693 p['x']=[x]
694 p['y']=[y]
695 p['RA']=[RADeg]
696 p['dec']=[decDeg]
697 p['tag']="scaleBar"
698 p['objLabels']=[None]
699 p['symbol']="scaleBar"
700 p['sizePix']=sizePix
701 p['sizeArcSec']=sizeArcSec
702 p['width']=width
703 p['color']=color
704 p['objLabelSize']=fontSize
705 alreadyGot=True
706
707 if alreadyGot == False:
708 self.plotObjects.append({'x': [x], 'y': [y], 'RA': [RADeg], 'dec': [decDeg],
709 'tag': "scaleBar", 'objLabels': [None], 'symbol': "scaleBar",
710 'sizePix': sizePix, 'width': width, 'color': color,
711 'objLabelSize': fontSize, 'sizeArcSec': sizeArcSec})
712 self.draw()
713
714
716 """
717 This function calculates the positions of coordinate labels for the RA and Dec axes of the
718 ImagePlot. The tick steps are calculated automatically unless self.RATickSteps,
719 self.decTickSteps are set to values other than "auto" (see L{ImagePlot.__init__}).
720
721 The ImagePlot must be redrawn for changes to be applied.
722
723 @type axesLabels: string
724 @param axesLabels: either "sexagesimal" (for H:M:S, D:M:S), "decimal" (for decimal degrees),
725 or None for no coordinate axes labels
726
727 """
728
729
730 equinox=self.wcs.getEquinox()
731 if equinox<1984:
732 equinoxLabel="B"+str(int(equinox))
733 else:
734 equinoxLabel="J"+str(int(equinox))
735
736 self.axesLabels=axesLabels
737
738 ticsDict=self.getTickSteps()
739
740
741 if self.RATickSteps != "auto":
742 ticsDict['major']['RA']=self.RATickSteps
743 if self.decTickSteps != "auto":
744 ticsDict['major']['dec']=self.decTickSteps
745
746 RALocs=[]
747 decLocs=[]
748 RALabels=[]
749 decLabels=[]
750 key="major"
751
752 if self.axesLabels == "sexagesimal":
753 self.RAAxisLabel="R.A. ("+equinoxLabel+")"
754 self.decAxisLabel="Dec. ("+equinoxLabel+")"
755 RADegStep=ticsDict[key]['RA']['deg']
756 decDegStep=ticsDict[key]['dec']['deg']
757 elif self.axesLabels == "decimal":
758 self.RAAxisLabel="R.A. Degrees ("+equinoxLabel+")"
759 self.decAxisLabel="Dec. Degrees ("+equinoxLabel+")"
760 RADegStep=ticsDict[key]['RA']
761 decDegStep=ticsDict[key]['dec']
762 else:
763 raise Exception, "axesLabels must be either 'sexagesimal' or 'decimal'"
764
765 xArray=numpy.arange(0, self.data.shape[1], 1)
766 yArray=numpy.arange(0, self.data.shape[0], 1)
767 xWCS=self.wcs.pix2wcs(xArray, [0]*xArray.shape[0])
768 yWCS=self.wcs.pix2wcs([0]*yArray.shape[0], yArray)
769 xWCS=numpy.array(xWCS)
770 yWCS=numpy.array(yWCS)
771 ras=xWCS[:,0]
772 decs=yWCS[:,1]
773 RAEdges=numpy.array([ras[0], ras[-1]])
774 RAMin=RAEdges.min()
775 RAMax=RAEdges.max()
776 decMin=decs.min()
777 decMax=decs.max()
778
779
780 midRAPix, midDecPix=self.wcs.wcs2pix((RAEdges[1]+RAEdges[0])/2.0, (decMax+decMin)/2.0)
781 if midRAPix < 0 or midRAPix > self.wcs.header['NAXIS1']:
782 wrappedRA=True
783 else:
784 wrappedRA=False
785
786
787 if ras[1] < ras[0]:
788 self.flipXAxis=False
789 ra2x=interpolate.interp1d(ras[::-1], xArray[::-1], kind='linear')
790 else:
791 self.flipXAxis=True
792 ra2x=interpolate.interp1d(ras, xArray, kind='linear')
793 if decs[1] < decs[0]:
794 self.flipYAxis=True
795 dec2y=interpolate.interp1d(decs[::-1], yArray[::-1], kind='linear')
796 else:
797 self.flipYAxis=False
798 dec2y=interpolate.interp1d(decs, yArray, kind='linear')
799
800 if wrappedRA == False:
801 RAPlotMin=RADegStep*math.modf(RAMin/RADegStep)[1]
802 RAPlotMax=RADegStep*math.modf(RAMax/RADegStep)[1]
803 if RAPlotMin < RAMin:
804 RAPlotMin=RAPlotMin+RADegStep
805 if RAPlotMax >= RAMax:
806 RAPlotMax=RAPlotMax-RADegStep
807 RADegs=numpy.arange(RAPlotMin, RAPlotMax+0.0001, RADegStep)
808 else:
809 RAPlotMin=RADegStep*math.modf(RAMin/RADegStep)[1]
810 RAPlotMax=RADegStep*math.modf(RAMax/RADegStep)[1]
811 if RAPlotMin > RAMin:
812 RAPlotMin=RAPlotMin-RADegStep
813 if RAPlotMax <= RAMax:
814 RAPlotMax=RAPlotMax+RADegStep
815 for i in range(ras.shape[0]):
816 if ras[i] >= RAMax and ras[i] <= 360.0:
817 ras[i]=ras[i]-360.0
818 ra2x=interpolate.interp1d(ras[::-1], xArray[::-1], kind='linear')
819 RADegs=numpy.arange(RAPlotMin, RAPlotMax-360.0-0.0001, -RADegStep)
820
821 decPlotMin=decDegStep*math.modf(decMin/decDegStep)[1]
822 decPlotMax=decDegStep*math.modf(decMax/decDegStep)[1]
823 if decPlotMin < decMin:
824 decPlotMin=decPlotMin+decDegStep
825 if decPlotMax >= decMax:
826 decPlotMax=decPlotMax-decDegStep
827 decDegs=numpy.arange(decPlotMin, decPlotMax+0.0001, decDegStep)
828
829 if key == "major":
830 if axesLabels == "sexagesimal":
831 for r in RADegs:
832 if r < 0:
833 r=r+360.0
834 h, m, s=astCoords.decimal2hms(r, ":").split(":")
835 hInt=int(round(float(h)))
836 if hInt < 10:
837 hString="0"+str(hInt)
838 else:
839 hString=str(hInt)
840 mInt=int(round(float(m)))
841 if mInt < 10:
842 mString="0"+str(mInt)
843 else:
844 mString=str(mInt)
845 sInt=int(round(float(s)))
846 if sInt < 10:
847 sString="0"+str(sInt)
848 else:
849 sString=str(sInt)
850 if ticsDict[key]['RA']['unit'] == 'h':
851 rString=hString+"$^h$"
852 elif ticsDict[key]['RA']['unit'] == 'm':
853 rString=hString+"$^h$"+mString+"$^m$"
854 else:
855 rString=hString+"$^h$"+mString+"$^m$"+sString+"$^s$"
856 RALabels.append(rString)
857 for D in decDegs:
858 d, m, s=astCoords.decimal2dms(D, ":").split(":")
859 dInt=int(round(float(d)))
860 if dInt < 10 and dInt >= 1 :
861 dString="+0"+str(dInt)
862 elif dInt > -10 and dInt <=-1:
863 dString="-0"+str(abs(dInt))
864 elif dInt >= 10:
865 dString="+"+str(dInt)
866 else:
867 dString=str(dInt)
868 mInt=int(round(float(m)))
869 if mInt < 10:
870 mString="0"+str(mInt)
871 else:
872 mString=str(mInt)
873 sInt=int(round(float(s)))
874 if sInt < 10:
875 sString="0"+str(sInt)
876 else:
877 sString=str(sInt)
878 if ticsDict[key]['dec']['unit'] == 'd':
879 dString=dString+DEG
880 elif ticsDict[key]['dec']['unit'] == 'm':
881 dString=dString+DEG+mString+PRIME
882 else:
883 dString=dString+DEG+mString+PRIME+sString+DOUBLE_PRIME
884 decLabels.append(dString)
885 elif axesLabels == "decimal":
886 if wrappedRA == False:
887 RALabels=RALabels+RADegs.tolist()
888 else:
889 nonNegativeLabels=[]
890 for r in RADegs:
891 if r < 0:
892 r=r+360.0
893 nonNegativeLabels.append(r)
894 RALabels=RALabels+nonNegativeLabels
895 decLabels=decLabels+decDegs.tolist()
896
897
898 dps=[]
899 for r in RALabels:
900 dps.append(len(str(r).split(".")[-1]))
901 dpNumRA=int(math.ceil(numpy.array(dps).mean()))
902 for i in range(len(RALabels)):
903 fString="%."+str(dpNumRA)+"f"
904 RALabels[i]=fString % (RALabels[i])
905 dps=[]
906 for d in decLabels:
907 dps.append(len(str(d).split(".")[-1]))
908 dpNumDec=int(math.ceil(numpy.array(dps).mean()))
909 for i in range(len(decLabels)):
910 fString="%."+str(dpNumDec)+"f"
911 decLabels[i]=fString % (decLabels[i])
912
913 if key == 'minor':
914 RALabels=RALabels+RADegs.shape[0]*['']
915 decLabels=decLabels+decDegs.shape[0]*['']
916
917 RALocs=RALocs+ra2x(RADegs).tolist()
918 decLocs=decLocs+dec2y(decDegs).tolist()
919
920 self.ticsRA=[RALocs, RALabels]
921 self.ticsDec=[decLocs, decLabels]
922
923
924 - def save(self, fileName):
925 """Saves the ImagePlot in any format that matplotlib can understand, as determined from the
926 fileName extension.
927
928 @type fileName: string
929 @param fileName: path where plot will be written
930
931 """
932
933 pylab.draw()
934 pylab.savefig(fileName)
935
936
938 """Chooses the appropriate WCS coordinate tick steps for the plot based on its size.
939 Whether the ticks are decimal or sexagesimal is set by self.axesLabels.
940
941 Note: minor ticks not used at the moment.
942
943 @rtype: dictionary
944 @return: tick step sizes for major, minor plot ticks, in format {'major', 'minor'}
945
946 """
947
948
949 xArray=numpy.arange(0, self.data.shape[1], 1)
950 yArray=numpy.arange(0, self.data.shape[0], 1)
951 xWCS=self.wcs.pix2wcs(xArray, [0]*xArray.shape[0])
952 yWCS=self.wcs.pix2wcs([0]*yArray.shape[0], yArray)
953 xWCS=numpy.array(xWCS)
954 yWCS=numpy.array(yWCS)
955 ras=xWCS[:,0]
956 decs=yWCS[:,1]
957 RAEdges=numpy.array([ras[0], ras[-1]])
958 RAMin=RAEdges.min()
959 RAMax=RAEdges.max()
960 decMin=decs.min()
961 decMax=decs.max()
962
963
964 midRAPix, midDecPix=self.wcs.wcs2pix((RAEdges[1]+RAEdges[0])/2.0, (decMax+decMin)/2.0)
965 if midRAPix < 0 or midRAPix > self.wcs.header['NAXIS1']:
966 wrappedRA=True
967 else:
968 wrappedRA=False
969 if wrappedRA == False:
970 RAWidthDeg=RAMax-RAMin
971 else:
972 RAWidthDeg=(360.0-RAMax)+RAMin
973 decHeightDeg=decMax-decMin
974
975 ticsDict={}
976 ticsDict['major']={}
977 ticsDict['minor']={}
978 if self.axesLabels == "sexagesimal":
979
980 matchIndex = 0
981 for i in range(len(RA_TICK_STEPS)):
982 if RAWidthDeg/2.5 > RA_TICK_STEPS[i]['deg']:
983 matchIndex = i
984
985 ticsDict['major']['RA']=RA_TICK_STEPS[matchIndex]
986 ticsDict['minor']['RA']=RA_TICK_STEPS[matchIndex-1]
987
988 matchIndex = 0
989 for i in range(len(DEC_TICK_STEPS)):
990 if decHeightDeg/2.5 > DEC_TICK_STEPS[i]['deg']:
991 matchIndex = i
992
993 ticsDict['major']['dec']=DEC_TICK_STEPS[matchIndex]
994 ticsDict['minor']['dec']=DEC_TICK_STEPS[matchIndex-1]
995
996 return ticsDict
997
998 elif self.axesLabels == "decimal":
999
1000 matchIndex = 0
1001 for i in range(len(DECIMAL_TICK_STEPS)):
1002 if RAWidthDeg/2.5 > DECIMAL_TICK_STEPS[i]:
1003 matchIndex = i
1004
1005 ticsDict['major']['RA']=DECIMAL_TICK_STEPS[matchIndex]
1006 ticsDict['minor']['RA']=DECIMAL_TICK_STEPS[matchIndex-1]
1007
1008 matchIndex = 0
1009 for i in range(len(DECIMAL_TICK_STEPS)):
1010 if decHeightDeg/2.5 > DECIMAL_TICK_STEPS[i]:
1011 matchIndex = i
1012
1013 ticsDict['major']['dec']=DECIMAL_TICK_STEPS[matchIndex]
1014 ticsDict['minor']['dec']=DECIMAL_TICK_STEPS[matchIndex-1]
1015
1016 return ticsDict
1017
1018 else:
1019 raise Exception, "axesLabels must be either 'sexagesimal' or 'decimal'"
1020