1
2 """module for producing astronomical plots
3
4 (c) 2007-2011 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
372 L{removeContourOverlay}. If a contour overlay already exists with this tag, it will be replaced.
373
374 @type contourImageData: numpy array
375 @param contourImageData: image data array from which contours are to be generated
376 @type contourWCS: astWCS.WCS
377 @param contourWCS: astWCS.WCS object for the image to be contoured
378 @type tag: string
379 @param tag: identifying tag for this set of contours
380 @type levels: list
381 @param levels: sets the contour levels - available options:
382 - values: contourLevels=[list of values specifying each level]
383 - linear spacing: contourLevels=['linear', min level value, max level value, number
384 of levels] - can use "min", "max" to automatically set min, max levels from image data
385 - log spacing: contourLevels=['log', min level value, max level value, number of
386 levels] - can use "min", "max" to automatically set min, max levels from image data
387 @type width: int
388 @param width: width of the overlaid contours
389 @type color: string
390 @param color: color of the overlaid contours, specified by the name of a standard
391 matplotlib color, e.g., "black", "white", "cyan"
392 etc. (do "help(pylab.colors)" in the Python interpreter to see available options)
393 @type smooth: float
394 @param smooth: standard deviation (in arcsec) of Gaussian filter for
395 pre-smoothing of contour image data (set to 0 for no smoothing)
396 @type highAccuracy: bool
397 @param highAccuracy: if True, sample every corresponding pixel in each image; otherwise, sample
398 every nth pixel, where n = the ratio of the image scales.
399
400 """
401
402 if self.rgbImage == True:
403 backgroundData=self.data[:,:,0]
404 else:
405 backgroundData=self.data
406 contourData=astImages.generateContourOverlay(backgroundData, self.wcs, contourImageData, \
407 contourWCS, levels, smooth, highAccuracy = highAccuracy)
408
409 alreadyGot=False
410 for c in self.contourOverlays:
411 if c['tag'] == tag:
412 c['contourData']=contourData
413 c['tag']=tag
414 c['color']=color
415 c['width']=width
416 alreadyGot=True
417
418 if alreadyGot == False:
419 self.contourOverlays.append({'contourData': contourData, 'tag': tag, 'color': color, \
420 'width': width})
421 self.draw()
422
423
425 """Removes the contourOverlay from the ImagePlot corresponding to the tag.
426
427 @type tag: string
428 @param tag: tag for contour overlay in ImagePlot.contourOverlays to be removed
429
430 """
431
432 index=0
433 for p in self.contourOverlays:
434 if p['tag'] == tag:
435 self.plotObjects.remove(self.plotObjects[index])
436 index=index+1
437 self.draw()
438
439
440 - def addPlotObjects(self, objRAs, objDecs, tag, symbol="circle", size=4.0, width=1.0, color="yellow",
441 objLabels = None, objLabelSize = 12.0):
442 """Add objects with RA, dec coords objRAs, objDecs to the ImagePlot. Only objects that fall within
443 the image boundaries will be plotted.
444
445 symbol specifies the type of symbol with which to mark the object in the image. The following
446 values are allowed:
447 - "circle"
448 - "box"
449 - "cross"
450 - "diamond"
451
452 size specifies the diameter in arcsec of the symbol (if plotSymbol == "circle"), or the width
453 of the box in arcsec (if plotSymbol == "box")
454
455 width specifies the thickness of the symbol lines in pixels
456
457 color can be any valid matplotlib color (e.g. "red", "green", etc.)
458
459 The objects can be removed from the plot by using removePlotObjects(), and then calling
460 draw(). If the ImagePlot already has a set of plotObjects with the same tag, they will be
461 replaced.
462
463 @type objRAs: numpy array or list
464 @param objRAs: object RA coords in decimal degrees
465 @type objDecs: numpy array or list
466 @param objDecs: corresponding object Dec. coords in decimal degrees
467 @type tag: string
468 @param tag: identifying tag for this set of objects
469 @type symbol: string
470 @param symbol: either "circle", "box", "cross", or "diamond"
471 @type size: float
472 @param size: size of symbols to plot (radius in arcsec, or width of box)
473 @type width: float
474 @param width: width of symbols in pixels
475 @type color: string
476 @param color: any valid matplotlib color string, e.g. "red", "green" etc.
477 @type objLabels: list
478 @param objLabels: text labels to plot next to objects in figure
479 @type objLabelSize: float
480 @param objLabelSize: size of font used for object labels (in points)
481
482 """
483
484 pixCoords=self.wcs.wcs2pix(objRAs, objDecs)
485
486 xMax=self.data.shape[1]
487 yMax=self.data.shape[0]
488
489 if objLabels == None:
490 objLabels=[None]*len(objRAs)
491
492 xInPlot=[]
493 yInPlot=[]
494 RAInPlot=[]
495 decInPlot=[]
496 labelInPlot=[]
497 for p, r, d, l in zip(pixCoords, objRAs, objDecs, objLabels):
498 if p[0] >= 0 and p[0] < xMax and p[1] >= 0 and p[1] < yMax:
499 xInPlot.append(p[0])
500 yInPlot.append(p[1])
501 RAInPlot.append(r)
502 decInPlot.append(d)
503 labelInPlot.append(l)
504
505 xInPlot=numpy.array(xInPlot)
506 yInPlot=numpy.array(yInPlot)
507 RAInPlot=numpy.array(RAInPlot)
508 decInPlot=numpy.array(decInPlot)
509
510
511 sizePix=(size/3600.0)/self.wcs.getPixelSizeDeg()
512
513 alreadyGot=False
514 for p in self.plotObjects:
515 if p['tag'] == tag:
516 p['x']=xInPlot
517 p['y']=yInPlot
518 p['RA']=RAInPlot
519 p['dec']=decInPlot
520 p['tag']=tag
521 p['objLabels']=objLabels
522 p['symbol']=symbol
523 p['sizePix']=sizePix
524 p['sizeArcSec']=size
525 p['width']=width
526 p['color']=color
527 p['objLabelSize']=objLabelSize
528 alreadyGot=True
529
530 if alreadyGot == False:
531 self.plotObjects.append({'x': xInPlot, 'y': yInPlot, 'RA': RAInPlot, 'dec': decInPlot,
532 'tag': tag, 'objLabels': labelInPlot, 'symbol': symbol,
533 'sizePix': sizePix, 'width': width, 'color': color,
534 'objLabelSize': objLabelSize, 'sizeArcSec': size})
535 self.draw()
536
537
539 """Removes the plotObjects from the ImagePlot corresponding to the tag. The plot must be redrawn
540 for the change to take effect.
541
542 @type tag: string
543 @param tag: tag for set of objects in ImagePlot.plotObjects to be removed
544
545 """
546
547 index=0
548 for p in self.plotObjects:
549 if p['tag'] == tag:
550 self.plotObjects.remove(self.plotObjects[index])
551 index=index+1
552 self.draw()
553
554
555 - def addCompass(self, location, sizeArcSec, color = "white", fontSize = 12, \
556 width = 20.0):
557 """Adds a compass to the ImagePlot at the given location ('N', 'NE', 'E', 'SE', 'S',
558 'SW', 'W', or 'NW'). Note these aren't directions on the WCS coordinate grid, they are
559 relative positions on the plot - so N is top centre, NE is top right, SW is bottom right etc..
560 Alternatively, pixel coordinates (x, y) in the image can be given.
561
562 @type location: string or tuple
563 @param location: location in the plot where the compass is drawn:
564 - string: N, NE, E, SE, S, SW, W or NW
565 - tuple: (x, y)
566 @type sizeArcSec: float
567 @param sizeArcSec: length of the compass arrows on the plot in arc seconds
568 @type color: string
569 @param color: any valid matplotlib color string
570 @type fontSize: float
571 @param fontSize: size of font used to label N and E, in points
572 @type width: float
573 @param width: width of arrows used to mark compass
574
575 """
576
577
578
579 if type(location) == str:
580 RADeg, decDeg=self.wcs.getCentreWCSCoords()
581 x, y=self.wcs.wcs2pix(RADeg, decDeg)
582 halfWidthRADeg, halfHeightDecDeg=self.wcs.getHalfSizeDeg()
583 compassDistRADeg=halfWidthRADeg-1.5*sizeArcSec/3600.0
584 compassDistDecDeg=halfHeightDecDeg-1.5*sizeArcSec/3600.0
585 if self.wcs.isFlipped() == True:
586 compassDistRADeg=compassDistRADeg*-1
587 foundLocation=False
588 if location.find("N") != -1:
589 y=y+compassDistDecDeg/self.wcs.getPixelSizeDeg()
590 foundLocation=True
591 if location.find("S") != -1:
592 y=y-compassDistDecDeg/self.wcs.getPixelSizeDeg()
593 foundLocation=True
594 if location.find("E") != -1:
595 x=x-compassDistRADeg/self.wcs.getPixelSizeDeg()
596 foundLocation=True
597 if location.find("W") != -1:
598 x=x+compassDistRADeg/self.wcs.getPixelSizeDeg()
599 foundLocation=True
600 if foundLocation == False:
601 raise Exception, "didn't understand location string for compass (should be e.g. N, S, E, W)."
602 elif type(location) == tuple:
603 x, y=location
604 else:
605 raise Exception, "didn't understand location for compass - should be string or tuple."
606 RADeg, decDeg=self.wcs.pix2wcs(x, y)
607
608
609 sizePix=(sizeArcSec/3600.0)/self.wcs.getPixelSizeDeg()
610
611 alreadyGot=False
612 for p in self.plotObjects:
613 if p['tag'] == "compass":
614 p['x']=[x]
615 p['y']=[y]
616 p['RA']=[RADeg]
617 p['dec']=[decDeg]
618 p['tag']="compass"
619 p['objLabels']=[None]
620 p['symbol']="compass"
621 p['sizePix']=sizePix
622 p['sizeArcSec']=sizeArcSec
623 p['width']=width
624 p['color']=color
625 p['objLabelSize']=fontSize
626 alreadyGot=True
627
628 if alreadyGot == False:
629 self.plotObjects.append({'x': [x], 'y': [y], 'RA': [RADeg], 'dec': [decDeg],
630 'tag': "compass", 'objLabels': [None], 'symbol': "compass",
631 'sizePix': sizePix, 'width': width, 'color': color,
632 'objLabelSize': fontSize, 'sizeArcSec': sizeArcSec})
633 self.draw()
634
635
636 - def addScaleBar(self, location, sizeArcSec, color = "white", fontSize = 12, \
637 width = 20.0):
638 """Adds a scale bar to the ImagePlot at the given location ('N', 'NE', 'E', 'SE', 'S',
639 'SW', 'W', or 'NW'). Note these aren't directions on the WCS coordinate grid, they are
640 relative positions on the plot - so N is top centre, NE is top right, SW is bottom right etc..
641 Alternatively, pixel coordinates (x, y) in the image can be given.
642
643 @type location: string or tuple
644 @param location: location in the plot where the compass is drawn:
645 - string: N, NE, E, SE, S, SW, W or NW
646 - tuple: (x, y)
647 @type sizeArcSec: float
648 @param sizeArcSec: scale length to indicate on the plot in arc seconds
649 @type color: string
650 @param color: any valid matplotlib color string
651 @type fontSize: float
652 @param fontSize: size of font used to label N and E, in points
653 @type width: float
654 @param width: width of arrow used to mark scale
655
656 """
657
658
659 if type(location) == str:
660 RADeg, decDeg=self.wcs.getCentreWCSCoords()
661 x, y=self.wcs.wcs2pix(RADeg, decDeg)
662 halfWidthRADeg, halfHeightDecDeg=self.wcs.getHalfSizeDeg()
663 compassDistRADeg=halfWidthRADeg-1.2*sizeArcSec/3600.0
664 compassDistDecDeg=halfHeightDecDeg-0.15*halfHeightDecDeg
665 if self.wcs.isFlipped() == True:
666 compassDistRADeg=compassDistRADeg*-1
667 foundLocation=False
668 if location.find("N") != -1:
669 y=y+compassDistDecDeg/self.wcs.getPixelSizeDeg()
670 foundLocation=True
671 if location.find("S") != -1:
672 y=y-compassDistDecDeg/self.wcs.getPixelSizeDeg()
673 foundLocation=True
674 if location.find("E") != -1:
675 x=x-compassDistRADeg/self.wcs.getPixelSizeDeg()
676 foundLocation=True
677 if location.find("W") != -1:
678 x=x+compassDistRADeg/self.wcs.getPixelSizeDeg()
679 foundLocation=True
680 if foundLocation == False:
681 raise Exception, "didn't understand location string for compass (should be e.g. N, S, E, W)."
682 elif type(location) == tuple:
683 x, y=location
684 else:
685 raise Exception, "didn't understand location for compass - should be string or tuple."
686 RADeg, decDeg=self.wcs.pix2wcs(x, y)
687
688
689 sizePix=(sizeArcSec/3600.0)/self.wcs.getPixelSizeDeg()
690
691 alreadyGot=False
692 for p in self.plotObjects:
693 if p['tag'] == "scaleBar":
694 p['x']=[x]
695 p['y']=[y]
696 p['RA']=[RADeg]
697 p['dec']=[decDeg]
698 p['tag']="scaleBar"
699 p['objLabels']=[None]
700 p['symbol']="scaleBar"
701 p['sizePix']=sizePix
702 p['sizeArcSec']=sizeArcSec
703 p['width']=width
704 p['color']=color
705 p['objLabelSize']=fontSize
706 alreadyGot=True
707
708 if alreadyGot == False:
709 self.plotObjects.append({'x': [x], 'y': [y], 'RA': [RADeg], 'dec': [decDeg],
710 'tag': "scaleBar", 'objLabels': [None], 'symbol': "scaleBar",
711 'sizePix': sizePix, 'width': width, 'color': color,
712 'objLabelSize': fontSize, 'sizeArcSec': sizeArcSec})
713 self.draw()
714
715
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, numpy.zeros(xArray.shape[0], dtype=float))
768 yWCS=self.wcs.pix2wcs(numpy.zeros(yArray.shape[0], dtype=float), 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 if ras[1] < ras[0]:
819 ra2x=interpolate.interp1d(ras[::-1], xArray[::-1], kind='linear')
820 else:
821 ra2x=interpolate.interp1d(ras, xArray, kind='linear')
822 RADegs=numpy.arange(RAPlotMin, RAPlotMax-360.0-0.0001, -RADegStep)
823
824 decPlotMin=decDegStep*math.modf(decMin/decDegStep)[1]
825 decPlotMax=decDegStep*math.modf(decMax/decDegStep)[1]
826 if decPlotMin < decMin:
827 decPlotMin=decPlotMin+decDegStep
828 if decPlotMax >= decMax:
829 decPlotMax=decPlotMax-decDegStep
830 decDegs=numpy.arange(decPlotMin, decPlotMax+0.0001, decDegStep)
831
832 if key == "major":
833 if axesLabels == "sexagesimal":
834 for r in RADegs:
835 if r < 0:
836 r=r+360.0
837 h, m, s=astCoords.decimal2hms(r, ":").split(":")
838 hInt=int(round(float(h)))
839 if ticsDict[key]['RA']['unit'] == 'h' and (60.0-float(m)) < 0.01:
840 hInt=hInt+1
841 if hInt < 10:
842 hString="0"+str(hInt)
843 else:
844 hString=str(hInt)
845 mInt=int(round(float(m)))
846 if ticsDict[key]['RA']['unit'] == 'm' and (60.0-float(s)) < 0.01:
847 mInt=mInt+1
848 if mInt < 10:
849 mString="0"+str(mInt)
850 else:
851 mString=str(mInt)
852 sInt=int(round(float(s)))
853 if sInt < 10:
854 sString="0"+str(sInt)
855 else:
856 sString=str(sInt)
857 if ticsDict[key]['RA']['unit'] == 'h':
858 rString=hString+"$^h$"
859 elif ticsDict[key]['RA']['unit'] == 'm':
860 rString=hString+"$^h$"+mString+"$^m$"
861 else:
862 rString=hString+"$^h$"+mString+"$^m$"+sString+"$^s$"
863 RALabels.append(rString)
864 for D in decDegs:
865 d, m, s=astCoords.decimal2dms(D, ":").split(":")
866 dInt=int(round(float(d)))
867 if ticsDict[key]['dec']['unit'] == 'd' and (60.0-float(m)) < 0.01:
868 dInt=dInt+1
869 if dInt < 10 and dInt >= 0 and D > 0:
870 dString="+0"+str(dInt)
871 elif dInt > -10 and dInt <= 0 and D < 0:
872 dString="-0"+str(abs(dInt))
873 elif dInt >= 10:
874 dString="+"+str(dInt)
875 else:
876 dString=str(dInt)
877 mInt=int(round(float(m)))
878 if ticsDict[key]['dec']['unit'] == 'm' and (60.0-float(s)) < 0.01:
879 mInt=mInt+1
880 if mInt < 10:
881 mString="0"+str(mInt)
882 else:
883 mString=str(mInt)
884 sInt=int(round(float(s)))
885 if sInt < 10:
886 sString="0"+str(sInt)
887 else:
888 sString=str(sInt)
889 if ticsDict[key]['dec']['unit'] == 'd':
890 dString=dString+DEG
891 elif ticsDict[key]['dec']['unit'] == 'm':
892 dString=dString+DEG+mString+PRIME
893 else:
894 dString=dString+DEG+mString+PRIME+sString+DOUBLE_PRIME
895 decLabels.append(dString)
896 elif axesLabels == "decimal":
897 if wrappedRA == False:
898 RALabels=RALabels+RADegs.tolist()
899 else:
900 nonNegativeLabels=[]
901 for r in RADegs:
902 if r < 0:
903 r=r+360.0
904 nonNegativeLabels.append(r)
905 RALabels=RALabels+nonNegativeLabels
906 decLabels=decLabels+decDegs.tolist()
907
908
909 dps=[]
910 for r in RALabels:
911 dps.append(len(str(r).split(".")[-1]))
912 dpNumRA=int(math.ceil(numpy.array(dps).mean()))
913 for i in range(len(RALabels)):
914 fString="%."+str(dpNumRA)+"f"
915 RALabels[i]=fString % (RALabels[i])
916 dps=[]
917 for d in decLabels:
918 dps.append(len(str(d).split(".")[-1]))
919 dpNumDec=int(math.ceil(numpy.array(dps).mean()))
920 for i in range(len(decLabels)):
921 fString="%."+str(dpNumDec)+"f"
922 decLabels[i]=fString % (decLabels[i])
923
924 if key == 'minor':
925 RALabels=RALabels+RADegs.shape[0]*['']
926 decLabels=decLabels+decDegs.shape[0]*['']
927
928 RALocs=RALocs+ra2x(RADegs).tolist()
929 decLocs=decLocs+dec2y(decDegs).tolist()
930
931 self.ticsRA=[RALocs, RALabels]
932 self.ticsDec=[decLocs, decLabels]
933
934
935 - def save(self, fileName):
936 """Saves the ImagePlot in any format that matplotlib can understand, as determined from the
937 fileName extension.
938
939 @type fileName: string
940 @param fileName: path where plot will be written
941
942 """
943
944 pylab.draw()
945 pylab.savefig(fileName)
946
947
949 """Chooses the appropriate WCS coordinate tick steps for the plot based on its size.
950 Whether the ticks are decimal or sexagesimal is set by self.axesLabels.
951
952 Note: minor ticks not used at the moment.
953
954 @rtype: dictionary
955 @return: tick step sizes for major, minor plot ticks, in format {'major', 'minor'}
956
957 """
958
959
960 xArray=numpy.arange(0, self.data.shape[1], 1)
961 yArray=numpy.arange(0, self.data.shape[0], 1)
962 xWCS=self.wcs.pix2wcs(xArray, numpy.zeros(xArray.shape[0], dtype=float))
963 yWCS=self.wcs.pix2wcs(numpy.zeros(yArray.shape[0], dtype=float), yArray)
964 xWCS=numpy.array(xWCS)
965 yWCS=numpy.array(yWCS)
966 ras=xWCS[:,0]
967 decs=yWCS[:,1]
968 RAEdges=numpy.array([ras[0], ras[-1]])
969 RAMin=RAEdges.min()
970 RAMax=RAEdges.max()
971 decMin=decs.min()
972 decMax=decs.max()
973
974
975 midRAPix, midDecPix=self.wcs.wcs2pix((RAEdges[1]+RAEdges[0])/2.0, (decMax+decMin)/2.0)
976 if midRAPix < 0 or midRAPix > self.wcs.header['NAXIS1']:
977 wrappedRA=True
978 else:
979 wrappedRA=False
980 if wrappedRA == False:
981 RAWidthDeg=RAMax-RAMin
982 else:
983 RAWidthDeg=(360.0-RAMax)+RAMin
984 decHeightDeg=decMax-decMin
985
986 ticsDict={}
987 ticsDict['major']={}
988 ticsDict['minor']={}
989 if self.axesLabels == "sexagesimal":
990
991 matchIndex = 0
992 for i in range(len(RA_TICK_STEPS)):
993 if RAWidthDeg/2.5 > RA_TICK_STEPS[i]['deg']:
994 matchIndex = i
995
996 ticsDict['major']['RA']=RA_TICK_STEPS[matchIndex]
997 ticsDict['minor']['RA']=RA_TICK_STEPS[matchIndex-1]
998
999 matchIndex = 0
1000 for i in range(len(DEC_TICK_STEPS)):
1001 if decHeightDeg/2.5 > DEC_TICK_STEPS[i]['deg']:
1002 matchIndex = i
1003
1004 ticsDict['major']['dec']=DEC_TICK_STEPS[matchIndex]
1005 ticsDict['minor']['dec']=DEC_TICK_STEPS[matchIndex-1]
1006
1007 return ticsDict
1008
1009 elif self.axesLabels == "decimal":
1010
1011 matchIndex = 0
1012 for i in range(len(DECIMAL_TICK_STEPS)):
1013 if RAWidthDeg/2.5 > DECIMAL_TICK_STEPS[i]:
1014 matchIndex = i
1015
1016 ticsDict['major']['RA']=DECIMAL_TICK_STEPS[matchIndex]
1017 ticsDict['minor']['RA']=DECIMAL_TICK_STEPS[matchIndex-1]
1018
1019 matchIndex = 0
1020 for i in range(len(DECIMAL_TICK_STEPS)):
1021 if decHeightDeg/2.5 > DECIMAL_TICK_STEPS[i]:
1022 matchIndex = i
1023
1024 ticsDict['major']['dec']=DECIMAL_TICK_STEPS[matchIndex]
1025 ticsDict['minor']['dec']=DECIMAL_TICK_STEPS[matchIndex-1]
1026
1027 return ticsDict
1028
1029 else:
1030 raise Exception, "axesLabels must be either 'sexagesimal' or 'decimal'"
1031