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