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