Source code for kapteyn.maputils

#!/usr/bin/env python
#----------------------------------------------------------------------
# FILE:    maputils.py
# PURPOSE: Provide methods to extract 2-dim image data from FITS file
#          if dimension of data set is 2 or higher.
#          A WCS map class allows a user to add wcs labels and 
#          wcs graticules.
# AUTHOR:  M.G.R. Vogelaar, University of Groningen, The Netherlands
# DATE:    April 17, 2008
# UPDATE:  May 17, 2008
#          June 30,      2009; Docstrings converted to Sphinx format
#          August 19,    2013; version 1.12. support for Visions interactive
#                              image display.
#          September 07, 2013; version 1.14. Contours in panels and panel
#                              widths now proportional to number of channels
#          February 27,  2014; version 1.15. Improved function that writes contour
#                              information to file. Also enhanced routines for 
#                              pan and zoom events.
#          March 17,     2014; Version 1.16. Added extra triggers for callbacks.
#                              Documented all triggers
#          April 27,     2014; Version 1.17. Make indices for boxdat[] integer to avoid 'deprecated' warnings
#          May 01,       2014; Version 1.18. Changed position of set_coordinate_mode(). Was called too late
#                              which resulted in crashes while deleting tabs in visions.
#          Dec 11,       2014; Version 1.19. Small changes. Leftovers from experiments with 
#                              display pixels can be re-used for next version.
#          Dec 14,       2014; Version 1.20. More DeprecationWarning-s removed. These were revealed by 
#                              NumPy 1.9. All == None and != None replaced by is and is not.
#          Aug 03,       2019; v1.30. Major migration to Python 3. Adjustments for Matplotlib 3. (3.1.1)
#                              and improvements for tick labeling (direction and pad) and other
#                              improvements. Uses axes divider to keep height/width of color bar consistent
#                              with height/weight other Axes objects after resizing.
#          Nov 22,       2019; Cross-hair cursor extends to panels and added shift arrow
#                              to move cursor 1 data pixel.
#          Jun 07,       2021; Added remove() method to graphical objects line beam, skypolygon etc.
#                              Added pixel option for skypolygon.
#          Jun 28,       2022; Updated ffmpeg parameters. Removed bitrate. Added frame rate for input, separated
#                              from frame rate of output video. Solved problem with splitscreen(). Data needed
#                              to be set with setter. Added remove function for Pixellabels object
#
#__version__ = 'x.xx'  Do not define it here because the first line in this
# program must be a Sphinx documentation string!
#
# (C) University of Groningen
# Kapteyn Astronomical Institute
# Groningen, The Netherlands
# E: gipsy@astro.rug.nl
#
# TODO:
# De buttons voor home, prev, next op de navigation toolbar werken wel
# goed in visions, maar niet in een los script met een show()
# Bij zoomacties (bv via pan button) crasht het systeem als er 
# te ver wordt uitgezoomd. De foutmelding is dat er in mathtext geen $$
# staat. 
# -Positionmessage offsets laten laten tonen.
# -Betere melding als header niet valide is
# -Rulers met 1 (start)punt
# -Blur factors aanpassen aan pixelaspectratio
# -Insertspatial kan teveel invoegen als bv een classic header wordt
#  nagestreefd en de oude header nog een CD element heeft voor niet
#  spatiele assen.
# -Classicheader taak moet ook kleinere box in 3e richting kunnen maken
# -Bij FREQ RA subset en invoer van {} 1.4... {} 178... geen foutmelding
#  en geen grids.
# -Colorbar met optie bar tegen plot aan (can use pad=0 parameter)
# -WCSflux geschikt maken voor subplots
# -Truc voor alignment. va=bottom en een delta introduceren
#----------------------------------------------------------------------

"""
.. highlight:: python
   :linenothreshold: 1000

Module maputils 
===============

In the maputils tutorial we show many examples with Python code and 
figures to illustrate the functionality and flexibility of this module.
The documentation below is restricted to the module's classes and methods.

Introduction
------------

One of the goals of the Kapteyn Package is to provide a user/programmer basic
tools to make plots (with WCS annotation) of image data from FITS files.
These tools are based on the functionality of PyFITS and Matplotlib.
The methods from these packages are modified in *maputils* for an optimal
support of inspection and presentation of astronomical image data with
easy to write and usually very short Python scripts. To illustrate
what can be done with this module, we list some steps you need
in the process to create a hard copy of an image from a FITS file:

* Open FITS file on disk or from a remote location (URL)
* Specify in which header data unit the image data is stored
* Specify the data slice for data sets with dimensions > 2
* Specify the order of the image axes
* Set the limits in pixels of both image axes
* Set the sky system in which you want to plot wcs information.

Then for the display:

* Plot the image or a mosaic of images in the correct aspect ratio
* Plot (labeled) contours
* Plot world coordinate labels along the image axes  (basic routines in :mod:`kapteyn.wcsgrat`)
* Plot coordinate graticules (basic routines in :mod:`kapteyn.wcsgrat`)
* Interactively change color map and color limits
* Read the position of features in a map and write these positions in your terminal.
* Resize your plot canvas to get an optimal layout while preserving the aspect ratio.
* Write the result to *png* or *pdf* (or another format from a list)

Of course there are many programs that can do this job some way or the other.
But most probably no program does it exactly the way you want or the program
does too much. Also many applications cannot be extended, at least not as simple
as with the building blocks in this `maputils` module.

Module `maputils` is also very useful as a tool to extract and plot
data slices from data sets with more than two axes. For example it can plot
so called *Position-Velocity* maps from a radio interferometer data cube
with channel maps. It can annotate these plots with the correct WCS annotation using
information about the 'missing' spatial axis.

To facilitate the input of the correct data to open a FITS image,
to specify the right data slice or to set the pixel limits for the
image axes, we implemented also some helper functions.
These functions are primitive (terminal based) but effective. You can
replace them by enhanced versions, perhaps with a graphical user interface.

Here is an example of what you can expect. We have a three dimensional data set
on disk called *ngc6946.fits* with axes RA, DEC and VELO.
The program prompts the user to enter
image properties like data limits, axes and axes order.
The image below is a data slice in RA, DEC at VELO=50.
We changed interactively the color map (keys *page-up/page-down*)
and the color limits (pressing right mouse button while moving the mouse) and saved
a hard copy on disk.

In the next code we use keyword parameter *promptfie* a number of times.
Abbreviation 'fie' stands for *Function Interactive Environment*.

.. literalinclude:: EXAMPLES/mu_introduction.py
   
.. image:: EXAMPLES/mu_introduction.png
   :width: 700
   :align: center
   
   
.. centered:: Image from FITS file with graticules and WCS labels

Module level data
-----------------

:data:`cmlist`
   Object from class Colmaplist which has attribute *colormaps* which
   is a sorted list with names of colormaps. It has attributes:

      * **colormaps** -- List with names of color lookup tables
      
      * **cmap_default** -- Color map set by default to 'jet'

   :Example:

      >>> from kapteyn import maputils
      >>> print(maputils.cmlist.colormaps)
      >>> cmap = raw_input("Enter name of a colormap: ")
      ['Accent', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'Dark2',
      'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn',
      'Paired', 'Pastel1', 'Pastel2', 'PiYG', 'PuBu', 'PuBuGn',
      'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu',
      'RdYlBu', 'RdYlGn', 'Reds', 'Set1', 'Set2', 'Set3',
      'Spectral', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd',
      'autumn', 'binary', 'bone', 'cool', 'copper', 'flag',
      'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar',
      'gist_rainbow', 'gist_stern', 'gist_yarg', 'gray',
      'hot', 'hsv', 'jet', 'pink', 'prism', 'spectral',
      'spring', 'summer', 'winter']

Prompt functions
----------------

.. index:: Open a FITS file
.. autofunction:: prompt_fitsfile
.. index:: Set axis numbers of FITS image
.. autofunction:: prompt_imageaxes
.. index:: Set pixel limits in FITS image
.. autofunction:: prompt_box
.. index:: Set spectral translation from list
.. autofunction:: prompt_spectrans
.. index:: Set sky system for output
.. autofunction:: prompt_skyout
.. index:: Get clip values for image data
.. autofunction:: prompt_dataminmax

Utility functions
-----------------

.. index:: Convert PyFITS header to Python dictionary
.. autofunction:: fitsheader2dict

.. index:: Calculate distance on sphere
.. autofunction:: dist_on_sphere

.. autofunction:: showall

Class FITSimage
---------------

.. index:: Extract image data from FITS file
.. autoclass:: FITSimage


Class Annotatedimage
---------------------

.. index:: Plot FITS image data with Matplotlib
.. autoclass:: Annotatedimage


Class Image
-----------

.. autoclass:: Image

Class Contours
--------------

.. autoclass:: Contours

Class Colorbar
--------------

.. autoclass:: Colorbar

Class Beam
----------

.. autoclass:: Beam

Class Skypolygon
-----------------

.. autoclass:: Skypolygon


Class Marker
------------

.. autoclass:: Marker


Class Gridplot
--------------

.. autoclass:: Gridplot


Class Pixellabels
-----------------

.. autoclass:: Pixellabels

Class Colmaplist
----------------

.. autoclass:: Colmaplist


Class FITSaxis
--------------

.. autoclass:: FITSaxis

Class Positionmessage
---------------------

.. autoclass:: Positionmessage


Class MovieContainer
--------------------

.. autoclass:: MovieContainer

Class Cubeatts
---------------

.. autoclass:: Cubeatts

Class Cubes
-----------

.. autoclass:: Cubes

   
"""
# ----------------- Use for experiments -----------------
"""
# Use this to change the default backend
#from matplotlib import use
use('qt5agg')

from matplotlib import __version__ as mplversion
print("Matplotlib version:", mplversion)
# Experiment with a local LaTeX e.g. to improve horizontal label alignment
#from matplotlib import rc
#rc('text', usetex=True)
"""
# Use this to find the current backend. We need this parameter to find
# out whether we work with a QT canvas or not. For a QT canvas we deal with
# toolbar messages in a different way.
__version__ = '1.32'     # 07 Jun 2021

# Local settings of LC_NUMERIC that interpret dots as comma's v.v., area
# causing problems with reading numbers. We change this variable to a global default
# a.s.a.p. but we are not sure if it propagates well (e.g. tabarray does not seem
# to recognize it. Update: We inserted it in the cython code of tabarray
import locale
locale.setlocale(locale.LC_NUMERIC,"C")

from matplotlib import rcParams
backend = rcParams['backend'].upper()

# Try to set default plot output to file to pdf format
try:
   rcParams['savefig.format'] = 'pdf'
except:
   pass

try:
   rcParams['figure.constrained_layout.use'] = False
except:
   pass


# From some Matplotlib version on, the default direction for tick marks
# changed from inwards to outwards. Here we restore the behaviour like
# it was for pre Python 3 versions with older Matplotlibs
# We changed also method setp_tickmarks() in wcsgrat so that it mimics the 
# Matplotlib method Axes.tick_params()
try:
   rcParams['xtick.direction'] = 'in'
   rcParams['ytick.direction'] = 'in'
except:
   pass



from sys import stdout
# Check version of Matplotlib because from version 1.2.0 the nxutils
# module is no longer available.
# Note that the new nxutils functionality with 'path' applies to version 1.3.
# It does not work for the 1.1 versions.
from matplotlib import __version__ as mplversion
mploldversion = mplversion < '1.2.0'
#print("Matplotlib version:", mplversion, mploldversion)

from matplotlib.pyplot import setp as plt_setp
from matplotlib.pyplot import figure, show
from matplotlib import cm
from matplotlib.colors import Colormap, Normalize          #, LogNorm, NoNorm
from matplotlib.colorbar import make_axes, Colorbar, ColorbarBase
from matplotlib.patches import Polygon, Rectangle, Ellipse
from matplotlib.ticker import MultipleLocator, ScalarFormatter, FormatStrFormatter
from matplotlib.lines import Line2D
from matplotlib.text import Text
from matplotlib.image import AxesImage
from matplotlib.widgets import Cursor
import matplotlib.axes as axesclass
from mpl_toolkits.axes_grid1 import make_axes_locatable, inset_locator



if mploldversion:
   import matplotlib.nxutils as nxutils
else:
   import matplotlib.path as path

# PyFITS 1.2.x is not compatible with 1.3.x, i.e. if we remove lines which cause
# the 'deprecated' warnings and replace them by the suggested PyFITS 1.3.x versions
# then the code is not backwards compatible.
# If pyfits cannot be imported, it could be that a user has Astropy installed
# The fits module is the old PyFITS but without a version number, so we create
# one.   
import warnings
try:   
   import pyfits
   pyfitsversion = pyfits.__version__
   pyfitsoldversion = pyfitsversion < '3.1.0'
except:
   from astropy import __version__ as astropyversion
   import astropy.io.fits as pyfits
   pyfitsoldversion = False
   pyfitsversion = "Astropy %s"%astropyversion
   from astropy.io.fits.verify import VerifyWarning
   warnings.simplefilter('ignore', category=VerifyWarning)
   
import numpy
from kapteyn import wcs, wcsgrat
from kapteyn.celestial import skyrefsystems, epochs, skyparser, lon2hms, lat2dms, lon2dms
from kapteyn.tabarray import tabarray, readColumns
from kapteyn.mplutil import AxesCallback, CanvasCallback, VariableColormap, TimeCallback, KeyPressFilter
from kapteyn.positions import str2pos, mysplit, unitfactor
# VOG: from kapteyn.interpolation import map_coordinates  # original from scipy.ndimage.interpolation
from scipy.ndimage import map_coordinates
# VOG: from kapteyn.filters import gaussian_filter
# Original location was: from scipy.ndimage.filters import gaussian_filter
from scipy.ndimage.filters import gaussian_filter
from kapteyn import rulers
import readline
from gc import collect as garbagecollect
from string import ascii_letters as letters
from random import choice
from re import split as re_split
from datetime import datetime
import time
from os import getpid as os_getpid
from os import remove, getcwd
from os.path import basename as os_basename
from subprocess import Popen, PIPE, check_call, CalledProcessError, check_output
from platform import system as os_system
#from scipy.ndimage import interpolation

try:
   from gipsy import anyout, typecli
   gipsymod = True
except:
   gipsymod = False

# VOG, 05-07-2023 The KeyPressFilter does not work anymore in MPL 3.7
# But we can filter in another way, using rcParams settings.
# I chose to disable all MPL key shortcuts in the canvas. They are all
# available in the navigation toolbar.
# The full screen option is kind of destructive so needed to be filtered anyhow.
# KeyPressFilter.allowed = ['f', 'g']

for key in ('keymap.fullscreen', 'keymap.home', 'keymap.back',
            'keymap.forward', 'keymap.pan', 'keymap.zoom', 'keymap.save',
            'keymap.quit', 'keymap.grid', 'keymap.yscale', 'keymap.xscale'):
    rcParams[key] = ''


(left,bottom,right,top) = (wcsgrat.left, wcsgrat.bottom, wcsgrat.right, wcsgrat.top)                 # Names of the four plot axes
(native, notnative, bothticks, noticks) = (wcsgrat.native, wcsgrat.notnative, wcsgrat.bothticks, wcsgrat.noticks) 


# Each object of class Annotatedimage is stored in a list.
# This list is used by function 'showall()' to plot
# all the objects in each Annotatedimage object.
annotatedimage_list = []
nonefunc = lambda x: None

backgroundchanged = False
MINBOUND = 0.005
MAXBOUND = 0.995

# Redefine some methods to intercept actions which causes a canvas.draw() call
# and a change in the Axes bbox, caused by a toolbar navigation action.
# 1) After zooming, the mouse button is released which will trigger a release_zoom event
#    After this event we want to restore image and graticule
# 2) After dragging and realeasing the mouse button, a pan_release event is triggered.
#    After this event we want to restore image and graticule
#    This also covers for zooming in drag mode, because after releasing the mouse
#    button in this mode, also a pan_release event will be triggered.
# 3) The navigation toolbar hosts a home, next and previous button, which stores
#    a memory of images which are zoomed or panned. After such actions, we want 
#    to restore image and graticule
# 4) The navigation toolbar also hosts a button which starts the subplot configuration
#    tool. We limited its functionality. If a subplots_adjust event is generated, 
#    we want to restore image and graticule according to the maputils settings
#    which takes colorbar and slices into account. So effectively the subplot configuration
#    tool does nothing. We still need to redefine subplots_adjust() because 
#    if an application starts with a frame created with subplots(1,1,1), then 
#    the callback to subplots_adjust() is triggered and we need to reposition 
#    images at startup to avoid copying the settings of subplots_adjust().
#
# These effects are realized by redefining the callback functions and adding the 
# calls to the image and graticule reposition functions.

from matplotlib.backend_bases import NavigationToolbar2
NavigationToolbar2.ext_callback = None
NavigationToolbar2.ext_callback2 = None

"""
# Not necessary anymore
def _update_view(self):
    print "+update_view++++++++++++++++++++"
    '''update the viewlim and position from the view and
    position stack for each axes
    '''
    # This callback is triggered if a user clicks the home or arrow
    # buttons to reset a previous view.
    lims = self._views()
    if lims is None:  return
    pos = self._positions()
    if pos is None: return
    try:      # Added by VOG. Not in original MPL routine
      for i, a in enumerate(self.canvas.figure.get_axes()):
         xmin, xmax, ymin, ymax = lims[i]
         a.set_xlim((xmin, xmax))
         a.set_ylim((ymin, ymax))
         # Restore both the original and modified positions
         a.set_position( pos[i][0], 'original' )
         a.set_position( pos[i][1], 'active' )
    except:
       pass
    
    self.draw()
    if self.ext_callback2:
       self.ext_callback2()
    if self.ext_callback:
       self.ext_callback()


def drag_pan(self, event):
    'the drag callback in pan/zoom mode'

    print "Drag pan"
    for a, ind in self._xypress:
        #safer to use the recorded button at the press than current button:
        #multiple button can get pressed during motion...
        a.drag_pan(self._button_pressed, event.key, event.x, event.y)
    self.dynamic_update()

    if self.ext_callback:
       print "callback+++"
       self.ext_callback()
       

#NavigationToolbar2._update_view = _update_view
#NavigationToolbar2.release = release
#NavigationToolbar2.drag_pan = drag_pan
       
"""

def removeLinesandTextfromframe(ax):
    # One cannot remove plot elements from a frame anymore with setting
    # its attribute to an empty list e.g. ... .lines = []
    # Here is an alternative:
    for line in ax.lines:
       line.remove()
    for txt in ax.texts:
       txt.remove()


def resetframe(self, event):
    # Reset content (image, panels, graticules after toolbar actions
    # zoom, pan, home, previous, next
    if self.ext_callback2:
       self.ext_callback2()
    if self.ext_callback:
       self.ext_callback()


oldzoom = NavigationToolbar2.release_zoom
def newzoom(self, event):
    oldzoom(self, event)
    resetframe(self, event)
NavigationToolbar2.release_zoom = newzoom

oldpan = NavigationToolbar2.release_pan
def newpan(self, event):
    oldpan(self, event)
    resetframe(self, event)
NavigationToolbar2.release_pan = newpan

old_update_view = NavigationToolbar2._update_view
def new_update_view(self):
  try:
    old_update_view(self)
  except:
    pass
  resetframe(self, None)
NavigationToolbar2._update_view = new_update_view
    
from matplotlib.figure import Figure
Figure.ext_callback = None
old_subplots_adjust = Figure.subplots_adjust
def new_subplots_adjust(self, *args, **kwargs):
    old_subplots_adjust(self, *args, **kwargs)
    if self.ext_callback:
       self.ext_callback()       
Figure.subplots_adjust = new_subplots_adjust

"""
def subplots_adjust(self, *args, **kwargs):
    #
    #fig.subplots_adjust(left=None, bottom=None, right=None, top=None,
        #wspace=None, hspace=None)

    #Update the :class:`SubplotParams` with *kwargs* (defaulting to rc where
    #None) and update the subplot locations

    #
    self.subplotpars.update(*args, **kwargs)
    import matplotlib.axes
    for ax in self.axes:
        if not isinstance(ax, matplotlib.axes.SubplotBase):
            # Check if sharing a subplots axis
            if ax._sharex is not None and isinstance(ax._sharex, matplotlib.axes.SubplotBase):
                ax._sharex.update_params()
                ax.set_position(ax._sharex.figbox)
            elif ax._sharey is not None and isinstance(ax._sharey, matplotlib.axes.SubplotBase):
                ax._sharey.update_params()
                ax.set_position(ax._sharey.figbox)
        else:
            ax.update_params()
            ax.set_position(ax.figbox)
    if self.ext_callback:
       self.ext_callback()
       
Figure.subplots_adjust = subplots_adjust
"""


def getmemory():
#-------------------------------------------------------------------------------
# Use 'ps' on Linux to get the resident memory in use by the program.
# For other os, use cbook.reports_memory() to get the memory pages.
# The Linux version is based on the report_memory() but with the # rss,sz
# options switched, because we want the residential memory here.
#-------------------------------------------------------------------------------
   pid = os_getpid()
   system = os_system()
   garbagecollect()
   linux = darwin = False
   if system.lower().startswith('linux'):
      command = ['ps', '-p', '%d' % pid, '-o', 'sz,rss']
      linux = True
   elif system.lower().startswith('darwin'):
      command = ['ps', '-p', '%d' % pid, '-o', 'vsz,rss']
      darwin = True      
   if linux or darwin:
      ou = check_output(command)
      ou1 = ou.split()                      # split bytes objects in list of 4
      mem = int(ou1[-1].decode("utf-8"))    # Convert last value (rss in kB) to a string type
      #p = Popen(command , shell=True, stdout=PIPE)
      #a = p.stdout.readlines()
      #mem = int(ou[1].split()[1])                     # Res. memory in kB
      #mem = int(a[1].split()[1])                     # Res. memory in kB
      mem /= 1024                                    # in mB
      mem = "Resident mem.: " + str(int(mem)) + " MB "
      #p.stdout.close()
      #p.kill()
   else:
      try:
         from matplotlib.cbook import report_memory
         mem = report_memory()
      except:
         mem = "?"
      mem = "Mem. pages: " + str(mem)
   return mem
   

def nint(x):
#-------------------------------------------------------------------------------
# GIPSY compatible nearest integer
#-------------------------------------------------------------------------------
   return numpy.floor(x+0.5)

         
def flushprint(s):
#-------------------------------------------------------------------------------
# Helper function to write debug information to the terminal, and because
# we flush stdout, it will be printed immediately.
#-------------------------------------------------------------------------------
   #return  # TODO if no debugging is required
   print(s)
   stdout.flush()


[docs]def showall(): #-------------------------------------------------------------------- """ Usually in a script with only one object of class :class:`maputils.Annotatedimage` one plots this object, and its derived objects, with method :meth:`Annotatedimage.plot`. Matplotlib must be instructed to do the real plotting with pyplot's function *show()*. This function does this all. :Examples: >>> im1 = f1.Annotatedimage(frame1) >>> im1.Image() >>> im1.Graticule() >>> im2 = f2.Annotatedimage(frame2) >>> im2.Image() >>> maputils.showall() """ #-------------------------------------------------------------------- for ai in annotatedimage_list: ai.plot() show() # For Matplotlib
def issequence(obj): return isinstance(obj, (list, tuple, numpy.ndarray)) def getfilename(pre='mu', post='fits'): # Create filename unique to the microsecond (%f), using # 'pre' as prefix and 'post' as filename extension. # Parameter 'post' should not contain a dot. d = datetime.now() stamp = d.strftime("%Y%m%d_%Hh%Mm%Ss") filename = "%s%s%d.%s" %(pre, stamp, d.microsecond, post) return filename def randomlabel(base=''): # Generate random label (e.q. to distinguish frames with labels) chars = letters label = base for i in range(8): label = label + choice(chars) return label def get_splitlon(projection): #-------------------------------------------------------------------- """ Assumed is a two dimensional map with two spatial axes. The projection is extracted from the ctype of the first axis. Then a decision is made whether we have a border in an all sky plot in longitude or not. """ #-------------------------------------------------------------------- if projection.category not in ['undefined', 'zenithal']: # then possible border problem for polygons splitlon = projection.crval[0] + 180.0 if splitlon >= 360.0: splitlon -= 360.0 if splitlon <= -360.0: splitlon += 360.0 else: splitlon = None return splitlon def getnumbers(prompt): #-------------------------------------------------------------------- """ Given a series of expressions all representing numbers, return a list with the evaluated numbers. An expression that could not be evaluated is skipped without a warning. Mathematical functions can be used from NumPy and should be entered e.g. as *numpy.sin(numpy.pi)* """ #-------------------------------------------------------------------- xstr = input(prompt) xsplit = mysplit(xstr) X = [] for x in xsplit: try: xlist = eval(x) if issequence(xlist): for xx in xlist: X.append(xx) else: X.append(eval(x)) except: pass return X def getscale(hdr): # Get relevant scaling keywords from this header # Make sure you use this function before assigning # the data from the header to a variable. bscale = None bzero = None blank = None bitpix = None if 'BITPIX' in hdr: bitpix = hdr['BITPIX'] # Exist always for PyFITS header, but not for dict. if 'BSCALE' in hdr: bscale = hdr['BSCALE'] if 'BZERO' in hdr: bzero = hdr['BZERO'] if 'BLANK' in hdr: blank = hdr['BLANK'] return bitpix, bzero, bscale, blank
[docs]def dist_on_sphere(l1, b1, l2, b2): #----------------------------------------------------------- """ Formula for distance on sphere accurate over entire sphere (Vincenty, Thaddeus, 1975). Input and output are in degrees. :param l1: Longitude of first location on sphere :type l1: float :param b1: Latitude of first location on sphere :type b1: float :param l2: Longitude of second location on sphere :type l2: float :param b2: Latitude of second location on sphere :type b2: float :Examples: >>> from kapteyn.maputils import dist_on_sphere >>> print(dist_on_sphere(0,0, 20,0)) 20.0 >>> print(dist_on_sphere(0,30, 20,30)) 17.2983302106 """ #----------------------------------------------------------- fac = numpy.pi / 180.0 l1 *= fac; b1 *= fac; l2 *= fac; b2 *= fac dlon = l2 - l1 aa1 = numpy.cos(b2)*numpy.sin(dlon) aa2 = numpy.cos(b1)*numpy.sin(b2) - numpy.sin(b1)*numpy.cos(b2)*numpy.cos(dlon) a = numpy.sqrt(aa1*aa1+aa2*aa2) b = numpy.sin(b1)*numpy.sin(b2) + numpy.cos(b1)*numpy.cos(b2)*numpy.cos(dlon) d = numpy.arctan2(a,b) return d*180.0/numpy.pi
def dispcoord(longitude, latitude, disp, direction, angle): #-------------------------------------------------------------------- """ INPUT: longitude: numpy array, enter in degrees. latitude: numpy array, enter in degrees. disp: the displacement in the sky entered in degrees. The value can also be negative to indicate the opposite direction angle: the angle wrt. a great circle of constant declination entered in degrees. direction: If the longitude increases in the -X direction (e.q. RA-DEC) then direction is -1. else direction = +1 Assume a triangle on a sphere with side b(=disp) connec- ting two positions along a great circle and sides 90-d1, and 90-d2 (d1, d2 are the declinations of the input and output positions) that connect the input and output position to the pole P of the sphere. Then the distance between the two points Q1=(a1,d1) and Q2=(a2,d2) is: cos(b)=cos(90-d1)cos(90-d2)+sin(90-d1)sin(90-d2)cos(a2-a1) Q2 is situated to the left of Q1. If the angle PQ1Q2 is alpha then we have another cosine rule: cos(90-d2) = cos(b)cos(90-d1)+sin(b)sin(90-d1)cos(alpha) or: sin(d2) = cos(b)sin(d1)+sin(b)cos(d1)cos(alpha) which gives d2. Angle Q1PQ2 is equal to a2-a1. For this angle we have the sine formula: sin(b)/sin(a2-a1) = sin(90-d2)/sin(alpha) so that: sin(a2-a1) = sin(b)sin(alpha)/cos(d2). b,alpha and d2 are known -> a2. """ #-------------------------------------------------------------------- Pi = numpy.pi b = abs(disp*Pi/180.0) a1 = longitude * Pi/180.0 d1 = latitude * Pi/180.0 alpha = angle * Pi/180.0 d2 = numpy.arcsin( numpy.cos(b)*numpy.sin(d1)+numpy.cos(d1)*numpy.sin(b)*numpy.cos(alpha) ) cosa2a1 = (numpy.cos(b) - numpy.sin(d1)*numpy.sin(d2))/(numpy.cos(d1)*numpy.cos(d2)) sina2a1 = numpy.sin(b)*numpy.sin(alpha)/numpy.cos(d2) dH = numpy.arctan2(direction*sina2a1, cosa2a1) a2 = a1 - dH lonout = a2*180.0/Pi latout = d2*180.0/Pi return lonout, latout def split_polygons(plons, plats, splitlon): #----------------------------------------------------------- """ Given a set coordinates which form a polygon, we need to return a list of polygons. The polygons that should be distinghuised are those on one side of a split meridian (splitlon) and those on the other side. We need this if polygons are plotted near the edges (in longitudes) of allsky plots with cylindrical (at least: non-zenithal) projections. Then we need to split up the polygon. But this polygon can be complicated. For example it is possible that the polygon must be splitted in more than two parts. This algorith works only for those polygons that, when sliced by 'splitlon' fall apart in two pieces. Returns: lon1, lat1, lon2, lat2 The longitudes and latitudes of first and second object. The lists could be empty if the polygon is contained in the range splitlon+epsilon to splitlon-epsilon Intersections: Line through: (a1,d1), (a2,d2). At which d3 is a3? lambda = (a3-a1)/(a2-a1). Then when a = a3 --> d3 = d1 + lambda *(d2-d1) """ #----------------------------------------------------------- epsilon = 0.00000001 # Find intersections with 'splitlon' meridian ilons = []; ilats = [] prevlon = None; prevlat = None lons = list(plons); lons.append(lons[0]) lats = list(plats); lats.append(lats[0]) lon1 = []; lat1 = [] lon2 = []; lat2 = [] right2left = False left2right = False lastlat = None changedborder = 0 intersectlon = [None, None] intersectlat = [None, None] for lon, lat in zip(lons, lats): if not prevlon is None: crossed = False if prevlon < splitlon: lon1.append(prevlon); lat1.append(prevlat) eps = -epsilon if lon > splitlon: crossed = True; else: lon2.append(prevlon); lat2.append(prevlat) eps = epsilon if lon <= splitlon: # Intersection from left to right crossed = True if crossed: # Intersection from right to left if prevlon == lon: d3 = prevlat else: Lambda = (splitlon-prevlon)/(lon-prevlon) d3 = prevlat + Lambda*(lat-prevlat) intersectlon[changedborder] = splitlon + eps # i.e. +/-epsilon intersectlat[changedborder] = d3 changedborder += 1 # Now we have to close the separate parts of the polygon if changedborder == 2: dlats = numpy.linspace(0,1,20) for dlat in dlats: if eps > 0.0: ds = intersectlat[0]; de = intersectlat[1] else: ds = intersectlat[1]; de = intersectlat[0] d = ds + dlat*(de-ds) lon1.append(splitlon-epsilon) lat1.append(d) d = de + dlat*(ds-de) lon2.append(splitlon+epsilon) lat2.append(d) changedborder = 0 # Reset prevlon = lon; prevlat = lat return lon1, lat1, lon2, lat2 def change_header(hdr, **kwargs): #----------------------------------------------------------- """ Given header 'hdr', change/add or delete all keys given in **kwargs. Input 'hdr' is either a Python dictionary or a PyFITS header object. """ #----------------------------------------------------------- if type(hdr) == 'dict': dicttype = True else: dicttype = False # We need to loop over all keys because we distinguish # deleting/adding and changing of entries which cannot # be combined into 1 action. for name in list(kwargs.keys()): value = kwargs[name] fitsname = name.upper() if value is None: try: del hdr[fitsname] except KeyError: pass else: if dicttype: hdr[fitsname] = value else: hdr[fitsname] = value # VOG the same as from astropy 0.1 # Nothing to return. Only contents of 'hdr' is changed def colornavigation_info(): #----------------------------------------------------------------- """ This function compiles and returns a help text for color map interaction. """ #----------------------------------------------------------------- helptext = "pgUp and pgDown: browse through colour maps -- MB right: Change slope and offset\n" helptext += "Colour scales: 0=reset 1=linear 2=logarithmic" helptext += "3=exponential 4=square-root 5=square 9=inverse\n" helptext += "h: Toggle histogram equalization & raw image -- " helptext += "z: Toggle smooth & raw image -- x: Increase smooth factor\n" helptext += "m: Save current colour map to disk -- " helptext += "b: Change colour of bad pixels -- " helptext += "Shift MB left: Write pos. to term." # Last line has no line feed (bottom aligned) return helptext
[docs]class Positionmessage(object): #----------------------------------------------------------- """ This class creates an object with attributes that are needed to set a proper message with information about a position in a map and its corresponding image value. The world coordinates are calculated in the sky system of the image. This system could have been changed by the user. The input parameters are usually set after initialization of an object from class :class:`Annotatedimage`. For users/programmers the atributes are more important. With the attributes of objects of this class we can change the format of the numbers in the informative message. Note that the methods of this class return separate strings for the pixel coordinates, the world coordinates and the image values. The final string is composed in the calling environment. :param skysys: The sky definition of the current image :type skysys: A single parameter or tuple with integers or string :param skyout: The sky definition of the current image as defined by a user/programmer :type skyout: A single parameter or tuple with integers or string :param skysys: The sky definition of the current image :type axtype: tuple with strings :Attributes: .. attribute:: pixfmt Python number format to set formatting of pixel coordinates in position message in toolbar. .. attribute:: wcsfmt Python number format to set formatting of world coordinates in position message in toolbar. If the map has a valid sky system then the values will be formatted in hms/dms, unless attribute *hmsdms* is set to *False*. .. attribute:: zfmt Python number format to set formatting of image value(s) in position message in toolbar. .. attribute:: hmsdms If True, spatial coordinates are formatted in hms/dms. .. attribute:: dmsprec Precision in (dms) seconds if coordinate is formatted in dms. The precision in seconds of a longitude axis in an equatorial system is automatically copied from this number and increased with 1. """ #----------------------------------------------------------- def __init__(self, skysys, skyout, axtype): self.pixfmt = "%.1f" self.wcsfmt = "%.3g" # If any is given, this will overrule hms/dms formatting self.wcsuffmt = "%.7f" self.zfmt = "%.3e" self.hmsdms = True self.dmsprec = 1 sys = None if skyout is None: if not skysys is None: sys, ref, equinox, epoch = skyparser(skysys) else: sys, ref, equinox, epoch = skyparser(skyout) self.sys = sys self.axtype = axtype def z2str(self, z): # Image value(s) to string if self.zfmt is None: return None if issequence(z): if len(z) != 3: raise Exception("z2str: Toolbar Message expects 1 or 3 image values") else: s = '' for number in z: if numpy.isnan(number): s += 'NaN' else: s += self.zfmt % number + ' ' return s.rstrip() else: if numpy.isnan(z): s = 'NaN' else: s = self.zfmt % z return s def pix2str(self, x, y): # Pixel coordinates to string if self.pixfmt is None: return None s = self.pixfmt%x + ' ' + self.pixfmt%y return s def wcs2str(self, xw, yw, missingspatial, returnlist=False, unformatted=False): # World coordinates to string if not self.wcsfmt and not unformatted: return None if unformatted and self.wcsuffmt is None: return None vals = (xw, yw) if not missingspatial is None: vals += (missingspatial,) if returnlist: s = [] else: s = '' if unformatted: wcsfmt = self.wcsuffmt else: wcsfmt = self.wcsfmt for atyp, val in zip(self.axtype, vals): if val is None: coord = "NaN" else: if self.hmsdms and not unformatted: if atyp == 'longitude': if self.sys == wcs.equatorial: coord = lon2hms(val, prec=self.dmsprec+1) # One extra digit for hms else: coord = lon2dms(val, prec=self.dmsprec) elif atyp == 'latitude': coord = lat2dms(val, prec=self.dmsprec) else: coord = wcsfmt%val else: coord = wcsfmt%val if returnlist: s.append(coord) else: s += coord + ' ' return s
[docs]class Colmaplist(object): #----------------------------------------------------------- """ This class provides an object which stores the names of all available colormaps. The method *add()* adds external colormaps to this list. The class is used in the context of other classes but its attribute *colormaps* can be useful. :param -: No parameters :Attributes: .. attribute:: colormaps List with names of colormaps as used in combination with keyword parameter *cmap* in the constructor of :class:`Annotatedimage`. Note that currently we use a module level object ``cmlist`` which has all the information, so we don't use class variable ``colormaps``. Only when we created a list with color maps with object ``cmlist``, this variable has a value. Then both of the next statements are equivalent: >>> print(maputils.Colmaplist.colormaps) >>> print(maputils.cmlist.colormaps) """ #----------------------------------------------------------- colormaps = [] def compare_key(self, a): return a.lower() def __init__(self): # A list with available Matplotlib color maps # The '_r' entries are reversed versions. We omit these versions # because maputils has an 'inverse' option to generate an inverted version # of each color lut. self.colormaps = [m for m in list(cm.datad.keys()) if not m.endswith("_r")] # Sort this list in a case insensitive way self.colormaps.sort(key=self.compare_key) Colmaplist.colormaps = self.colormaps # The class variable colormaps points always to the current list self.cmap_default = 'jet' def add(self, clist): if not issequence(clist): clist = [clist] # Just add and sort afterwards: self.colormaps += clist #for c in clist[::-1]: # self.colormaps.insert(0,c) self.colormaps.sort(key=self.compare_key) def addfavorites(self, flist): # Prepend a list with (unsorted) favorites if not issequence(flist): flist = [flist] newlist = flist + self.colormaps self.colormaps = flist + self.colormaps
cmlist = Colmaplist() cmlist.add(VariableColormap.luts()) # Add luts from lut directory of the Kapteyn Package #colormaps = cmlist.colormaps # Add other attributes like a list with scales and the defaults # For the default color map, we cannot set an index because we # never know what color maps are added and how they are sorted. # So we store only the name, and when necessary we retrieve an index # with list method index() # cmlist.cmap_default = 'jet' def cmp_to_key(mycmp): 'Convert a cmp= function into a key= function' class K: def __init__(self, obj, *args): self.obj = obj def __lt__(self, other): return mycmp(self.obj, other.obj) < 0 def __gt__(self, other): return mycmp(self.obj, other.obj) > 0 def __eq__(self, other): return mycmp(self.obj, other.obj) == 0 def __le__(self, other): return mycmp(self.obj, other.obj) <= 0 def __ge__(self, other): return mycmp(self.obj, other.obj) >= 0 def __ne__(self, other): return mycmp(self.obj, other.obj) != 0 return K
[docs]def fitsheader2dict(header, comment=True, history=True): #----------------------------------------------------------- """ Transform a FITS header (read with PyFITS) into a Python dictionary. This is useful if one wants to iterate over all keys in the header. The PyFITS header is not iterable. """ #----------------------------------------------------------- class fitsdict(dict): pass result = fitsdict() result.comment = {} if isinstance(header, dict): for key in header: result[key] = header[key] try: result.comment[key] = header.comment[key] except: result.comment[key] = '' else: if pyfitsoldversion: # Deprecated from PyFITS >= 3.0 cardset = header.ascard else: cardset = header.cards for card in cardset: try: if pyfitsoldversion: key = card.key else: key = card.keyword if (history and key=='HISTORY') or (comment and key=='COMMENT'): try: result[key].append(card.value) except KeyError: result[key] = [card.value] else: result[key] = header[key] result.comment[key] = card.comment except: card.verify() return result
[docs]def prompt_box(pxlim, pylim, axnameX, axnameY): #----------------------------------------------------------- """ External helper function which returns the limits in pixels of the x- and y-axis. The input syntax is: xlo,xhi, ylo,yhi. For *x* and *y* the names of the image axes are substituted. Numbers can be separated by comma's and or spaces. A number can also be specified with an expression e.g. ``0, 10, 10/3, 100*numpy.pi``. All these numbers are converted to integers. :param pxlim: Sequence of two numbers representing limits in pixels along the x axis as defined in the FITS file. :type pxlim: tuple with two integers :param pylim: Sequence of two numbers representing limits in pixels along the y axis as defined in the FITS file. :type pylim: tuple with two integers :param axnameX: Name of image X-axis :type axnameX: String :param axnameY: Name of image Y-axis :type axnameY: String :Prompts: *Enter pixel limits in Xlo,Xhi, Ylo,Yhi ..... [xlo,xhi, ylo,yhi]:* The default should be the axis limits as defined in the FITS header in keywords *NAXISn*. In a real case this could look like: *Enter pixel limits in RAlo,RAhi, DEClo,DEChi ..... [1, 100, 1, 100]:* :Returns: Tuple with two elements pxlim, pylim (see parameter description) :Notes: This function does not check if the limits are within the index range of the (FITS)image. This check is done in the :meth:`FITSimage.set_limits` method of the :class:`FITSimage` class. :Examples: Use of this function as prompt function in the :meth:`FITSimage.set_limits` method:: fitsobject = maputils.FITSimage('rense.fits') fitsobject.set_imageaxes(1,2, slicepos=30) # Define image in cube fitsobject.set_limits(promptfie=maputils.prompt_box) This 'box' prompt needs four numbers. The first is the range in x and the second is the range in y. The input are pixel coordinates, e.g.:: >>> 0, 10 10/3, 100*numpy.pi Note the mixed use of spaces and comma's to separate the numbers. Note also the use of NumPy for mathematical functions. The numbers are truncated to integers. """ #----------------------------------------------------------- boxtxt = "%slo,%shi, %slo,%shi" %(axnameX, axnameX, axnameY, axnameY) while True: try: s = "Enter pixel limits in %s ..... [%d, %d, %d, %d]: " % (boxtxt, pxlim[0], pxlim[1], pylim[0], pylim[1]) box = input(s) if box != "": lims = re_split('[, ]+', box.strip()) xlo = int(eval(lims[0])) xhi = int(eval(lims[1])) ylo = int(eval(lims[2])) yhi = int(eval(lims[3])) readline.add_history(box) # Facilitate input from terminal ok = (pxlim[0] <= xlo <= pxlim[1]) and \ (pxlim[0] <= xhi <= pxlim[1]) and \ (pylim[0] <= ylo <= pylim[1]) and \ (pylim[0] <= ylo <= pylim[1]) if not ok: print("One of the values is outside the pixel limits") else: break else: return pxlim, pylim except KeyboardInterrupt: # Allow user to press ctrl-c to abort program raise except: print("Wrong box") return (xlo, xhi), (ylo, yhi)
[docs]def prompt_fitsfile(defaultfile=None, prompt=True, hnr=None, alter=None, memmap=None): #----------------------------------------------------------------- """ An external helper function for the FITSimage class to prompt a user to open the right Header and Data Unit (hdu) of a FITS file. A programmer can supply his/her own function of which the return value is a sequence containing the hdu list, the header unit number, the filename and a character for the alternate header. :param defaultfile: Name of FITS file on disk or url of FITS file on the internet. The syntax follows the standard described in the PyFITS documentation. See also the examples. :type defaultfile: String :param prompt: If False and a default file exists, then do not prompt for a file name. Open file and start checking HDU's :type prompt: Boolean :param hnr: The number of the FITS header that you want to use. This function lists the hdu information and when hnr is not given, you will be prompted. :type hnr: Integer :param alter: Selects an alternate header. Default is the standard header. Keywords in alternate headers end on a character A..Z :type alter: Empty or a single character. Input is case insensitive. :param memmap: Set PyFITS memory mapping on/off. Let PyFITS set the default. :type memmap: Boolean :Prompts: 1. *Enter name of fits file ...... [a default]:* Enter name of file on disk of valid url. 2. *Enter number of Header Data Unit ...... [0]:* If a FITS file has more than one HDU, one must decide which HDU contains the required image data. :Returns: * *hdulist* - The HDU list and the user selected index of the wanted hdu from that list. The HDU list is returned so that it can be closed in the calling environment. * *hnr* - FITS header number. Usually the first header, i.e. *hnr=0* * *fitsname* - Name of the FITS file. * *alter* - A character that corresponds to an alternate header (with alternate WCS information e.g. a spectral translation). :Notes: -- :Examples: Besides file names of files on disk, PyFITS allows url's and gzipped files to retrieve FITS files e.g.:: http://www.atnf.csiro.au/people/mcalabre/data/WCS/1904-66_ZPN.fits.gz """ #-------------------------------------------------------------------- while True: try: if defaultfile is None: filename = '' s = "Enter name of FITS file: " else: filename = defaultfile s = "Enter name of FITS file ...... [%s]: " % filename # PyFits syntax if defaultfile is None or prompt: fn = input(s) else: fn = filename if fn != '': filename = fn # print("Filename memmap", filename, memmap) hdulist = pyfits.open(filename, memmap=memmap) break except IOError as xxx_todo_changeme1: (errno, strerror) = xxx_todo_changeme1.args print("I/O error(%s): %s opening [%s]" % (errno, strerror, filename)) prompt = True # Also prompt when a default file name was entered except KeyboardInterrupt: raise except: defaultfile = None print("Cannot open file, unknown error.") con = input("Abort? ........... [Y]/N:") if con == '' or con.upper() == 'Y': raise Exception("Loop aborted by user") hdulist.info() # Note that an element of this list can be accessed either # by integer number or by name of the extension. if hnr is None: n = len(hdulist) if n > 1: while True: p = input("Enter number of Header Data Unit ... [0]:") if p == '': hnr = 0 break else: try: p = int(p) except: pass try: k = hdulist[p] hnr = p break except: pass else: hnr = 0 # If there is no character given for an alternate header # but an alternate header is detected, then the user is # prompted to enter a character from a list with allowed # characters. Currently an alternate header is found if # there is a CRPIX1 followed by a character A..Z if alter == '': alternates = [] hdr = hdulist[hnr].header for a in letters[:26]: k = "CRPIX1%c" % a.upper() # To be sure that it is uppercase if k in hdr: print("Found alternate header:", a.upper()) alternates.append(a) #alter = '' if len(alternates): while True: p = input("Enter char. for alternate header ... [No alt. header]:") if p == '': alter = '' break else: if p.upper() in alternates: alter = p.upper() break else: print("Character not in list with allowed alternates!") return hdulist, hnr, filename, alter
[docs]def prompt_imageaxes(fitsobj, axnum1=None, axnum2=None, slicepos=None): #----------------------------------------------------------------------- """ Helper function for FITSimage class. It is a function that requires interaction with a user. Therefore we left it out of any class definition. so that it can be replaced by any other function that returns the position of the data slice in a FITS file. It prompts the user for the names of the axes of the wanted image. For a 2D FITS data set there is nothing to ask, but for dimensions > 2, we should prompt the user to enter two image axes. Then also a list with pixel positions should be returned. These positions set the position of the data slice on the axes that do not belong to the image. Only with this information the right slice can be extracted. The user is prompted in a loop until a correct input is given. If a spectral axis is part of the selected image then a second prompt is prepared for the input of the required spectral translation. :param fitsobj: An object from class FITSimage. This prompt function derives useful attributes from this object such as the allowed spectral translations. :type fitsobj: Instance of class FITSimage :param axnum1: The axis number of the first (horizontal in terms of plot software) axis of the selected image which should be used as the default in the prompt. If *None* then the default is set to 1. :type axnum1: Integer [1, NAXIS] :param axnum2: The axis number of the first (horizontal in terms of plot software) axis of the selected image which should be used as the default in the prompt. If *None* then the default is set to 1. If both *axnum1* and *axnum2* are specified then the image axis input prompt is skipped. :type axnum2: Integer [1, NAXIS] :Prompts: Name of the image axes: *Enter 2 axes from (list with allowed axis names) .... [default]:* e.g.: ``Enter 2 axes from (RA,DEC,VELO) .... [RA,DEC]:`` The axis names can be abbreviated. A minimal match is applied. :Returns: Tuple with three elements: * *axnum1*: Axis number of first image axis. Default or entered by a user. * *axnum2*: Axis number of second image axis. Default or entered by a user. * *slicepos*: A list with pixel positions. One pixel for each axis outside the image in the same order as the axes in the FITS header. These pixel positions are necessary to extract the right 2D data from FITS data with dimensions > 2. :Example: Interactively set the axes of an image using a prompt function:: # Create a maputils FITSimage object from a FITS file on disk fitsobject = maputils.FITSimage('rense.fits') fitsobject.set_imageaxes(promptfie=maputils.prompt_imageaxes) """ #----------------------------------------------------------------------- n = fitsobj.naxis if (axnum1 is None or axnum2 is None): a1 = axnum1; a2 = axnum2 ax1 = a1; ax2 = a2 unvalid = True axnamelist = '(' for i in range(1,n+1): axnamelist += fitsobj.axisinfo[i].axname if i < n: axnamelist += ',' axnamelist += ')' while unvalid: if axnum1 is None: a1 = 1 ax1 = -1 if axnum2 is None: a2 = 2 ax2 = -1 deflt = "%s,%s" % (fitsobj.axisinfo[a1].axname, fitsobj.axisinfo[a2].axname) mes = "Enter 2 axes from %s .... [%s]: " % (axnamelist, deflt) str1 = input(mes) if str1 == '': str1 = deflt axes = re_split('[, ]+', str1.strip()) if len(axes) == 2: for i in range(n): ax = i + 1 str2 = fitsobj.axisinfo[ax].axname if str2.find(axes[0].upper(), 0, len(axes[0])) > -1: ax1 = ax if str2.find(axes[1].upper(), 0, len(axes[1])) > -1: ax2 = ax unvalid = not (ax1 >= 1 and ax1 <= n and ax2 >= 1 and ax2 <= n and ax1 != ax2) if unvalid: # No exceptions because we are in a loop print("Incorrect input of image axes!") if (ax1 == ax2 and ax1 != -1): print("axis 1 == axis 2") else: print("Number of images axes must be 2. You entered %d" % (len(axes),)) print("You selected: ", fitsobj.axisinfo[ax1].axname, fitsobj.axisinfo[ax2].axname) axnum1 = ax1; axnum2 = ax2 axperm = [axnum1, axnum2] # Retrieve pixel positions on axes outside image # To facilitate the parsing of defaults one needs to allow # pre-defined values for slicepos. if slicepos is None: slicepos = [] if n > 2: for i in range(n): axnr = i + 1 maxn = fitsobj.axisinfo[axnr].axlen if (axnr not in [axnum1, axnum2]): unvalid = True while unvalid: crpix = fitsobj.axisinfo[axnr].crpix if crpix < 1 or crpix > fitsobj.axisinfo[axnr].axlen: crpix = 1 prompt = "Enter pixel position between 1 and %d on %s ..... [%d]: " % (maxn, fitsobj.axisinfo[axnr].axname,crpix) x = input(prompt) if x == "": x = int(crpix) else: x = int(eval(x)) #x = eval(x); print("X=", x, type(x)) unvalid = not (x >= 1 and x <= maxn) if unvalid: print("Pixel position not in range 1 to %d! Try again." % maxn) slicepos.append(x) return axnum1, axnum2, slicepos
[docs]def prompt_spectrans(fitsobj): #----------------------------------------------------------------------- """ Ask user to enter spectral translation if one of the axes is spectral. :param fitsobj: An object from class FITSimage. From this object we derive the allowed spectral translations. :type fitsobj: Instance of class FITSimage :Prompts: The spectral translation if one of the image axes is a spectral axis. *Enter number between 0 and N of spectral translation .... [native]:* *N* is the number of allowed translations minus 1. The default *Native* in this context implies that no translation is applied. All calculations are done in the spectral type given by FITS header item *CTYPEn* where *n* is the number of the spectral axis. :Returns: * *spectrans* - The selected spectral translation from a list with spectral translations that are allowed for the input object of class FITSimage. A spectral translation translates for example frequencies to velocities. :Example: >>> fitsobject = maputils.FITSimage(promptfie=maputils.prompt_fitsfile) >>> print(fitsobject.str_spectrans() # Print a list with options first) >>> fitsobject.set_spectrans(promptfie=maputils.prompt_spectrans) """ #----------------------------------------------------------------------- asktrans = False for ax in fitsobj.axperm: if fitsobj.axisinfo[ax].wcstype == 'spectral': asktrans = True spectrans = None nt = len(fitsobj.allowedtrans) if (nt > 0 and asktrans): s = '' for i, tr in enumerate(fitsobj.allowedtrans): s += "%d:%s (%s) " % (i, tr[0], tr[1]) print(s) unvalid = True while unvalid: try: prompt = "Enter number between 0 and %d of spectral translation .... [native]: " % (nt - 1) st = input(prompt) if st != '': st = int(st) unvalid = (st < 0 or st > nt-1) if unvalid: print("Not a valid number!") else: spectrans = fitsobj.allowedtrans[st][0] else: unvalid = False except KeyboardInterrupt: raise except: unvalid = True return spectrans
[docs]def prompt_skyout(fitsobj): #----------------------------------------------------------------------- """ Ask user to enter the output sky system if the data is a spatial map. :param fitsobj: An object from class FITSimage. This prompt function uses this object to get information about the axis numbers of the spatial axes in a data structure. :type fitsobj: Instance of class FITSimage :Returns: * *skyout* - The sky definition to which positions in the native system will be transformed. :Example: >>> fitsobject = maputils.FITSimage(promptfie=maputils.prompt_fitsfile) >>> fitsobject.set_skyout(promptfie=maputils.prompt_skyout) """ #----------------------------------------------------------------------- # The check below is also done in the calling environment (method set_skyout) # but we need this test also here when this function is used in a different context. spatials = [fitsobj.proj.lonaxnum, fitsobj.proj.lataxnum] spatialmap = fitsobj.axperm[0] in spatials and fitsobj.axperm[1] in spatials if not spatialmap: return # Silently skyout = skysys = refsys = equinox = epoch = None unvalid = True maxskysys = 4 while unvalid: try: prompt = "Sky system 0=eq, 1=ecl, 2=gal, 3=sup.gal .... [native]: " st = input(prompt) if st != '': st = int(st) unvalid = (st < 0 or st >= maxskysys) if unvalid: print("Not a valid number!") else: skysys = st else: unvalid = False except KeyboardInterrupt: raise except: unvalid = True if skysys in [wcs.equatorial, wcs.ecliptic]: # Equatorial or ecliptic, so ask reference system unvalid = True maxrefsys = 5 prompt = "Ref.sys 0=fk4, 1=fk4_no_e, 2=fk5, 3=icrs, 4=dynj2000 ... [native]: " while unvalid: try: st = input(prompt) if st != '': st = int(st) unvalid = (st < 0 or st >= maxrefsys) if unvalid: print("Not a valid number!") else: refsys = st + maxskysys # The ref. systems start at maxskysys else: unvalid = False except KeyboardInterrupt: raise except: unvalid = True if refsys in [wcs.fk4, wcs.fk4_no_e, wcs.fk5]: prompt = "Enter equinox (e.g. J2000 or B1983.5) .... [native]: " unvalid = True while unvalid: try: st = input(prompt) if st != '': B, J, JD = epochs(st) equinox = st unvalid = False except KeyboardInterrupt: raise except: unvalid = True if refsys in [wcs.fk4, wcs.fk4_no_e]: prompt = "Enter date of observation (e.g. MJD24034) .... [native]: " unvalid = True while unvalid: try: st = input(prompt) if st != '': B, J, JD = epochs(st) epoch = st unvalid = False except KeyboardInterrupt: raise except: unvalid = True if skysys is None: skyout = None else: skyout = [] skyout.append(skysys) if equinox is not None: skyout.append(equinox) if refsys is not None: skyout.append(refsys) if epoch is not None: skyout.append(epoch) skyout = tuple(skyout) return skyout
[docs]def prompt_dataminmax(fitsobj): #----------------------------------------------------------------------- """ Ask user to enter one or two clip values. If one clip level is entered then in display routines the data below this value will be clipped. If a second level is entered, then all data values above this level will also be filtered. :param fitsobj: An object from class FITSimage. :type fitsobj: Instance of class FITSimage :Returns: * *clipmin*, *clipmax* - Two values to set limits on the image value e.g. for color editing. :Example: >>> fitsobject = maputils.FITSimage(promptfie=maputils.prompt_fitsfile) >>> clipmin, clipmax = maputils.prompt_dataminmax(fitsobject) >>> annim = fitsobject.Annotatedimage(frame, clipmin=clipmin, clipmax=clipmax) """ #----------------------------------------------------------------------- mi, ma = fitsobj.get_dataminmax(box=True) mes = "Enter clip levels [%g %g]: " % (mi, ma) clips = input(mes) if clips: clips = re_split('[, ]+', clips.strip()) if len(clips) == 1: clipmin = eval(clips[0]) clipmax = ma else: clipmin = eval(clips[0]) clipmax = eval(clips[1]) else: clipmin = mi clipmax = ma return clipmin, clipmax
[docs]class Image(object): #-------------------------------------------------------------------- """ Prepare the FITS- or external image data to be plotted in Matplotlib. All parameters are set by method :meth:`Annotatedimage.Image`. The keyword arguments are those for Matplotlib's method *imshow()*. Two of them are useful in the context of this class. These parameters are *visible*, a boolean to set the visibility of the image to on or off, and *alpha*, a number between 0 and 1 which sets the transparency of the image. See also: :meth:`Annotatedimage.Image` :Methods: .. automethod:: plot """ #-------------------------------------------------------------------- def __init__(self, imdata, box, cmap, norm, **kwargs): #-------------------------------------------------------------------- # Prepare the FITS- or external image data to be plotted in Matplotlib #-------------------------------------------------------------------- self.ptype = "Image" self.box = box newkwargs = ({'origin':'lower', 'extent':self.box, 'interpolation':'nearest'}) # Our first choice was interpolation='none' which is an option from # Matplotlib version 1.1.0 and newer. However without interpolation it # is impossible to use the set_alpha method to make an image transparent newkwargs.update(kwargs) self.kwargs = newkwargs self.data = imdata self.frame = None # MPL Axes object is where the image is displayed self.im = None # The MPL image as a result of imshow() self.xyn_mouse = [0.5,0.5] # Mouse position for color editing self.cmap = cmap self.norm = norm
[docs] def plot(self, frame): #-------------------------------------------------------------------- """ Plot image object. Usually this is done by method :meth:`Annotatedimage.plot` but it can also be used separately. """ #-------------------------------------------------------------------- if self.data is None: raise Exception("Cannot plot image because image data is not available!") self.frame = frame # # To prevent an overkill of plotpixels, we decimate the map # The zoom factor is set to the ratio of the number of display pixels and data pixels. t = self.frame.figure.transFigure.transform([(0, 0), (1,1)]) pixX = abs(t[0,0] - t[1,0]) zoom = pixX / self.data.shape[1] #print("pixel, Zoom:", pixX, zoom, self.data.shape) """ if zoom < 1.0: dummy = interpolation.zoom(self.data, zoom, order=3, mode='constant', cval=0.0, prefilter=True) #self.data = dummy else: dummy = self.data """ dummy = self.data # In the following call to imshow() it seems to be necessary to set the # aspect ratio explicitly. We copy its value from the current frame. self.frame.set_xlim((self.box[0], self.box[1])) self.frame.set_ylim((self.box[2], self.box[3])) self.im = self.frame.imshow(dummy, cmap=self.cmap, norm=self.norm, aspect=frame.get_aspect(), **self.kwargs)
[docs]class Contours(object): #-------------------------------------------------------------------- """ Objects from this class calculate and plot contour lines. Most of the parameters are set by method :meth:`Annotatedimage.Contours`. The others are: :param filled: If True, then first create filled contours and draw the contour lines upon these filled contours :type filled: Boolean :param negative: Set the line style of the contours that represent negative image numbers. The line styles are Matplotlib line styles e.g.: [None | 'solid' | 'dashed' | 'dashdot' | 'dotted'] :type negative: String :param kwargs: Parameters for properties of all contours (e.g. *linewidths*). :Notes: If the line widths of contours are given in the constructor (parameter *linewidths*) then these linewidths are copied to the line widths in the colorbar (if requested). :Methods: .. automethod:: plot .. automethod:: setp_contour .. automethod:: setp_label """ #-------------------------------------------------------------------- def __init__(self, imdata, box, levels=None, cmap=None, norm=None, filled=False, negative="dashed", **kwargs): #-------------------------------------------------------------------- # See class description #-------------------------------------------------------------------- self.ptype = "Contour" # Set type of this object self.box = box self.cmap = cmap # If not None, select contour colours from cmap self.norm = norm # Scale data according to this normalization newkwargs = ({'origin':'lower', 'extent':box}) # Necessary to get origin right newkwargs.update(kwargs) # Input kwargs can overrule this. self.kwargs = newkwargs self.data = imdata # Necessary for methods contour/contourf in plot() self.clevels = levels self.commoncontourkwargs = None # Is set for all contours by method setp_contour() self.ckwargslist = None # Properties in setp_contour for individual contours if self.clevels is not None: self.ckwargslist = [None]*len(self.clevels) self.commonlabelkwargs = None # Is set for all contour labels in setp_labels() self.lkwargslist = None # Properties in setp_labels for individual labels if self.clevels is not None: self.lkwargslist = [None]*len(self.clevels) self.labs = None # Label objects from method clabel() self.filled = filled # Do we require filled contours? # Prevent exception for the contour colors if 'colors' in self.kwargs: # One of them (colors or cmap) must be None! if self.kwargs['colors'] is not None: self.cmap = None # Given colors overrule the colormap self.negative = negative
[docs] def plot(self, frame): #-------------------------------------------------------------------- """ Plot contours object. Usually this is done by method :meth:`Annotatedimage.plot` but it can also be used separately. """ #-------------------------------------------------------------------- if self.data is None: raise Exception("Cannot plot image because image data is not available!") self.frame = frame if self.clevels is None: if self.filled: self.CS = self.frame.contourf(self.data, cmap=self.cmap, norm=self.norm, **self.kwargs) else: self.CS = self.frame.contour(self.data, cmap=self.cmap, norm=self.norm, **self.kwargs) self.clevels = self.CS.levels else: if not issequence(self.clevels): self.clevels = [self.clevels] if self.filled: self.CS = self.frame.contourf(self.data, self.clevels, cmap=self.cmap, norm=self.norm, **self.kwargs) else: self.CS = self.frame.contour(self.data, self.clevels, cmap=self.cmap, norm=self.norm, **self.kwargs) self.clevels = self.CS.levels # Restore the frame that includes entire pixels self.frame.set_xlim((self.box[0], self.box[1])) self.frame.set_ylim((self.box[2], self.box[3])) # Properties if self.commoncontourkwargs is not None: for c in self.CS.collections: plt_setp(c, **self.commoncontourkwargs) if self.ckwargslist is not None: for i, kws in enumerate(self.ckwargslist): if kws is not None: plt_setp(self.CS.collections[i], **kws) if self.negative is not None: for i, lev in enumerate(self.CS.levels): if lev < 0: plt_setp(self.CS.collections[i], linestyle=self.negative) if self.commonlabelkwargs is not None: self.labs = self.frame.clabel(self.CS, **self.commonlabelkwargs) #for c in self.labs: # plt_setp(c, **self.commonlabelkwargs) if self.lkwargslist is not None: for i, kws in enumerate(self.lkwargslist): if kws is not None: lab = self.frame.clabel(self.CS, [self.clevels[i]], **kws)
#plt_setp(lab, **kws)
[docs] def setp_contour(self, levels=None, **kwargs): #-------------------------------------------------------------------- """ Set properties for contours either for all contours if *levels* is omitted or for specific levels if keyword *levels* is set to one or more levels. :param levels: None or one or more levels from the set of given contour levels :type levels: None or one or a sequence of numbers :Examples: >>> cont = annim.Contours(levels=list(range(10000,16000,1000))) >>> cont.setp_contour(linewidth=1) >>> cont.setp_contour(levels=11000, color='g', linewidth=3) """ #-------------------------------------------------------------------- if levels is None: self.commoncontourkwargs = kwargs else: if self.clevels is None: raise Exception("Contour levels not set so I cannot identify contours") # Change only contour properties for levels in parameter 'levels'. if not issequence(levels): levels = [levels] for lev in levels: try: i = list(self.clevels).index(lev) except ValueError: i = -1 # no match if i != -1: self.ckwargslist[i] = kwargs
[docs] def setp_label(self, levels=None, tex=True, **kwargs): #-------------------------------------------------------------------- """ Set properties for the labels along the contours. The properties are Matplotlib properties (fontsize, colors, inline, fmt). :param levels: None or one or more levels from the set of given contour levels :type levels: None or one or a sequence of numbers :param tex: Print the labels in TeX if a format is entered. If set to True, add '$' characters so that Matplotlib knows that it has to format the label in TeX. The default is *True*. :type tex: Boolean :param xxx: Other parameters are Matplotlib parameters for method :meth:`clabel` in Matplotlib :class:`ContourLabeler` (fontsize, colors, inline, fmt). :Examples: >>> cont2 = annim.Contours(levels=(8000,9000,10000,11000)) >>> cont2.setp_label(11000, colors='b', fontsize=14, fmt="%.3f") >>> cont2.setp_label(fontsize=10, fmt="%g \lambda") """ #-------------------------------------------------------------------- if tex and ('fmt' in kwargs): kwargs['fmt'] = r'$'+kwargs['fmt']+'$' if levels is None: self.commonlabelkwargs = kwargs else: if self.clevels is None: raise Exception("Contour levels not set so I cannot identify contours") # Change only contour properties for levels in parameter 'levels'. if not issequence(levels): levels = [levels] for lev in levels: try: i = list(self.clevels).index(lev) except ValueError: i = -1 # no match if i != -1: self.lkwargslist[i] = kwargs
def remove(self): #-------------------------------------------------------------------- # Remove matplotlib object after which it will not be redrawn #-------------------------------------------------------------------- for coll in self.CS.collections: coll.remove()
[docs]class Colorbar(object): #-------------------------------------------------------------------- """ Colorbar class. Usually the parameters will be provided by method :meth:`Annotatedimage.Colorbar` Useful keyword parameters: :param frame: If a frame is given then this frame will be the colorbar frame. If None, the frame is calculated by taking space from its parent frame. :type frame: Matplotlib Axes instance :Methods: .. automethod:: plot .. automethod:: set_label """ #-------------------------------------------------------------------- def __init__(self, cmap, location=None, size="5%", pad=None, frame=None, parentframe=None, norm=None, contourset=None, clines=False, fontsize=9, label=None, linewidths=None, visible=True, **kwargs): #-------------------------------------------------------------------- """ cmap: Applies only for Matplotlib versions <= 0.99.8. This and some other arguments are supplied by the :meth:`Annotatedimage.Colorbar` method. location: The location of the colorbar can be one of 'left', 'bottom', 'top' or 'right'. The keyword argument 'orientation' should be in agreement with the location. The arguments 'location', 'size' and 'pad' are the three arguments for method divider.append_axes(). size: Size of colorbar in percentage of mother axes object (e.g. image). pad: Pad between image axes and colorbar axes frame: If a frame is entered it will be stored here and used as Axes object. However when None, the calling environment must call plot() with a valid frame (usually this frame will be 'stolen' from a mother frame. clines: Draw contour lines in colorbar also. Does not matter if this comes from an image or contour set. linewidhts: One number to set the line width of all the contour lines in the colorbar. Note that linewidths set for a contour level will be copied for the line width of the line in the colorbar. However if parameter *linewidths* is set, it will be applied to all lines in the contour colorbar. visible: """ #-------------------------------------------------------------------- self.ptype = "Colorbar" self.cmap = cmap self.size = size self.pad = pad self.norm = norm self.contourset = contourset self.plotcontourlines = clines self.cbfontsize = fontsize self.linewidths = linewidths self.frame = frame # Axes dedicated to colorbar only self.parentframe = parentframe # If no Axes given for colorbar, derive one from this Axes self.label = label self.labelkwargs = None orientation = kwargs.get('orientation', None) if not location: if not orientation: location = 'right' else: if orientation == 'horizontal': location = 'bottom' else: location = 'right' self.location = location # We use the axes divider from the MPL toolkit if not orientation: if location in ('right', 'left'): orientation = 'vertical' else: orientation = 'horizontal' kwargs.update({'orientation': orientation}) if pad is None: if location == 'left': pad = 0.2 elif location == 'right': pad = 0.15 elif location == 'top': pad = 0.05 elif location == 'bottom': pad = 0.2 if not visible: alpha = {'alpha':0.0} kwargs.update(alpha) self.kwargs = kwargs if self.frame is None: # Then this colorbar does not have its own Axes. So we need to create one. # We had an implementation with an AxesDivider but only a parent # Axes is resized, and the resize is at draw time so other Axes that # are overlayed, will not be properly resized. The onrezise() function # does not help here. So our new approach (16-07-2023) is to make # an Axes object for the colorbar without allowing the parent Axes to change # dimensions. For this we use a special version of inset_axes() # loc parameter: 'upper left', 'upper center', # 'upper right', 'center left', 'center', # 'center right', 'lower left', 'lower center', 'lower right' if location in ('top', 'bottom'): width = "100%"; height=self.size if location == 'top': loc = "upper right" bbox = (0, pad, 1, 1) else: loc = "lower left" bbox = (0, -pad, 1, 1) else: width = self.size; height="100%" if location == 'left': loc = "upper left" bbox = (-pad, 0, 0.1, 1) else: loc = "upper right" bbox = (pad, 0., 1, 1) pos = parentframe.get_position().bounds self.frame = inset_locator.inset_axes( parentframe, width=width, height=height, loc=loc, bbox_to_anchor=bbox, bbox_transform=parentframe.transAxes, borderpad=0) def colorbarticks(self): #-------------------------------------------------------------------- """ Usually used within the context of this class, but can also be used if one needs to resize the colorbar tick labels in the calling environment. """ #-------------------------------------------------------------------- for t in self.cb.ax.get_xticklabels(): # Smaller font for color bar t.set_fontsize(self.cbfontsize) for t in self.cb.ax.get_yticklabels(): # Smaller font for color bar along y t.set_fontsize(self.cbfontsize)
[docs] def plot(self, cbframe, mappable=None): #-------------------------------------------------------------------- """ Plot image object. Usually this is done by method :meth:`Annotatedimage.plot` but it can also be used separately. Note: We changed the default formatter for the colorbar. This can be done with the 'format' parameter. We changed the formatter to a fixed format string. This prevents that MPL uses an offset and a scaling for the labels. If MPL does this, it adds an extra label, showing the offset and scale in scientific notation. We do not want this extra label because we don't have enough control over it (e.g. it can appear outside your viewport or in a black background with a black font). With the new formatter we are sure to get the real value in our labels. """ #-------------------------------------------------------------------- self.frame = cbframe # self.frame could have been None fig = cbframe.figure # We need the figure to use the colorbar method majorFormatter = FormatStrFormatter("%g") self.cb = fig.colorbar(mappable, cax=self.frame, norm=self.norm, format=majorFormatter, **self.kwargs) if self.plotcontourlines and self.contourset is not None: CS = self.contourset.CS # VOG, 07-07-2023: The add_lines() method complains. The object should # be a ContourSet, but it is a QuadContourSet and then MPL's colorbar class # throws an exception. So we try something else: """ if not ('ticks' in self.kwargs): self.kwargs["ticks"] = CS.levels # Copy the line widhts. Note that this is a piece of code # that assumes certain behaviour from the attributes in a # contour set. It can fail with newer Matplotlib versions. lws = [] if not self.linewidths is None: for lw in CS.tlinewidths: lws.append((self.linewidths,)) CS.tlinewidths = lws self.cb.add_lines(CS) """ tcolors = [c[0] for c in CS.tcolors] if len(tcolors) != len(CS.levels): # For filled contours there is 1 color and 1 linewidth less. tcolors.append((0,0,0,0)) lw = self.linewidths if not lw: lw = [1.5]*len(CS.levels) if numpy.isscalar(lw): lw = [lw]*len(CS.levels) if len(lw) != len(CS.levels): lw.append(lw[-1]) self.cb.add_lines(CS.levels, tcolors, lw) else: CS = None self.colorbarticks() # Set font size given in kwargs or use default if self.label is not None: if self.labelkwargs is None: self.cb.set_label(self.label) else: self.cb.set_label(self.label, **self.labelkwargs)
[docs] def set_label(self, label, **kwargs): #-------------------------------------------------------------------- """ Set a text label along the long side of the color bar. It is a convenience routine for Matplotlib's *set_label()* but this one needs a plotted colorbar while we postpone plotting. """ #-------------------------------------------------------------------- self.label = label self.labelkwargs = kwargs
[docs]class Beam(object): #-------------------------------------------------------------------- """ Beam class. Usually the parameters will be provided by method :meth:`Annotatedimage.Beam` Objects from class Beam are graphical representations of the resolution of an instrument. The beam is centered at a position xc, yc. The major axis of the beam is the FWHM of longest distance between two opposite points on the ellipse. The angle between the major axis and the North is the position angle. Note that it is not correct to calculate the ellipse that represents the beam by applying distance 'r' (see code) as a function of angle, to get the new world coordinates. The reason is that the fwhm's are given as sizes on a sphere and therefore a correction for the declination is required. With method *dispcoord()* (see source code of class Beam) we sample the ellipse on a sphere with a correct position angle and with the correct sizes. Note that a beam can also be specified in 'pixel'/'grid' units. Then the ellipse axes and center are in pixel coordinates, while the position angle is defined with respect to the positive X axis (i.e. not the astronomical position angle). """ #-------------------------------------------------------------------- def __init__(self, xc, yc, fwhm_major, fwhm_minor, pa, projection=None, units=None, **kwargs): self.ptype = "Beam" if units: # Could be pixels/grids grids = units.upper().startswith('PIX') else: grids = False if not grids: if units is not None: uf, errmes = unitfactor('degree', units) if uf is None: raise ValueError(errmes) else: fwhm_major /= uf fwhm_minor /= uf semimajor = fwhm_major / 2.0 semiminor = fwhm_minor / 2.0 Pi = numpy.pi startang, endang, delta = (0.0, 360.0, 1.0) sinP = numpy.sin( pa*Pi/180.0 ) cosP = numpy.cos( pa*Pi/180.0 ) phi = numpy.arange( startang, endang+delta, delta, dtype="f" ) cosA = numpy.cos( phi*Pi/180.0 ) sinA = numpy.sin( phi*Pi/180.0 ) d = (semiminor*cosA) * (semiminor*cosA) + (semimajor*sinA) * (semimajor*sinA) r = numpy.sqrt( (semimajor*semimajor * semiminor*semiminor)/d ) self.p1 = None self.p2 = None lon_new, lat_new = dispcoord(xc, yc, r, -1, phi+pa) splitlon = get_splitlon(projection) if splitlon is None: lon1, lat1 = lon_new, lat_new lon2 = lat2 = [] else: lon1, lat1, lon2, lat2 = split_polygons(lon_new, lat_new, splitlon) if len(lon1): xp, yp = projection.topixel((lon1, lat1)) self.p1 = Polygon(list(zip(xp, yp)), **kwargs) if len(lon2): xp, yp = projection.topixel((lon2, lat2)) self.p2 = Polygon(list(zip(xp, yp)), **kwargs) else: self.p1 = Ellipse((xc, yc), fwhm_major, fwhm_minor, angle=pa, **kwargs) self.p2 = None def plot(self, frame): #-------------------------------------------------------------------- """ Plot a beam with the parameters given in the constructor. """ #-------------------------------------------------------------------- if not self.p1 is None: #flushprint("Patch xy = %s"%str(self.p1.get_path())) frame.add_patch(self.p1) if not self.p2 is None: frame.add_patch(self.p2) def remove(self): #-------------------------------------------------------------------- # Remove matplotlib object after which it will not be redrawn #-------------------------------------------------------------------- if self.p1: self.p1.remove() if self.p2: self.p2.remove()
[docs]class Skypolygon(object): #-------------------------------------------------------------------- """ This class defines objects that can only be plotted onto spatial maps. Usually the parameters will be provided by method :meth:`Annotatedimage.Skypolygon` """ #-------------------------------------------------------------------- def __init__(self, projection, prescription=None, xc=None, yc=None, major=None, minor=None, nangles=6, pa=0.0, units=None, lons=None, lats=None, stepsize=1.0, **kwargs): self.ptype = "Skypolygon" self.p1 = self.p2 = None # Shape could be splitted into two parts self.patch = None splitlon = get_splitlon(projection) grids = False if prescription is None: if lons is None and lats is None: raise ValueError("No prescription entered nor longitudes and latitudes") elif lons is None or lats is None: raise ValueError("No prescription entered and missing longitudes or latitudes") grids = False if units and units.upper().startswith('PIX'): if (grids): splitlon = None else: # Test minimal set of required parameters if xc is None or yc is None: raise ValueError("Missing value for center xc or yc!") if major is None and minor is None: raise ValueError("Both major and minor axes are not specified!") if major is None: major = minor if minor is None: minor = major if units is not None: uf, errmes = unitfactor('degree', units) if uf is None: raise ValueError(errmes) else: major /= uf minor /= uf Pi = numpy.pi if prescription[0].upper() == 'E': semimajor = major / 2.0 semiminor = minor / 2.0 startang, endang, delta = (0.0, 360.0, abs(stepsize)) # should be enough samples sinP = numpy.sin( pa*Pi/180.0 ) cosP = numpy.cos( pa*Pi/180.0 ) phi = numpy.arange( startang, endang+delta, delta, dtype="f") cosA = numpy.cos( phi*Pi/180.0 ) sinA = numpy.sin( phi*Pi/180.0 ) d = (semiminor*cosA) * (semiminor*cosA) + (semimajor*sinA) * (semimajor*sinA) r = numpy.sqrt( (semimajor*semimajor * semiminor*semiminor)/d ) lons, lats = dispcoord(xc, yc, r, -1, phi+pa) #for lo,la in zip(lons,lats): # print("%.1f %.1f"%(lo,la)) elif prescription[0].upper() == 'R': # Create rectangle with Major as the long side and aligned with North xs = minor/2.0 ys = -major/2.0 samples = 100 deltax = minor/float(samples) deltay = major/float(samples) x = numpy.zeros(samples) y = numpy.zeros(samples) for i in range(samples): x[i] = xs y[i] = ys + i *deltay phi1 = numpy.arctan2(y,x) r1 = numpy.hypot(x,y) xs = minor/2.0 ys = major/2.0 for i in range(samples): x[i] = xs - i * deltax y[i] = ys phi2 = numpy.arctan2(y,x) r2 = numpy.hypot(x,y) xs = -minor/2.0 ys = major/2.0 for i in range(samples): x[i] = xs y[i] = ys - i *deltay phi3 = numpy.arctan2(y,x) r3 = numpy.hypot(x,y) xs = -minor/2.0 ys = -major/2.0 for i in range(samples): x[i] = xs + i * deltax y[i] = ys phi4 = numpy.arctan2(y,x) r4 = numpy.hypot(x,y) phi = numpy.concatenate((phi1, phi2, phi3, phi4))*180/Pi+90 # dispcoord wants it in degs. r = numpy.concatenate((r1, r2, r3, r4)) lons, lats = dispcoord(xc, yc, r, -1, phi+pa) elif prescription[0].upper() == 'N': if nangles < 3: raise ValueError("Number of angles in regular polygon must be > 2!") nsamples = 360.0/nangles psi = numpy.linspace(0, 360, nangles+1) radius = major/2.0; xs = radius; ys = 0.0 # Start value first = True for ang in psi[1:]: xe = radius * numpy.cos(ang*Pi/180) ye = radius * numpy.sin(ang*Pi/180) lambd = numpy.arange(0.0, 1.0, 1.0/nsamples) x = xs + lambd * (xe - xs) y = ys + lambd * (ye - ys) Phi = numpy.arctan2(y,x)*180.0/Pi R = numpy.hypot(x,y) xs = xe ys = ye if first: phi = Phi.copy() r = R.copy() first = False else: phi = numpy.concatenate((phi, Phi)) r = numpy.concatenate((r, R)) lons, lats = dispcoord(xc, yc, r, -1, phi+pa) if lons is None or lats is None: raise ValueError("Unknown prescription") if splitlon is None: lon1, lat1 = lons, lats lon2 = lat2 = [] else: lon1, lat1, lon2, lat2 = split_polygons(lons, lats, splitlon) if len(lon1): if grids: # Here we have only lon1, not lon2 xp, yp = lon1, lat1 else: xp, yp = projection.topixel((lon1, lat1)) s = str(xp) self.p1 = Polygon(list(zip(xp, yp)), closed=True, **kwargs) self.lon1 = lon1; self.lat1 = lat1 if len(lon2): xp, yp = projection.topixel((lon2, lat2)) self.p2 = Polygon(list(zip(xp, yp)), closed=True, **kwargs) self.lon2 = lon2; self.lat2 = lat2 def plot(self, frame): if not self.p1 is None: frame.add_patch(self.p1) self.patch = self.p1 if not self.p2 is None: frame.add_patch(self.p2) self.patch = self.p2 def remove(self): #-------------------------------------------------------------------- # Remove matplotlib object after which it will not be redrawn #-------------------------------------------------------------------- if self.p1: self.p1.remove() if self.p2: self.p2.remove()
[docs]class Gridplot(object): #-------------------------------------------------------------------- """ Gridplot class. Usually the parameters will be provided by method :meth:`Annotatedimage.Marker` Use Matplotlib's plot function to plot lines etc. in the current grid system. Properties of the marker are set with Matplotlib's keyword arguments. """ #-------------------------------------------------------------------- def __init__(self, gridsX, gridsY, **kwargs): self.ptype = "Gridplot" self.xp = gridsX self.yp = gridsY self.kwargs = kwargs self.patch = None def plot(self, frame): self.lines = frame.plot(self.xp, self.yp, **self.kwargs)[0] # First returned def remove(self): #-------------------------------------------------------------------- # Remove matplotlib object after which it will not be redrawn #-------------------------------------------------------------------- self.lines.remove()
[docs]class Marker(object): #-------------------------------------------------------------------- """ Marker class. Usually the parameters will be provided by method :meth:`Annotatedimage.Marker` Mark features in your map with a marker symbol. Properties of the marker are set with Matplotlib's keyword arguments. """ #-------------------------------------------------------------------- def __init__(self, xp=None, yp=None, **kwargs): self.ptype = "Marker" self.xp = xp self.yp = yp self.kwargs = kwargs self.patch = None def plot(self, frame): if self.xp is None or self.yp is None: return if not issequence(self.xp): self.xp = [self.xp] if not issequence(self.yp): self.yp = [self.yp] # VOG: default 'o' not necessary: self.patch = frame.plot(self.xp, self.yp, 'o', **self.kwargs) # Set default marker symbol to prevent connections self.patch = frame.plot(self.xp, self.yp, **self.kwargs) # Set default marker symbol to prevent connections self.patch = self.patch[0] def remove(self): #-------------------------------------------------------------------- # Remove matplotlib object after which it will not be redrawn #-------------------------------------------------------------------- self.patch.remove()
class Gridframe(object): """ ------------------------------------------------------------------------------- Helper class which defines objects with properties which are read when pixel coordinates need to be plotted. ------------------------------------------------------------------------------- """ def __init__(self, pxlim, pylim, plotaxis, gridlines, major, minor, inside=False, **kwargs): self.ptype = "Gridframe" self.pxlim = pxlim self.pylim = pylim self.plotaxis = plotaxis self.markerkwargs = {} self.kwargs = kwargs self.gridlines = gridlines self.major = major self.minor = minor self.inside = inside
[docs]class Pixellabels(object): """ Draw positions in pixels along one or more plot axes. Nice numbers and step size are calculated by Matplotlib's own plot methods. :param plotaxis: The axis number of one or two of the axes of the plot rectangle: * wcsgrat.left * wcsgrat.bottom * wcsgrat.right * wcsgrat.top or 'left', 'bottom', 'right', 'top' :type plotaxis: Integer :param direction: Set direction of tick marks to inwards or outwards. There are three options: 'in', 'out', 'inout' :type direction: String :param pad: Set a value for the distance between axis and tick mark label. By default, this value is set to None. If you did set 'inside=True', the default padding will be -10,but this value can be overruled by a value for 'pad'. Note that padding is applied to all axes. :type pad: None or Float :param gridlines: Set plotting of grid lines (connected tick marks) on or off (True/False). The default is off. :type gridlines: Boolean :param major: This number overrules the default positions for the major tick marks. The tick marks and labels are plotted at a multiple number of *major*. :type major: Float or Integer (usually the input will be an integer). :param minor: This number sets the plotting of minor tick marks on. The markers are plotted at a multiple value of *minor*. :type minor: Float or Integer (usually the input will be an integer). :param offset: The pixels can have an integer offset. If you want the reference pixel to be pixel 0 then supply offset=(crpixX, crpixY). These crpix values are usually read from then header. In this routine the nearest integer of the input is calculated to ensure that the offset is an integer value. :type offset: *None* or a floating point number :param inside: If True, plot labels within the plot, else outside :type inside: Boolean :param kwargs: Keyword arguments to set attributes for the labels (e.g. color='g', fontsize=8) :type kwargs: Matplotlib keyword argument(s) :Returns: An object from class *Gridframe* which is added to the plot container with Plotversion's method :meth:`Plotversion.add`. :Notes: Graticules and Pixellabels are plotted in their own plot frame. If you want to be able to toggle grid lines in a frame labeled with pixel coordinates, then you have to make sure that the Pixellabels frame is plotted last. So always define Pixellabels objects before Graticule objects. :Examples: Annotate the pixels in a plot along the right and top axis of a plot. Change the color of the labels to red:: mplim = f.Annotatedimage(frame) mplim.Pixellabels(plotaxis=("bottom", "right"), color="r") or with separate axes: mplim.Pixellabels(plotaxis="bottom", color="r") mplim.Pixellabels(plotaxis="right", color="b", markersize=10) mplim.Pixellabels(plotaxis="top", color="g", markersize=-10, gridlines=True) Example using the functions that set the properties of the tick markers and tick labels:: plabels = annim.Pixellabels(plotaxis=("right","top"), gridlines=False, inside=True, direction='out', pad=-15) plabels.setp_label(color='c', fontsize=6) plabels.setp_marker(markersize=10, markeredgewidth=3, color='y') :Methods: .. automethod:: setp_marker .. automethod:: setp_label """ def __init__(self, pxlim, pylim, plotaxis=None, direction='in', pad=None, gridlines=False, ticks=None, major=None, minor=None, offset=None, inside=False, **kwargs): def nint(x): return numpy.floor(x+0.5) self.ptype = "Pixellabels" # not a gridframe object self.direction = direction self.pad = pad defkwargs = {'fontsize':7} defkwargs.update(kwargs) if plotaxis is None: plotaxis = [2,3] px = [0,0]; py = [0,0] px[0] = pxlim[0]; py[0] = pylim[0] # Do not copy directly because new values must be temporary px[1] = pxlim[1]; py[1] = pylim[1] if offset is not None: offX = nint(offset[0]) offY = nint(offset[1]) px[0] -= offX; px[1] -= offX; py[0] -= offY; py[1] -= offY; gridlabs = Gridframe(px, py, plotaxis, gridlines, major, minor, inside, **defkwargs) self.gridlabs = gridlabs self.frame = None
[docs] def setp_marker(self, **kwargs): #-------------------------------------------------------------------- """ Set properties of the pixel label tick markers for all axes. For ticks in graticules we allowed keyword *direction=*, but that was to facilitate setting a direction for all axes seperately. For pixel labels, we apply the tick properties to all axes, and because the tick marker direction is not a property of a Line2D object, we allow setting the tick mark direction only in the constructor of this class. :param kwargs: keyword arguments to change properties of the tick marks. A tick mark is a Matploltlib :class:`Line2D` object with attributes like *markeredgewidth* etc. :type kwargs: Python keyword arguments. """ #-------------------------------------------------------------------- self.gridlabs.markerkwargs.update(kwargs)
[docs] def setp_label(self, **kwargs): #-------------------------------------------------------------------- """ Set properties of the pixel label tick markers for all axes :param kwargs: keyword arguments to change properties of (all) the tick labels. A tick mark is a Matploltlib :class:`Text` object with attributes like *fontsize*, *fontstyle* etc. :type kwargs: Python keyword arguments. """ #-------------------------------------------------------------------- self.gridlabs.kwargs.update(kwargs)
def plot(self, frame): """ #----------------------------------------------------------- Purpose: Plot labels that annotate the pixels (or another system if an offset is given) Parameters: pixellabels - Object from class Gridframe made with method Graticule.Pixellabels Returns: Notes: This method can only plot the grid labels along two axes. If you need to label the other axes too, then add another grid with method Pixellabels(). #----------------------------------------------------------- valid keywords for tick_params are ['size', 'width', 'color', 'tickdir', 'pad', 'labelsize', 'labelcolor', 'zorder', 'gridOn', 'tick1On', 'tick2On', 'label1On', 'label2On', 'length', 'direction', 'left', 'bottom', 'right', 'top', 'labelleft', 'labelbottom', 'labelright', 'labeltop', 'labelrotation', 'grid_agg_filter', 'grid_alpha', 'grid_animated', 'grid_antialiased', 'grid_clip_box', 'grid_clip_on', 'grid_clip_path', 'grid_color', 'grid_dash_capstyle', 'grid_dash_joinstyle', 'grid_dashes', 'grid_data', 'grid_drawstyle', 'grid_figure', 'grid_fillstyle', 'grid_gapcolor', 'grid_gid', 'grid_in_layout', 'grid_label', 'grid_linestyle', 'grid_linewidth', 'grid_marker', 'grid_markeredgecolor', 'grid_markeredgewidth', 'grid_markerfacecolor', 'grid_markerfacecoloralt', 'grid_markersize', 'grid_markevery', 'grid_mouseover', 'grid_path_effects', 'grid_picker', 'grid_pickradius', 'grid_rasterized', 'grid_sketch_params', 'grid_snap', 'grid_solid_capstyle', 'grid_solid_joinstyle', 'grid_transform', 'grid_url', 'grid_visible', 'grid_xdata', 'grid_ydata', 'grid_zorder', 'grid_aa', 'grid_c', 'grid_ds', 'grid_ls', 'grid_lw', 'grid_mec', 'grid_mew', 'grid_mfc', 'grid_mfcalt', 'grid_ms'] """ fig = frame.figure pixellabels = self.gridlabs plotaxes = wcsgrat.parseplotaxes(pixellabels.plotaxis) # Is always a list with integers now! # What are the combinations: # Only one axis 0, 1, 2, 3 # Two axes 0,1 - 0,2 - 0,3 # 1,2 - 1,3 # 2,3 # # The combinations 0,1 - 0,3 - 1,2 - 2,3 can be plotted in 1 frame # For 0,2 - 1,3 we need two frames. Then we raise an exception # and suggest a user to plot it in two steps # If there are more than two plot axes then raise also # an exception. if len(plotaxes) > 2: raise Exception("Can plot labels for a maximum of 2 axes. Please split up!") if (0 in plotaxes and 2 in plotaxes) or (1 in plotaxes and 3 in plotaxes): raise Exception("Cannot plot labels for this combination. Please split up!") aspect = frame.get_aspect() adjust = frame.get_adjustable() kwargs = pixellabels.kwargs markerkwargs = pixellabels.markerkwargs xlo = pixellabels.pxlim[0]-0.5 ylo = pixellabels.pylim[0]-0.5 xhi = pixellabels.pxlim[1]+0.5 yhi = pixellabels.pylim[1]+0.5 # Copy frame framelabel = randomlabel('fr_') gframe = frame.inset_axes([0,0,1,1], aspect=aspect, adjustable=adjust, autoscale_on=False, frameon=False, label=framelabel) """ try: r,c,n = frame.get_geometry() gframe = fig.add_subplot(r, c, n, aspect=aspect, adjustable=adjust, autoscale_on=False, frameon=False, label=framelabel) gframe.set_position(frame.get_position()) except: gframe = fig.add_axes(frame.get_position(), aspect=aspect, adjustable=adjust, autoscale_on=False, frameon=False, label=framelabel) """ gframe.set_xlim((xlo,xhi)) gframe.set_ylim((ylo,yhi)) self.frame = gframe if 3 in plotaxes or 1 in plotaxes: if pixellabels.major is not None: majorLocator = MultipleLocator(pixellabels.major) gframe.xaxis.set_major_locator(majorLocator) if pixellabels.minor: minorLocator = MultipleLocator(pixellabels.minor) gframe.xaxis.set_minor_locator(minorLocator) if 3 in plotaxes: gframe.xaxis.set_label_position('top') gframe.xaxis.tick_top() elif 1 in plotaxes: gframe.xaxis.set_label_position('bottom') gframe.xaxis.tick_bottom() else: # both not available -> make invisible for tick in gframe.xaxis.get_major_ticks(): tick.label2.set(visible=False) tick.label1.set(visible=False) tick.tick1On = False tick.tick2On = False # If labels must be plotted inside the frame, the # value of inside= is set to True. # Then we apply a pad and an alignment. But setting # padding with tick.set_pad() overwrites all # properties of tick's Line2D and Text objects. # So this must be done beforehand. setmarker = len(markerkwargs) > 0 labinside = self.gridlabs.inside if self.pad: padding = True pad = self.pad else: if labinside: padding = True pad = -10 else: padding = False # # gframe.xaxis.set_tick_params(tickdir=self.direction, grid_visible=pixellabels.(gridlines)) if pixellabels.gridlines: gframe.xaxis.set_tick_params(grid_visible=pixellabels.gridlines) # gframe.yaxis.set_tick_params(grid_visible=pixellabels.gridlines) for tick in gframe.xaxis.get_major_ticks(): if padding: tick.set_pad(pad) #tick.apply_tickdir(self.direction) if 3 in plotaxes: tick.label2.set(**kwargs) if setmarker: tick.tick2line.set(**markerkwargs) if labinside: tick.label2.set_verticalalignment('bottom') tick.tick1On = False tick.gridOn = pixellabels.gridlines #tick.tick2line.set_marker(tick._tickmarkers[1]) elif 1 in plotaxes: tick.label1.set(**kwargs) if setmarker: tick.tick1line.set(**markerkwargs) if labinside: tick.label1.set_verticalalignment('top') tick.tick2On = False tick.gridOn = pixellabels.gridlines #tick.tick1line.set_marker(tick._tickmarkers[0]) if 2 in plotaxes or 0 in plotaxes: if pixellabels.major is not None: majorLocator = MultipleLocator(pixellabels.major) gframe.yaxis.set_major_locator(majorLocator) if pixellabels.minor: minorLocator = MultipleLocator(pixellabels.minor) gframe.yaxis.set_minor_locator(minorLocator) if 2 in plotaxes: gframe.yaxis.set_label_position('right') gframe.yaxis.tick_right() elif 0 in plotaxes: gframe.yaxis.set_label_position('left') gframe.yaxis.tick_left() else: for tick in gframe.yaxis.get_major_ticks(): tick.label2.set(visible=False) tick.label1.set(visible=False) tick.tick1On = False tick.tick2On = False #if pixellabels.gridlines: # gframe.yaxis.set_tick_params(grid_visible=pixellabels.gridlines) for tick in gframe.yaxis.get_major_ticks(): if padding: tick.set_pad(pad) #tick.apply_tickdir(self.direction) if 2 in plotaxes: tick.label2.set(**kwargs) if setmarker: tick.tick2line.set(**markerkwargs) if labinside: tick.label2.set_horizontalalignment('right') tick.tick1line.set_visible(False) #tick.gridOn = pixellabels.gridlines #tick.tick2line.set_marker(tick._tickmarkers[1]) elif 0 in plotaxes: tick.label1.set(**kwargs) if setmarker: tick.tick1line.set(**markerkwargs) if labinside: tick.label1.set_horizontalalignment('left') tick.tick2line.set_visible(False) #tick.gridOn = pixellabels.gridlines #tick.tick1line.set_marker(tick._tickmarkers[0]) if pixellabels.gridlines: gframe.yaxis.set_tick_params(grid_visible=pixellabels.gridlines) # gframe.grid(pixellabels.gridlines) fig.sca(frame) # back to frame from calling environment def remove(self): #-------------------------------------------------------------------- # Remove matplotlib object after which it will not be redrawn #-------------------------------------------------------------------- self.frame.set_xticks([]) # for minor ticks self.frame.set_xticks([], minor=True) self.frame.set_yticks([]) # for minor ticks self.frame.set_yticks([], minor=True)
[docs]class Annotatedimage(object): #-------------------------------------------------------------------- """ This is one of the core classes of this module. It sets the connection between the FITS data (created or read from file) and the routines that do the actual plotting with Matplotlib. The class is usually used in the context of class :class:`FITSimage` which has a method that prepares the parameters for the constructor of this class. :param frame: This is the frame where image and or contours will be plotted. If omitted then a default frame will be set :type frame: Matplotlib Axes instance :param header: The header data for this file. Either from a FITS header or a dictionary with header data. :type header: Python dictionary or pyfits.NP_pyfits.Header instance :param pxlim: Two integer numbers which should not be smaller than 1 and not bigger than the header value *NAXISn*, where n represents the x axis. :type pxlim: Tuple with two integers :param pylim: Two integer numbers which should not be smaller than 1 and not bigger than the header value *NAXISn*, where n represents the y axis. :type pylim: Tuple with two integers :param imdata: Image data. This data must represent the area defined by *pxlim* and *pylim*. :type imdata: 2D NumPy array :param projection: The current projection object which provides this class with conversion methods from :mod:`wcs` like :meth:`kapteyn.wcs.Projection.toworld` and :meth:`kapteyn.wcs.Projection.topixel` needed for conversions between pixel- and world coordinates. :type projection: Instance of Projection class from module :mod:`wcs` :param axperm: Tuple or list with the FITS axis number of the two image axes, e.g. axperm=(1,2) :type axperm: Tuple with two integers :param wcstypes: In some modules we need to know what the type of an axis in the image is so that for example we can find out whether two different images have swapped axes. The order of this list is the same as the order in the original FITS file. 'lo' is longitude axis, 'la' is latitude axis, 'sp' is spectral axis, 'li' is a linear axis. Appended to 'li' is an underscore and the ctype of that axis (e.g. 'li_stokes'). If the original data has axes (RA, DEC, FREQ, STOKES), then FITSimage.wcstypes = ['lo','la', 'sp', 'li_STOKES'] and when we have an Annotatedimage object with axes (FREQ, DEC) then the axis permutation array is (3, 2) and the wcsypes list is ['sp', 'la']. :type wcstypes: List of strings. :param skyout: A so called sky definition (sky system, reference system, equinox) which is used to annotate the world coordinates and to draw graticule lines. :type skyout: String :param spectrans: The spectral translation. It sets the output system for spectral axes. E.g. a frequency axis can be labeled with velocities. :type spectrans: String :param alter: The alternative description of a world coordinate system. In a FITS header there is an alternative description of the world coordinate system if for each wcs related keyword, there is an alternative keyword which has a character appended to it (e.g. CRVAL1a, CDELT1a). The character that is appended is the one that need to be entered if one wants to use the alternate system. Note that this is only relevant to axis labeling and the plotting of graticules. :type alter: Character (case insensitive) :param mixpix: The axis number (FITS standard i.e. starts with 1) of the missing spatial axis for images with only one spatial axis (e.q. Position-Velocity plots). :type mixpix: *None* or an integer :param aspect: The aspect ratio. This value is used to correct the scale of the plot so that equal sizes in world coordinates (degrees) represent equal sizes in a plot. This aspect ratio is only useful for spatial maps. Its default value is 1.0. The aspect ratio is defined as: :math:`abs(cdelty/cdeltx)`. This value is automatically set in objects from :class:`FITSimage` :param slicepos: Pixel coordinates used to slice the data in a data set with more than two axes. The pixel coordinates represent positions on the axes that do not belong to the image. :type slicepos: Single value or tuple with integers :param sliceaxnames: List with names of the axes outside the map. Assume we have a map of a RA-DEC map from a RA-DEC-FREQ cube, then *sliceaxnames* = ['FREQ']. Currently these names are not used. :type sliceaxnames: List with strings :param basename: Base name for new files on disk, for example to store a color map on disk. The default is supplied by method :meth:`FITSimage.Annotatedimage`. :type basename: string :param cmap: A colormap from class :class:`mplutil.VariableColormap` or a string that represents a colormap (e.g. 'jet', 'spectral' etc.). :type cmap: mplutil.VariableColormap instance or string :param blankcolor: Color of the undefined pixels (NaN's, blanks). It is a matplotlib color e.g. blankcolor='g'. In the display the color of the blanks can be changed with key 'B'. It loops throug a small set with predefined colors. Changing the colors of bad pixels helps to make them more visible. :type blankcolor: Matplotlib color (e.g. 'c', '#aabb12') :param clipmin: A value which sets the lower value of the interval between which the colors in the colormap are distributed. If omitted, the minimum data value will be *clipmin*. :type clipmin: Float :param clipmax: A value which sets the upper value of the interval between which the colors in the colormap are distributed. If omitted, the maximum data value will be *clipmin*. :type clipmax: Float :param boxdat: An 2dim. array with the same shape as the *boxdat* attribute of the input FITSimage object. :type boxdat: NumPy array :param sourcename: Name of origin of data. By default set to 'unknownsource' :type sourcename: String :param gridmode: By default this value is set to False. Positions are written as pixel coordinates and input of coordinates as strings will parse numbers as pixel coordinates. If one sets this to True, then a system of grids is used. The relation between a pixel and a grid for axis *n* is: ``grid = pixel - NINT(CRPIXn)`` Some (legacy) astronomy software packages use this system. Toolbar position information is written in grid coordinates and also numbers in the (string) input of positions are processed as grids. This option is useful when an interface is needed between the Kapteyn Package and another software package (e.g. GIPSY). Note that with positions as strings, we mean positions parsed with method :meth:`kapteyn.positions.str2pos`. These are not the positions which are described as **pixel** positions. :type gridmode: Boolean :param adjustable: todo :type adjustable: todo :param anchor: todo :type anchor: todo :param newaspect: todo :type newaspect: todo :param clipmode: todo :type clipmode: Integer :param clipmn: todo :type clipmn: tuple with 2 integers :param cubemanager: todo :type cubemanager: maputils.figure.canvas.manager object or None :param callbackslist: todo :type callbackslist: Python dictionary :Attributes: .. attribute:: alter Character that sets an alternate world coordinate system. .. attribute:: aspect Aspect ratio of a pixel according to the FITS header. For spatial maps this value is used to set and keep an image in the correct aspect ratio. .. attribute:: axperm Axis numbers of the two axis in this map. Axis numbers start with 1. .. attribute:: basename Name of data origin. .. attribute:: blankcolor Color of 'bad' pixels as a Matplotlib color. .. attribute:: box Coordinates of the plot box. In order to keep the entire pixel in the corners in the plot, one has to extend the values of *pxlim* and *pylim* with 0.5 pixel. .. attribute:: clipmin Value either entered or calculated, which scales the image data to the available colors. Clipmin is the minimum value. .. attribute:: clipmax Value either entered or calculated, which scales the image data to the available colors. Clipmax is the maximum value. .. attribute:: cmap The color map. This is an object from class :class:`mplutil.VariableColormap`. which is inherited from the Matplotlib color map class. .. attribute:: cmapinverse Boolean which store the status of the current colormap, standard or inverted. .. attribute:: data Image data. Other data containers are attibutes 'data_blur', 'data_hist', and 'data_orig'. .. attribute:: fluxfie Function or Lambda expression which can be used to scale the flux found with method *getflux()*. There must be two parameters in this function or expression: *a* for the area and *s* for the sum of the pixel values. E.g. ``Annotatedimage.fluxfie = lambda s, a: s/a`` Note that there is no method to set this attribute. The attribute is used in the shapes module. .. attribute:: frame Matplotlib Axes instance where image and contours are plotted .. attribute:: gridmode Boolean that indicates when we work in pixel- or in grid coordinates. .. attribute:: hdr Header which is used to derive the world coordinate system for axis labels and graticule lines. The header is either a Python dictionary or a PyFITS header. .. attribute:: mixpix The pixel of the missing spatial axis in a Position-Velocity image. .. attribute:: objlist List with all plot objects for the current *Annotatedimage* object derived from classes: 'Beam', 'Colorbar', 'Contours', 'Graticule', 'Image', 'Marker', 'Minortickmarks', 'Pixellabels', 'RGBimage', 'Ruler', 'Skypolygon', 'Gridplot' .. attribute:: pixelstep The step size in pixels or fraction of pixels. This size is used to sample the area of an object. Used in the context of the shapes module. E.g. ``annim.pixelstep = 0.5;`` .. attribute:: pixoffset Tuple with two offsets in pixels used to distinguish a pixel coordinate system from a grid coordinate system. .. attribute:: projection An object from the Projection class as defined in module :mod:`wcs` .. attribute:: ptype Each object in the object list has an attribute which describes the (plot) type of the object. The ptype of an Annotatedimage is *Annotatedimage*. .. attribute:: pxlim Pixel limits in x = (xlo, xhi) .. attribute:: pylim Pixel limits in y = (ylo, yhi) .. attribute:: rgbs Boolean which is set to True if the current image is composed of three images each representing one color. .. attribute:: sliceaxnames A list with axis names that are not part of the current image, but are part of the data structure from which the current Annotated image data is extracted. .. attribute:: skyout The sky definition for which graticule lines are plotted and axis annotation is made (e.g. "Equatorial FK4") .. attribute:: spectrans The translation code to transform native spectral coordinates to another system (e.g. frequencies to velocities) .. attribute:: slicepos Single value or tuple with more than one value representing the pixel coordinates on axes in the original data structure that do not belong to the image. It defines how the data slice is ectracted from the original. The order of these 'outside' axes is copied from the (FITS) header. .. attribute:: wcstypes Type of the axes in this data. The order is the same as of the axes. The types ara strings and are derived from attribute wcstype of the Projection object. The types are: 'lo' is longitude axis. 'la' is latitude axis, 'sp' is spectral axis. 'li' is a linear axis. Appended to 'li' is an underscore and the ctype of that axis (e.g. 'li_stokes'). .. attribute:: lutscales Class variable. A list with possible color mapping scales: ['linear', 'log', 'exp', 'sqrt', 'square'] .. attribute:: blankcols Class variable. A list with Matplotlib colors: ['c', 'w', 'k', 'y', 'm', 'r', 'g', 'b'] .. attribute:: blanknames Class variable. A list with strings, representing the colors in :attr:`maputils.Annotatedimage.blankcols`: ['Cyan', 'White', 'Black', 'Yellow', 'Magenta', 'Red', 'Green', 'Blue'] .. attribute:: slopetrans Pixel values between [a,b] are mapped to colors with index between [A,B]. The mapping is a straight line with a slope and an offset Scale horizontal mouse position between [0,1] into a value between 0 and ``slopetrans`` degrees to scale the mapping of pixel values to colors. Current value is set to 89 degrees. .. attribute:: shifttrans Translate vertical mouse position from [0,1] to [-``shifttrans``, ``shifttrans``] to scale the mapping of pixel values to colors. Currently ``shifttrans`` is set to 0.5. :Methods: .. automethod:: set_norm .. automethod:: set_colormap .. automethod:: write_colormap .. automethod:: set_blankcolor .. automethod:: set_aspectratio .. automethod:: get_colornavigation_info .. automethod:: Image .. automethod:: RGBimage .. automethod:: Contours .. automethod:: Colorbar .. automethod:: Graticule .. automethod:: Pixellabels .. automethod:: Minortickmarks .. automethod:: Beam .. automethod:: Marker .. automethod:: Gridplot .. automethod:: Ruler .. automethod:: Skypolygon .. automethod:: plot .. automethod:: toworld .. automethod:: topixel .. automethod:: inside .. automethod:: histeq .. automethod:: blur .. automethod:: interact_toolbarinfo .. automethod:: interact_imagecolors .. automethod:: interact_writepos .. automethod:: positionsfromfile """ #-------------------------------------------------------------------- # Class variables that set the lut scales and keys lutscales = ['linear', 'log', 'exp', 'sqrt', 'square'] scalekeys = {'1': 0, '2': 1, '3': 2, '4': 3, '5': 4} scales_default = 0 blankcols = ['c', 'w', 'k', 'y', 'm', 'r', 'g', 'b'] blanknames = ['Cyan', 'White', 'Black', 'Yellow', 'Magenta', 'Red', 'Green', 'Blue'] blankcols_default = 0 # Class variables for scaling the colormap slopetrans = 89.0 # Scale mouse value to value between 0 and 89 degrees and to colormap slope shifttrans = 0.5 # Translate mouse value between 0 and 1 to -0.5, 0.5 def __init__(self, frame, header, pxlim, pylim, imdata, projection, axperm, wcstypes, skyout, spectrans, alter='', mixpix=None, aspect=1, slicepos=None, sliceaxnames=None, basename=None, cmap=None, blankcolor=blankcols[blankcols_default], clipmin=None, clipmax=None, boxdat=None, sourcename='unknownsource', gridmode=False, adjustable='box', anchor='C', newaspect=None, clipmode=0, clipmn=(4,5), callbackslist={}): #----------------------------------------------------------------- """ """ #----------------------------------------------------------------- self.ptype = "Annotatedimage" self.sourcename = sourcename self.hdr = header self.projection = projection self.pxlim = pxlim self.pylim = pylim if boxdat is not None: # shp = (pylim[1]-pylim[0]+1, pxlim[1]-pxlim[0]+1) #shp = imdata.shape #if boxdat.shape != shp: # raise ValueError("The shape of 'boxdat' is not (%d,%d)" %(shp[0],shp[1])) self.data = boxdat else: self.data = imdata self.mixpix = mixpix self.axperm = axperm self.wcstypes = wcstypes self.skyout = skyout self.spectrans = spectrans self.alter = alter if self.pxlim[0] <= self.pxlim[1]: deltax = 0.5 else: deltax = -0.5 if self.pylim[0] <= self.pylim[1]: deltay = 0.5 else: deltay = -0.5 self.box = (self.pxlim[0]-deltax, self.pxlim[1]+deltax, self.pylim[0]-deltay, self.pylim[1]+deltay) self.image = None # A Matplotlib instance made with imshow() self.aspect = aspect if not newaspect is None: self.aspect = newaspect self.adjustable = adjustable self.anchor = anchor self.slicepos = slicepos # Information about current slice self.gridmode = gridmode self.sliceaxnames = sliceaxnames self.contours = None self.colorbar = None self.contourset = None if cmap is None: cmap = cmlist.cmap_default self.objlist = [] self.frame = self.adjustframe(frame, adjustable) self.frame = frame self.messenger = None self.toolbarkey = None # Keys of class dictionary with callbacks are: slope, offset, ... self.callbackslist = callbackslist # The next lines need some explanation. Mouse cursor positions # are written to an area on the canvas set by the backend in use. # This area is either in the toolbar or in a status bar (QT4agg) # The default toolbar message is bound to a figure manager # (self.figmanager = self.frame.figure.canvas.manager) # For multiple images we want to set the messenger to the same function. # To disable Matplotlib interference with this function we # set the function to a function that does nothing # with lambda x: None (nonefunc) # Note that external messengers and messengers defined in the Cubes() class # have priority. try: # because this will not be successful for certain backends self.figmanager = self.frame.figure.canvas.manager # Every figure has its own manager if not (self.figmanager.toolbar.set_message is nonefunc): # This is the first image of the current Cubes() object # We introduce a new attribute: 'globalmessenger' to set all messengers of # the current Cubes() object to the same messenger. self.figmanager.toolbar.globalmessenger = self.figmanager.toolbar.set_message self.figmanager.toolbar.set_message = nonefunc except: self.figmanager = None if 'exmes' in callbackslist: self.messenger = callbackslist['exmes'] self.canvasmessenger = False else: # There is no external messenger defined. # Then we have to use the one from the canvas manager (i.e. if it has a toolbar.) try: self.messenger = self.figmanager.toolbar.globalmessenger self.canvasmessenger = True except: self.messenger = nonefunc self.canvasmessenger = False # In the constructor, the colormap should not update the image # otherwise we get incompatible frames which behave differently # if we resize the plot window. self.autostate = None try: self.autostate = cmap.auto cmap.auto = False except: pass self.set_colormap(cmap) self.set_blankcolor(blankcolor) # Calculate defaults for clips if nothing is given # If no clip values are entered, then we calculate them either from # the minimum and maximum values of the current data (self.data) or # we use mean-n*rms and mean+m*rms as the default. This depends on the # option given in parameter 'clipmode'. needstats = False datmin = datmax = mean = rms = None if clipmode == 0: if None in [clipmin, clipmax]: needstats = True else: needstats = True if needstats: datmin, datmax, mean, rms = self.get_stats() if clipmin is None: if clipmode in [0, 1]: clipmin = datmin else: if None in [mean, rms]: clipmin = None else: # Clipmode 2 uses mean and rms clipmin = mean - rms*clipmn[0] if clipmax is None: if clipmode in [0, 1]: clipmax = datmax else: if None in [mean, rms]: clipmax = None # Clipmode 2 uses mean and rms else: clipmax = mean + rms*clipmn[1] # flushprint("I calculated clip min max=%s %s %d"%(str(clipmin), str(clipmax), clipmode)) self.clipmin = clipmin self.clipmax = clipmax self.datmin = datmin self.datmax = datmax self.mean = mean self.rms = rms # Give defaults if clips are still None or +-inf: if self.clipmin is None or not numpy.isfinite(self.clipmin): self.clipmin = 0.0 if self.clipmax is None or not numpy.isfinite(self.clipmax): self.clipmax = self.clipmin + 1.0 if self.clipmin > self.clipmax: self.clipmin, self.clipmax = self.clipmax , self.clipmin self.AxesCallback_ids = [] # A list with 'mpl connect id's. Used for disconnecting self.norm = Normalize(vmin=self.clipmin, vmax=self.clipmax, clip=True) self.histogram = False # Is current image equalized? self.data_hist = None # There is not yet a hist. eq. version of the data self.blurred = False self.data_blur = None self.blurfac = (self.pxlim[1]-self.pxlim[0]+1)/200.0 self.blurindx = 0 self.X_lastvisited = None self.Y_lastvisited = None self.data_orig = self.data # So we can toggle between image versions if basename is None: self.basename = "Unknown" # Default name for file with colormap lut data else: self.basename = basename self.pixelstep = 1.0 # Sub divide pixel for flux self.fluxfie = lambda s, a: s/a annotatedimage_list.append(self) self.pixoffset = [0.0, 0.0] # Use this offset to display pixel positions if self.gridmode: self.set_pixoffset(nint(self.projection.crpix[0]), nint(self.projection.crpix[1])) self.rgbs = None # If not None, then the image is a # composed image with r,g & b components. # Restore the update status of the colormap if not self.autostate is None: cmap.auto = self.autostate self.cmap.auto = self.autostate # TODO: is dit dubbel?? (zie iets hoger) # An Annotatedimage object could be an object for a slice panel. # Then it has its own callback for a motion_notify event to # update a plot. self.regcb = None #if regcallback: # self.regcb = AxesCallback(regcallback, self.frame, 'motion_notify_event') # self.AxesCallback_ids.append(self.regcb) self.infoobj = None # Some object with information. Can be displayed in plot. self.linepieces = [] # Store graticule line pieces for blitting self.textlabels = [] self.splitcb = None # Callback id of split method self.cbframe = None # Frame of associated color bar self.composed_slicemessage = '' # Each im. can have its own info about its slice self.grat = None # Image can be extended with contours self.clevels = None # TODO: Er bestaat ook een contourset (dus zonder hoofdletter) als attribuut. Overbodig?? self.contourSet = None self.contours_on = False self.contours_changed = False self.contours_cmap = None self.contours_colour = 'r' self.backgroundImageNr = None self.alpha = 1.0 # VOG, see doc at onresize(). Next line not necessary anymore # self.resize_id = self.frame.figure.canvas.mpl_connect('resize_event', self.onresize) def onresize(self, event): #----------------------------------------------------------------- """ Helper callback function to deal with a resize event defined in __init__() above. This call is necessary because frames with the same Bbox do not scale the same when a figure is resized. Newer versions of Matplotlib (3.1.x) seem to deal with space for axes labeling and an Axes object with labels scales different than the Axes object without. This resize callback forces the Bbox to remain the same. VOG: The July 2023 update for version 3.4 makes use of inset_axes() to keep Axes in sync. Also we do not use a divider for a colorbar to 'steal' from a parent Axes, but wrote suitable defaults for an independent Axes for a color bar. So the callback for onresize() is not necessary anymore. """ #----------------------------------------------------------------- return """ p1 = self.frame.get_position() for obj in self.objlist: print("Resize frame:", obj.ptype) try: pt = obj.ptype except: raise Exception("Unknown object. Cannot plot this!") if pt == 'Graticule': print("On resize Graticule:", obj.frame.get_position(original=True)) print("On resize Graticule:", obj.frame.get_position(original=False)) obj.frame.set_position(p1) print("On resize Graticule frame 1 after set_position:", obj.frame.get_position(original=True)) print("On resize Graticule frame 1 after set_position:", obj.frame.get_position(original=False)) obj.frame2.set_position(p1) if pt == 'Pixellabels': # or pt == 'Image': print("On resize %s voor set_position:"%(pt), obj.frame.get_position(original=True), id(obj.frame)) print("On resize %s voor set_position:"%(pt), obj.frame.get_position(original=False), id(obj.frame)) obj.frame.set_position(p1) print("On resize %s after set_position:"%(pt), obj.frame.get_position(original=True), id(obj.frame)) print("On resize %s after set_position:"%(pt), obj.frame.get_position(original=False), id(obj.frame)) """ def callback(self, cbid, *arg): #----------------------------------------------------------------- """ Helper function for registered callbacks """ #----------------------------------------------------------------- if cbid in self.callbackslist: self.callbackslist[cbid](*arg) def get_stats(self): #----------------------------------------------------------------- """ Get necessary statistics e.g. to set clip levels """ #----------------------------------------------------------------- if self.data is None: return None, None, None, None # If the data is copied from the FITSobject then # it does not contain -inf and inf values, because these are # replaced by NaN's. If this class is used with an external image # you should replace these values in that image data before creating an object # otherwise Matplotlib will fail to plot anything (image, contours). # If somehow the inf values still exist, then we still want to see # an image and therefore discard inf, -inf and nan to find the clip values. datmin = datmax = mean = rms = mask = None validmask = False datmin = numpy.nanmin(self.data) # Fast method first if not numpy.isfinite(datmin): # value is inf or -inf, try again... mask = numpy.isfinite(self.data) # We need a mask validmask = numpy.any(mask) # At least one must be a valid number if validmask: datmin = float(self.data[mask].min()) else: datmin = None if validmask: # We are sure that there are inf's if numpy.any(mask): # At least one must be a valid number datmax = float(self.data[mask].max()) else: datmax = None else: datmax = numpy.nanmax(self.data) # The fast method if not numpy.isfinite(datmax): datmax = None # For example a map with all values == -inf if validmask: mean = float(self.data[mask].mean()) rms = float(self.data[mask].std()) else: # Here we know that there are no inf's, but there could be still NaN's mean = float(self.data.mean()) rms = float(self.data.std()) if not (numpy.isfinite(rms) and numpy.isfinite(mean)): mask = numpy.isfinite(self.data) if numpy.any(mask): # At least one must be a valid number mean = float(self.data[mask].mean()) rms = float(self.data[mask].std()) #flushprint("Calculated min, max=%s %s"%(str(datmin), str(datmax))) #flushprint("Calculated std, mean=%f %f"%(rms, mean)) return datmin, datmax, mean, rms def set_pixoffset(self, xoff=0.0, yoff=0.0): #----------------------------------------------------------------- """ For conversions from FITS to other pixel based coordinate systems. """ #----------------------------------------------------------------- self.pixoffset[0] = xoff self.pixoffset[1] = yoff
[docs] def set_norm(self, clipmin=None, clipmax=None): #----------------------------------------------------------------- """ Matplotlib scales image values between 0 and 1 for its distribution of colors from the color map. With this method we set the image values which we want to scale between 0 and 1. The default image values are the minimum and maximum of the data in :attr:`Annotatedimage.data`. If you want to inspect a certain range of data values you need more colors in a smaller intensity range, then use different *clipmin* and *clipmax* in the constructor of :class:`Annotatedimage` or in this method. :param clipmin: Image data below this threshold will get the same color Value None will be replaced by 'clipmin'. :type clipmin: Float :param clipmax: Image data above this threshold will get the same color Value None will be replaced by 'clipmax'. :type clipmax: Float :Examples: >>> fitsobj = maputils.FITSimage("m101.fits") >>> annim = fitsobj.Annotatedimage(frame, cmap="spectral") >>> annim.Image(interpolation='spline36') >>> annim.set_norm(10000, 15500) or: >>> fitsobj = maputils.FITSimage("m101.fits") >>> annim = fitsobj.Annotatedimage(frame, cmap="spectral", clipmin=10000, clipmax=15500) :notes: It is also possible to change the norm after an image has been displayed. This enables a programmer to setup key interaction for changing the clip levels in an image for example when the default clip levels are not suitable to inspect a certain data range. Usually the color editing (with :meth:`Annotatedimage.interact_imagecolors`) can do this job very well so we think there is not much demand in a scripting environment. With GUI's it will be different. """ #----------------------------------------------------------------- #if clipmin is None and clipmax is None: # # Nothing to do # return if clipmin is None: clipmin = self.clipmin if clipmax is None: clipmax = self.clipmax if clipmin > clipmax: clipmin, clipmax = clipmax, clipmin # Swap, to prevent ValueError self.norm = Normalize(vmin=clipmin, vmax=clipmax, clip=True) #self.clipmin = clipmin; self.clipmax = clipmax if self.cmap is not None: self.cmap.update() if self.image is not None: self.image.norm = self.norm if self.image.im is not None: self.image.im.set_clim(clipmin, clipmax) if self.colorbar is not None: #self.colorbar.cb.set_norm(self.norm) self.colorbar.cb.set_clim(clipmin, clipmax)
[docs] def set_colormap(self, cmap, blankcolor=None): #----------------------------------------------------------------- """ Method to set the initial color map for images, contours and colorbars. These color maps are either strings (e.g. 'jet' or 'spectral') from a list with Matplotlib color maps or it is a path to a color map on disk (e.g. cmap="/home/user/luts/mousse.lut"). If the color map is not found in the list with known color maps then it is added to the list, which is a global variable called *cmlist*. (see: :data:`maputils.cmlist`) The Kapteyn Package has also a list with useful color maps. See example below or example 'mu_luttest.py' in the :doc:`maputilstutorial`. If you add the event handler *interact_imagecolors()* it is possible to change colormaps with keyboard keys and mouse. :param cmap: The color map to be used for image, contours and colorbar :type cmap: String or instance of VariableColormap :param blankcolor: Color of the bad pixels in your image. :type blankcolor: Matplotlib color :Examples: >>> fitsobj = maputils.FITSimage("m101.fits") >>> annim = fitsobj.Annotatedimage(frame, clipmin=10000, clipmax=15500) >>> annim.set_colormap("spectral") or use the constructor as in: >>> annim = fitsobj.Annotatedimage(frame, cmap="spectral", clipmin=10000, clipmax=15500) Get extra lookup tables from Kapteyn Package (by default, these luts are appended at creation time of cmlist, see :data:`maputils.cmlist`) >>> extralist = mplutil.VariableColormap.luts() >>> maputils.cmlist.add(extralist) """ #----------------------------------------------------------------- if cmap is None: cmap = cmlist.cmap_default if isinstance(cmap, Colormap): self.cmap = cmap # Either a string or a Colormap instance # What to do with the index. This is not a string from the list. self.cmindx = 0 elif isinstance(cmap, str): try: # Is this colormap registered in our colormap list? self.cmindx = cmlist.colormaps.index(cmap) except: # then register it now cmlist.add(cmap) self.cmindx = cmlist.colormaps.index(cmap) self.cmap = VariableColormap(cmap) else: raise Exception("Color map is not of type Colormap or string") if self.image is not None: self.cmap.set_source(cmap) self.startcmap = self.cmap # This could be one that is not in the list with color maps #self.startcmindx = self.cmindx # Use the start color map if a reset is requested self.startcmindx = cmlist.colormaps.index(cmlist.cmap_default) self.cmapinverse = False if blankcolor is not None: self.set_blankcolor(blankcolor) try: self.cmap.auto = False except: pass
[docs] def write_colormap(self, filename): #----------------------------------------------------------------- """ Method to write current colormap rgb values to file on disk. If you add the event handler *interact_imagecolors()*, this method is automatically invoked if you press key 'm'. This method is only useful if the colormap changes i.e. in an interactive environment. :param filename: Name of file to which the current (modified) color look up table (lut) is written. This lut can be imported as a new lut in another session. :type filename: Python stringz """ #----------------------------------------------------------------- #print(self.cmap.name) #print(self.cmap._lut[0:-3,0:3]) a = tabarray(self.cmap._lut[0:-3,0:3]) a.writeto(filename)
[docs] def set_blankcolor(self, blankcolor, alpha=1.0): #----------------------------------------------------------------- """ Set the color of bad pixels. If you add the event handler *interact_imagecolors()*, this method steps through a list of colors for the bad pixels in an image. :param blankcolor: The color of the bad pixels (blanks) in your map :type blankcolor: Matplotlib color :param alpha: Make the color of bad pixels transparent with *alpha < 1* :type alpha: Float in interval [0,1] :Example: >>> annim.set_blankcolor('c') """ #----------------------------------------------------------------- self.cmap.set_bad(blankcolor, alpha) self.blankcol = blankcolor
[docs] def set_aspectratio(self, aspect=1.0): #----------------------------------------------------------------- """ Set the aspect ratio. Overrule the default aspect ratio which corrects pixels that are not square in world coordinates. Can be useful if you want to stretch images for which the aspect ratio doesn't matter (e.g. XV maps). :param aspect: The aspect ratio is defined as *pixel height / pixel width*. With this value one can stretch an image in x- or y direction. The default is such that 1 arcmin in x has the same length in cm as 1 arcmin in y. :type aspect: Float :Example: >>> annim = fitsobj.Annotatedimage(frame) >>> annim.set_aspectratio(1.2) """ #----------------------------------------------------------------- if aspect <= 0.0: raise ValueError("Only aspect ratios > 0 are allowed") self.aspect = abs(aspect) self.frame.set_aspect(aspect=self.aspect, adjustable=self.adjustable, anchor=self.anchor)
def adjustframe(self, frame, adjustable='box'): #----------------------------------------------------------------- """ Method to change the frame for the right aspect ratio and how to react on a resize of the plot window. """ #----------------------------------------------------------------- frame.set_xlim((self.box[0], self.box[1])) # Initialize in case no objects are created frame.set_ylim((self.box[2], self.box[3])) # then we still can navigate with the mouse frame.set_aspect(aspect=self.aspect, adjustable=adjustable, anchor=self.anchor) frame.set_autoscale_on(False) frame.xaxis.set_visible(False) frame.yaxis.set_visible(False) return frame
[docs] def get_colornavigation_info(self): #----------------------------------------------------------------- """ This method compiles and returns a help text for color map interaction. """ #----------------------------------------------------------------- helptext = colornavigation_info() return helptext
[docs] def blur(self, nx, ny=None): #----------------------------------------------------------------- """ Blur the image by convolving with a gaussian kernel of typical size nx (pixels). The optional keyword argument ny allows for a different size in the y direction. nx, ny are the sigma's for the gaussian kernel. """ #----------------------------------------------------------------- if ny is None: ny = nx if self.data is None: # Current data raise Exception("Cannot plot image because image data is not available!") if self.data_blur is None: # Blurred data self.data_blur = numpy.zeros(self.data.shape) # Prevent byte order problems gaussian_filter(self.data_orig, sigma=(nx,ny), order=0, output=self.data_blur, mode='reflect', cval=numpy.nan) # Put NaN's back in map self.data_blur = (self.data_orig-self.data_orig)+self.data_blur
# Slower: self.data_blur = numpy.where(numpy.isfinite(self.data_orig),self.data_blur, numpy.nan)
[docs] def histeq(self, nbr_bins=256): #----------------------------------------------------------------- """ Create a histogram equalized version of the data. The histogram equalized data is stored in attribute *data_hist*. """ #----------------------------------------------------------------- if self.data is None: raise Exception("Cannot plot image because image data is not available!") # Algorithm by Jan Erik Solem im = self.data ix = numpy.isfinite(im) #im = numpy.ma.masked_where(numpy.isfinite(self.data), self.data) #get image histogram imhist,bins = numpy.histogram(im[ix].flatten(), nbr_bins, density=True, range=(self.clipmin, self.clipmax)) cdf = imhist.cumsum() #cumulative distribution function #cdf = 255 * cdf / cdf[-1] #normalize cdf = cdf / cdf[-1] #normalize #use linear interpolation of cdf to find new pixel values im2 = numpy.interp(im.flatten(),bins[:-1],cdf) # Rescale between clip levels of original data m = (self.clipmax-self.clipmin) / (cdf[-1]-cdf[0]) im2 = m * (im2-cdf[0]) + self.clipmin # Ensure that NaN's in the original map are restored in the equalized map # Note that adding a number to a not finite number ends up in a not finite number self.data_hist = (im-im)+im2.reshape(im.shape)
[docs] def Image(self, **kwargs): #----------------------------------------------------------------- """ Setup of an image. This method is a call to the constructor of class :class:`Image` with a default value for most of the keyword parameters. This created object has an attribute *im* which is an instance of Matplotlib's *imshow()* method. This object has a plot method. This method is used by the more general :meth:`Annotatedimage.plot` method. :param kwargs: From the documentation of Matplotlib we learn that for method *imshow()* (used in the plot method if an Image) a few interesting keyword arguments remain: * *interpolation* - From Matplotlib's documentation: Acceptable values are 'none', 'nearest', 'bilinear', 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos' * *visible* - Switch the visibility of the image * *alpha* - Value between 0 and 1 which sets the transparency of the image. :type kwargs: Python keyword parameters :Attributes: .. attribute:: im The object generated after a call to Matplotlib's *imshow()*. :Example: >>> fitsobject = maputils.FITSimage(promptfie=maputils.prompt_fitsfile) >>> annim = fitsobject.Annotatedimage() >>> annim.Image(interpolation="spline36") or create an image but make it invisible: >>> annim.Image(visible=False) """ #----------------------------------------------------------------- if self.image is not None: raise Exception("Only 1 image allowed per Annotatedimage object") image = Image(self.data, self.box, self.cmap, self.norm, **kwargs) self.objlist.append(image) self.image = image return image
[docs] def RGBimage(self, f_red, f_green, f_blue, fun=None, **kwargs): #----------------------------------------------------------------- """ Matplotlib's method imshow() is able to produce RGB images. To create a real RGB image, we need three arrays with identical shape representing the red, green and blue components. Method imshow() requires data scaled between 0 and 1. This utility method prepares a composed and scaled data array derived from three FITSimage objects. It scales the composed array and not the individual image arrays. The method allows for a function or lambda expression to be entered to process the scaled data. The world coordinate system (e.g. to plot graticules) is copied from the current :class:`Annotatedimage` object. Note that for the three images only the shape of the array must be equal to the shape of the data of the current :class:`Annotatedimage` object. :param f_red: This object describes a two dimensional data structure which represents the red part of the composed image. :type f_red: Object from class :class:`FITSimage` :param f_green: This object describes a two dimensional data structure which represents the green part of the composed image. :type f_green: Object from class :class:`FITSimage` :param f_blue: This object describes a two dimensional data structure which represents the blue part of the composed image. :type f_blue: Object from class :class:`FITSimage` :param fun: A function or a Lambda expression to process the scaled data. :type fun: Function or Lambda expression :param kwargs: See description method :meth:`Annotatedimage.Image`. :type kwargs: Python keyword parameters :Note: A RGB image does not interact with a colormap. Interacting with a colormap (e.g. after adding annim.interact_imagecolors() in the example below) is not forbidden but it gives weird results. To rescale the data, for instance for a better view of interesting data, you need to enter a function or Lambda expression with parameter *fun*. :Example: :: from kapteyn import maputils from numpy import sqrt from matplotlib import pyplot as plt f_red = maputils.FITSimage('m101_red.fits') f_green = maputils.FITSimage('m101_green.fits') f_blue = maputils.FITSimage('m101_blue.fits') fig = plt.figure() frame = fig.add_subplot(1,1,1) annim = f_red.Annotatedimage(frame) annim.RGBimage(f_red, f_green, f_blue, fun=lambda x:sqrt(x), alpha=0.5) grat = annim.Graticule() annim.interact_toolbarinfo() maputils.showall() """ #----------------------------------------------------------------- if self.image is not None: raise Exception("Only 1 image allowed per Annotatedimage object") if f_red.boxdat.shape != self.data.shape: raise Exception("Shape of red image is not equal to shape of Annotatedimage object!") if f_green.boxdat.shape != self.data.shape: raise Exception("Shape of green image is not equal to shape of Annotatedimage object!") if f_blue.boxdat.shape != self.data.shape: raise Exception("Shape of blue image is not equal to shape of Annotatedimage object!") # Compose a new array. Note that this syntax implies a real copy of the data. rgbim = numpy.zeros((self.data.shape+(3,))) rgbim[:,:,0] = f_red.boxdat rgbim[:,:,1] = f_green.boxdat rgbim[:,:,2] = f_blue.boxdat # Scale the composed array to values between 0 and 1 dmin = rgbim.min() dx = rgbim.max() - dmin if dx == 0.0: dx = 1.0 rgbim = (rgbim - dmin)/dx # Apply the function or lambda expression if any is given, to # rescale the data. if not fun is None: rgbim = fun(rgbim) image = Image(rgbim, self.box, self.cmap, self.norm, **kwargs) self.objlist.append(image) self.image = image # Set a flag to indicate that this Annotatedimage object has a rgb # data as image. We store the FITSimage objects as an attribute. # We need this data later if we wnat to display the image values of the # three maps in an informative message. self.rgbs = (f_red.boxdat, f_green.boxdat, f_blue.boxdat) return image
[docs] def Contours(self, levels=None, cmap=None, colors=None, filled=False, **kwargs): #----------------------------------------------------------------- """ Setup of contour lines. This method is a call to the constructor of class :class:`Contours` with a number of default parameters. Either it plots single contour lines or a combination of contour lines and filled regions between the contours. The colors are taken from the current colormap. :param levels: Image values for which contours must be plotted. The default is *None* which results in a list with values calculated by the Contour constructor. :type levels: None or a list with floating point numbers :param cmap: A colormap from class :class:`mplutil.VariableColormap` or a string that represents a colormap (e.g. 'jet', 'spectral' etc.). It sets the contour colours. :type cmap: mplutil.VariableColormap instance or string :param colors: A sequence with Matplotlib colors, e.g. ['r','g','y'] :type colors: Sequence with strings :param filled: if set to True the area between two contours will get a color (close to the color of the contour. :type filled: Boolean :param kwargs: There are a number of keyword arguments that are useful: * negative - one of Matplotlib's line styles 'solid', 'dashed', 'dashdot', 'dotted' in which contours are plotted which represent negative image values. * *colors* - If None, the current colormap will be used. If a character or string, all levels will be plotted in this color. If a tuple of matplotlib colors then different levels will be plotted in different colors in the order specified. * *linewidths* - If a number, all levels will be plotted with this linewidth. If a tuple, different levels will be plotted with different linewidths in the order specified * *linestyles* - One of 'solid', 'dashed', 'dashdot', 'dotted', which sets the style of the contour. It can also be given in a list. See the Matplotlib documentation for its behaviour. :type kwargs: Python keyword parameters :Methods: * :meth:`Contours.setp_contour` - Set properties for individual contours * :meth:`Contours.setp_label` - Plot labels for individual contours :Examples: >>> fitsobj = maputils.FITSimage("m101.fits") >>> annim = fitsobj.Annotatedimage() >>> annim.Image(alpha=0.5) >>> cont = annim.Contours() >>> print("Levels=", cont.clevels) Levels= [ 4000. 6000. 8000. 10000. 12000. 14000.] >>> annim.Contours(filled=True) In the next example note the plural form of the standard Matplotlib keywords. They apply to all contours: >>> annim.Contours(colors='w', linewidths=2) Set levels and the line style for negative contours: >>> annim.Contours(levels=[-500,-300, 0, 300, 500], negative="dotted") A combination of keyword parameters with less elements than the number of contour levels: >>> cont = annim.Contours(linestyles=('solid', 'dashed', 'dashdot', 'dotted'), linewidths=(2,3,4), colors=('r','g','b','m')) Example of setting of properties for all and 1 contour with *setp_contour()*: >>> cont = annim.Contours(levels=list(range(10000,16000,1000))) >>> cont.setp_contour(linewidth=1) >>> cont.setp_contour(levels=11000, color='g', linewidth=3) Plot a (formatted) label near a contour with *setp_label()*: >>> cont2 = annim.Contours(levels=(8000,9000,10000,11000)) >>> cont2.setp_label(11000, colors='b', fontsize=14, fmt="%.3f") >>> cont2.setp_label(fontsize=10, fmt="$%g \lambda$") """ #----------------------------------------------------------------- # No colormap is given and no contour colors. Then choose one color for all. # It is not useful to set cmap to self.cmap here because the contours # are not visible if the colormaps are the same. if cmap is None and colors is None: colors=['r'] if cmap and colors: # Both are defined, we must make a choice cmap = None contourset = Contours(self.data, self.box, levels, cmap=cmap, norm=self.norm, colors=colors, filled=filled, **kwargs) self.objlist.append(contourset) self.contourset = contourset return contourset
def writecontours_file(self, filename=None): #--------------------------------------------------------------------- """ Get info about the contours that are plotted and write their vertices to a text file on disk. The contours in the contour set are processed in order of the given contour levels. Per level, all the closed contours are processed in unknown order. For each contour the vertices are written to the file given by keyword parameter filename= There are various situations where it is useful to have this data. For example, it can be used to contruct masks or to analyze the shape of a contour or to calculate the flux if you know the size of a pixel. The contents of the file on disk is something similar to this: ! Level: 10000, contour number 0 area=20 sum=234302 -132.000000 -72.255098 -131.043290 -72.000000 .... ! Level: 10000, contour number 1 area=141 sum=1.87093e+06 -142.000000 -68.235209 -141.000000 -68.235209 .... :param filename: Name of file on disk where the vertices of all contours are written. By default the name is set to: contour_info_xxxxx.txt where xxxxx contains data and time of creation. :type filename: String :returns: None, or a tuple with filename and current directory """ #--------------------------------------------------------------------- if not self.contourSet: return # Do we need to create a filename? if not filename: stamp = datetime.now().strftime("%d%m%Y_%H%M%S") filename = "visions_contour_info" + "_" + stamp + ".txt" contourSet = self.contourSet gridmode = self.gridmode fp = open(filename, 'w') allcollections = contourSet.collections alllevels = contourSet.levels for levnr, collection in enumerate(allcollections): allpaths = collection.get_paths() cnr = 0 for path in allpaths: verts = path.vertices flux = self.getflux(verts) fp.write("! Level: %g, contour number %d area=%g sum=%g\n"%(alllevels[levnr], cnr, flux[0], flux[1])) # Convert to grids only in gridmode if gridmode: xygrid = self.projection.pixel2grid(verts) else: xygrid = verts for xco, yco in xygrid: s = "%12f %12f\n"%(xco, yco) fp.write(s) cnr += 1 fp.close() return filename, getcwd()
[docs] def Colorbar(self, frame=None, clines=False, **kwargs): #----------------------------------------------------------------- """ This method is a call to the constructor of class :class:`Colorbar` with a number of default parameters. A color bar is an image which represents the current color scheme. It annotates the colors with image values so that it is possible to get an idea of the distribution of the values in your image. :param frame: By default a colorbar will 'steal' some space from its parent frame but this behaviour can be overruled by setting an explicit frame (Matplotlib Axes object). :type frame: Matplotlib Axes object :param clines: If set to true AND a contour set (an :meth:`Annotatedimage.Contours` object) is available, then lines will be plotted in the colorbar at positions that correspond to the contour levels :type clines: Boolean :param kwargs: Specific keyword arguments and Keyword arguments for Matplotlib's method *ColorbarBase()* * *label* - A text that will be plotted along the long axis of the colorbar. * *linewidths* - One number that sets the line width of all the contour lines in the colorbar. * *location* - The location of the colorbar can be one of 'left', 'bottom', 'top' or 'right'. The keyword argument 'orientation' should be in agreement with the location. The arguments 'location', 'size' and 'pad' are the three arguments for method :meth:`mpl_toolkits.axes_grid1.make_axes_locatable.divider.append_axes()`. * *size*: Size (e.g. "5%") of colorbar in percentage of mother axes object (e.g. image). * *pad* Pad between image axes and colorbar axes From Matplotlib: * *orientation* - 'horizontal' or 'vertical' * *fontsize* - Size of numbers along the colorbar * *ticks* - Levels which are annotated along the colorbar * *visible* - Make image in colorbar invisible :type kwargs: Python keyword arguments :methods: :meth:`Colorbar.set_label` - Plot a title along the long side of the colorbar. :Examples: A basic example were the font size for the ticks are set: >>> fitsobj = maputils.FITSimage("m101.fits") >>> annim = fitsobj.Annotatedimage(cmap="spectral") >>> annim.Image() >>> colbar = annim.Colorbar(fontsize=8) >>> annim.plot() >>> plt.show() Set frames for Image and Colorbar: >>> frame = fig.add_axes((0.1, 0.2, 0.8, 0.8)) >>> cbframe = fig.add_axes((0.1, 0.1, 0.8, 0.1)) >>> annim = fitsobj.Annotatedimage(cmap="Accent", clipmin=8000, frame=frame) >>> colbar = annim.Colorbar(fontsize=8, orientation='horizontal', frame=cbframe) Create a title for the colorbar and change its font size: >>> units = r'$ergs/(sec.cm^2)$' >>> colbar.set_label(label=units, fontsize=24) Using location, pad and size: >>> from kapteyn import maputils >>> from matplotlib import pyplot as plt >>> fitsobj = maputils.FITSimage("m101.fits") >>> mplim = fitsobj.Annotatedimage(cmap="Spectral") >>> mplim.Image() >>> units = r'$ergs/(sec.cm^2)$' >>> colbar = mplim.Colorbar(location='left', pad=0, size="2%", fontsize=8) >>> colbar.set_label(label=units, fontsize=10, rotation=-90, labelpad=-40) >>> mplim.plot() >>> plt.show() """ #------------------------------------------------------------------ if self.colorbar is not None: raise Exception("Only 1 colorbar allowed per Annotatedimage object") colorbar = Colorbar(self.cmap, frame=frame, norm=self.norm, contourset=self.contourset, clines=clines, parentframe=self.frame, **kwargs) self.objlist.append(colorbar) self.colorbar = colorbar return colorbar
[docs] def Graticule(self, visible=True, pxlim=None, pylim=None, **kwargs): #----------------------------------------------------------------- """ This method is a call to the constructor of class :class:`wcsgrat.Graticule` with a number of default parameters. It calculates and plots graticule lines of constant longitude or constant latitude. The description of the parameters is found in :class:`wcsgrat.Graticule`. An extra parameter is *visible*. If visible is set to False than we can plot objects derived from this class such as 'Rulers' and 'Insidelabels' without plotting unwanted graticule lines and labels. :Methods: * :meth:`kapteyn.wcsgrat.Graticule.Ruler` * :meth:`kapteyn.wcsgrat.Graticule.Insidelabels` Other parameters such as *hdr*, *axperm*, *pxlim*, *pylim*, *mixpix*, *skyout* and *spectrans* are set to defaults in the context of this method and should not be overwritten. :Examples: >>> fitsobj = maputils.FITSimage('m101.fits') >>> annim = fitsobj.Annotatedimage() >>> grat = annim.Graticule() >>> annim.plot() >>> plt.show() Set the range in world coordinates and set the positions for the labels with (X, Y): >>> X = list(arange(0,360.0,15.0)) >>> Y = [20, 30,45, 60, 75, 90] >>> grat = annim.Graticule(wylim=(20.0,90.0), wxlim=(0,360), startx=X, starty=Y) Add a ruler, based on the current Annotatedimage object: >>> ruler3 = annim.Ruler(23*15,30,22*15,15, 0.5, 1, world=True, fmt=r"$%4.0f^\prime$", fun=lambda x: x*60.0, addangle=0) >>> ruler3.setp_labels(color='r') Add world coordinate labels inside the plot. Note that these are derived from the current Graticule object. >>> grat.Insidelabels(wcsaxis=0, constval=-51, rotation=90, fontsize=10, color='r', ha='right') """ #----------------------------------------------------------------- class Gratdata(object): def __init__(self, hdr, axperm, wcstypes, pxlim, pylim, mixpix, skyout, spectrans, alter): self.hdr = hdr self.axperm = axperm self.pxlim = pxlim self.pylim = pylim self.mixpix = mixpix self.skyout = skyout self.spectrans = spectrans self.alter = alter self.wcstypes = wcstypes # One can overrule the pixel limits of the Annotatedimage object if pxlim is None: pxlim = self.pxlim if pylim is None: pylim = self.pylim gratdata = Gratdata(self.hdr, self.axperm, self.wcstypes, pxlim, pylim, self.mixpix, self.skyout, self.spectrans, self.alter) graticule = wcsgrat.Graticule(graticuledata=gratdata, **kwargs) graticule.visible = visible # A new attribute only for this context self.objlist.append(graticule) return graticule
[docs] def Minortickmarks(self, graticule, partsx=10, partsy=10, **kwargs): #------------------------------------------------------------- """ Drawing minor tick marks is as easy or as difficult as finding the positions of major tick marks. Therefore we decided that the best way to draw minor tick marks is to calculate a new (but invisible) graticule object. Only the tick marks are visible. :param graticule: Graticule object from which we change the step size between tick marks to create a new Graticule object for which most components (graticule lines, labels, ...) are made invisible. :type graticule: Object from class :class:`wcsgrat.Graticule` :param partsx: Divide major tick marks in this number of parts. This method forces this number to be an integer between 2 and 20 If the input is None then nothing is plotted. For example for maps with only one spatial axis, one can decide to plot tick marks for only one of the axes. :type partsx: Integer or None :param partsx: See description for parameter *partsx* :type partsx: Integer or None :param kwargs: Parameters for changing the attributes of the tick mark symbols. Useful keywords are *color*, *markersize* and *markeredgewidth*. :type kwargs: Matplotlib keyword arguments related to a Line2D object. :Notes: Minor tick marks are also plotted at the positions of major tick marks. By default this will not be visible. It is visible if you user a longer marker size, a different color or a marker with increased width. The default marker size is set to 2. :Returns: This method returns a graticule object. Its properties can be changed in the calling environment with the appropriate methods (e.g. :meth:`kapteyn.wcsgrat.Graticule.setp_tickmark`). :Examples: :: from kapteyn import maputils from matplotlib import pyplot as plt fitsobj = maputils.FITSimage("m101.fits") mplim = fitsobj.Annotatedimage() grat = mplim.Graticule() grat2 = grat.minortickmarks() mplim.plot() plt.show() Adding parameters to change attributes: >>> grat2 = grat.minortickmarks(3, 5, color="#aa44dd", markersize=3, markeredgewidth=2) Minor tick marks only along x axis: >>> grat2 = minortickmarks(grat, 3, None) """ #------------------------------------------------------------- # Get the position of the first label in the original graticule startx = graticule.xstarts[0] starty = graticule.ystarts[1] skyout = graticule.skyout spectrans=graticule.spectrans wxlim = graticule.wxlim wylim = graticule.wylim # Separate the kwargs to be able to set just one of them # to invisible (partsx/y is None) if not ('markersize' in kwargs): kwargs.update(markersize=2) kwargs1 = kwargs.copy() kwargs2 = kwargs.copy() # Get the steps along the axes deltax = graticule.delta[0] deltay = graticule.delta[1] # Adjust the steps. if partsx is None: deltax = None kwargs1.update(visible=False) else: deltax /= float(max(2,min(20,int(abs(partsx))))) if partsy is None: deltay = None kwargs2.update(visible=False) else: deltay /= float(max(2,min(20,int(abs(partsy))))) # Build the new graticule minorgrat = self.Graticule(startx=startx, starty=starty, deltax=deltax, deltay=deltay, wxlim=wxlim, wylim=wylim, skyout=skyout, spectrans=spectrans) # Make unnecessary elements invisible minorgrat.setp_gratline(wcsaxis=[0,1], visible=False) minorgrat.setp_ticklabel(wcsaxis=[0,1], visible=False) minorgrat.setp_axislabel(plotaxis=[0,1,2,3], visible=False) # Change attributes of tick mark (i.e. a Matplotlib Line2D object) minorgrat.setp_tickmark(wcsaxis=0, **kwargs1) minorgrat.setp_tickmark(wcsaxis=1, **kwargs2) # Return the graticule so that its attributes can also be changed with # methods in the calling environment. # Note that there is need to store the new object because # method Graticule() has aready done that. return minorgrat
[docs] def Pixellabels(self, **kwargs): #----------------------------------------------------------------- """ This method is a call to the constructor of class :class:`Pixellabels` with a number of default parameters. It sets the annotation along a plot axis to pixel coordinates. :param plotaxis: The axis name of one or two of the axes of the plot rectangle: or 'left', 'bottom', 'right', 'top' Combinations are always between 'left' and 'bottom' and 'right' and 'top'. :type plotaxis: String :param markersize: Set size of ticks at pixel positions. The size can be negative to get tick marks that point outwards. :type markersize: Integer :param gridlines: Set plotting of grid lines (connected tick marks) on or off (True/False). The default is off. :type gridlines: Boolean :param offset: The pixels can have an integer offset. If you want the reference pixel to be pixel 0 then supply offset=(crpixX, crpixY). These crpix values are usually read from then header (e.g. as CRPIX1 and CRPIX2). In this routine the nearest integer of the input is calculated to ensure that the offset is an integer value. :type offset: *None* or floating point numbers :param xxx: Other parameters are related to Matplotlib label attributes. :Examples: >>> fitsobject = maputils.FITSimage("m101.fits") >>> annim = fitsobject.Annotatedimage() >>> annim.Pixellabels(plotaxis=("top","right"), color="b", markersize=10) or separate the labeling so that you can give different properties for different axes. In this case we shift the labels along the top axis towards the axis line with *va='top'*: >>> annim.Pixellabels(plotaxis='top', va='top') >>> annim.Pixellabels(plotaxis='right') :Notes: If a pixel offset is given for this Annimated object, then plot the pixel labels with this offset. """ #----------------------------------------------------------------- pixlabels = Pixellabels(self.pxlim, self.pylim, offset=self.pixoffset, **kwargs) self.objlist.append(pixlabels) return pixlabels
[docs] def Beam(self, major, minor, pa=0.0, pos=None, xc=None, yc=None, units=None, **kwargs): #----------------------------------------------------------------- """ Objects from class Beam are graphical representations of the resolution of an instrument. The beam is centered at a position xc, yc. The major axis of the beam is the FWHM of the longest distance between two opposite points on the ellipse. The angle between the major axis and the North is the position angle. A beam is an ellipse in world coordinates. To draw a beam given the input parameters, points are calculated in world coordinates so that angle and required distance of sample points on the ellipse are correct on a sphere. :param major: Full width at half maximum of major axis of beam in degrees. :type major: Float :param minor: Full width at half maximum of minor axis of beam in degrees. :type minor: Float :param pa: Position angle in degrees. This is the angle between the positive y-axis and the major axis of the beam. The default value is 0.0. :type pa: Float :param pos: A string that represents the position of the center of the beam. If two numbers are available then one can also use parameters *xc* and *yc*. The value in parameter *pos* supersedes the values in *xc* and *yc*. :type pos: String :param xc: X value in **world** coordinates of the center position of the beam. :type xc: Float :param yc: Y value in **world** coordinates of the center position of the beam. :type yc: Float :Examples: :: fwhm_maj = 0.08 fwhm_min = 0.06 lat = 54.347395233845 lon = 210.80254413455 beam = annim.Beam(fwhm_maj, fwhm_min, 90, xc=lon, yc=lat, fc='g', fill=True, alpha=0.6) pos = '210.80254413455 deg, 54.347395233845 deg' beam = annim.Beam(fwhm_maj, fwhm_min, pos=pos, fc='m', fill=True, alpha=0.6) pos = '14h03m12.6105s 54d20m50.622s' beam = annim.Beam(fwhm_maj, fwhm_min, pos=pos, fc='y', fill=True, alpha=0.6) pos = 'ga 102.0354152 {} 59.7725125' beam = annim.Beam(fwhm_maj, fwhm_min, pos=pos, fc='g', fill=True, alpha=0.6) pos = 'ga 102d02m07.494s {} 59.7725125' beam = annim.Beam(fwhm_maj, fwhm_min, pos=pos, fc='b', fill=True, alpha=0.6) pos = '{ecliptic,fk4, j2000} 174.3674627 {} 59.7961737' beam = annim.Beam(fwhm_maj, fwhm_min, pos=pos, fc='r', fill=True, alpha=0.6) pos = '{eq, fk4-no-e, B1950} 14h01m26.4501s {} 54d35m13.480s' beam = annim.Beam(fwhm_maj, fwhm_min, pos=pos, fc='c', fill=True, alpha=0.6) pos = '{eq, fk4-no-e, B1950, F24/04/55} 14h01m26.4482s {} 54d35m13.460s' beam = annim.Beam(fwhm_maj, fwhm_min, pos=pos, fc='c', fill=True, alpha=0.6) pos = '{ecl} 174.367764 {} 59.79623457' beam = annim.Beam(fwhm_maj, fwhm_min, pos=pos, fc='c', fill=True, alpha=0.6) pos = '53 58' # Pixels beam = annim.Beam(0.04, 0.02, pa=30, pos=pos, fc='y', fill=True, alpha=0.4) pos = '14h03m12.6105s 58' # World coordinate and a pixel coordinate beam = annim.Beam(0.04, 0.02, pa=-30, pos=pos, fc='y', fill=True, alpha=0.4) # Using system of pixels/grids: use units='pixels' # Note that for the use of pos= nothing changes. Only ellipse axes are # in pixels and the position angle is wrt. the X axis. beam(9, 3, pos='10 10', pa=0, units='pixels', fc='y', hatch='/', alpha=0.4) # Make hatching more dense: beam(9, 3, pos='10 10', pa=0, units='pixels', fc='y', hatch='////', alpha=0.4) # Note the use of an escape character for the hatch symbol beam(9, 3, pos='pc pc', pa=0, units='PIX', fc='m', hatch='\\', alpha=0.4) # Note that the center of the ellipse can also be entered with xc and yc # which prevents parsing by str2pos(). beam(9, 3, xc=10, yc=10, pa=40, units='pix', fc='c', hatch='x', alpha=0.4) :Properties: A selection of keyword arguments for the beam (which is a Matplotlib :class:`Polygon` object) are: * alpha - float (0.0 transparent through 1.0 opaque) * color - Matplotlib color arg or sequence of rgba tuples * edgecolor or ec - Matplotlib color spec, or None for default, or 'none' for no color * facecolor or fc - Matplotlib color spec, or None for default, or 'none' for no color * linestyle or ls - ['solid' | 'dashed' | 'dashdot' | 'dotted'] * linewidth or lw - float or None for default * hatch - ['/' | '\' | '|' | '-' | '+' | 'x' | 'o' | 'O' | '.' | '*'] """ #----------------------------------------------------------------- if units: # Could be pixels/grids grids = units.upper().startswith('PIX') else: grids = False if pos is not None: poswp = str2pos(pos, self.projection, mixpix=self.mixpix, gridmode=self.gridmode) if poswp[3] != "": raise Exception(poswp[3]) if grids: world = poswp[1][0] # Only first element of the parsed position is used else: world = poswp[0][0] else: world = (xc, yc) if grids: world = self.projection.grid2pixel(world) spatials = [self.projection.lonaxnum, self.projection.lataxnum] spatialmap = self.axperm[0] in spatials and self.axperm[1] in spatials if not spatialmap: raise Exception("Can only plot a beam in a spatial map") beam = Beam(world[0], world[1], major, minor, pa, projection=self.projection, units=units, **kwargs) self.objlist.append(beam) return beam
[docs] def Skypolygon(self, prescription=None, xc=None, yc=None, cpos=None, major=None, minor=None, nangles=6, pa=0.0, units=None, lons=None, lats=None, stepsize=1.0, **kwargs): #----------------------------------------------------------------- """ Construct an object that represents an area in the sky. Usually this is an ellipse, rectangle or regular polygon with given center and other parameters to define its size or number of angles and the position angle. The object is plotted in a way that the sizes and angles, as defined on a sphere, are preserved. The objects need a 'prescription'. This is a recipe to calculate a distance to a center point (0,0) as function of an angle in a linear and flat system. Then the object perimeter is re-calculated for a given center (xc,yc) and for corresponding angles and distances on a sphere. If *prescription=None*, then this method expects two arrays *lons* and *lats*. These are copied unaltered as vertices for an irregular polygon. The coordinates are expected to be grids (e.g. for a GIPSY set) and are converted to pixels first. For cylindrical projections it is possible that a polygon in a all sky plot crosses a boundary (e.g. 180 degrees longitude if the projection center is at 0 degrees). Then the object is splitted into two parts one for the region 180-phi and one for the region 180+phi where phi is an arbitrary positive angle. This splitting is done for objects with and without a prescription. :param prescription: How should the polygon be created? The prescriptions are "ellipse", "rectangle", "npolygon" or None. This method only checks the first character of the string. :type prescription: String or None :param xc: Coordinate in degrees to set the center of the shape in X :type xc: Floating point number :param yc: Coordinate in degrees to set the center of the shape in Y :type yc: Floating point number :param cpos: Instead of a position in world coordinates (xc, yc), supply a string with a position. The syntax is described in the positions module. For example: ``cpos='20h30m10s -10d10m20.23s`` or ``cpos=ga 110.3 ga 33.4`` :type cpos: String :param major: Major axis of ellipse in degrees. This parameter is also used as height for rectangles and as radius for regular polygons. :type major: Floating point number :param minor: Minor axis of ellipse in degrees. This parameter is also used as width for rectangles. If the prescription is an ellipse then a circle is defined if *major*=*minor* :type minor: Floating point number :param nangles: The number of angles in a regular polygon. The radius of this shape is copied from parameter *major*. :type nangles: Integer :param pa: Position angle. This is an astronomical angle i.e. with respect to the north in the direction of the east. For an ellipse the angle is between the north and the major axis. For a rectangle it is the angle between the north and the parameter that represents the height. For a regular polygon, it is the angle between the north and the line that connects the center with the first angle in the polygon. :type pa: Floating point number :param units: A case insensitive minimal matched string that sets the units for the values in *major* and *minor* (e.g. arcmin, arcsec). :type units: String :param lons: Sequence with longitudes in degrees that (together with matching latitudes) are used to define the vertices of a polygon. If nothing is entered for *prescription* or *prescription=None* then these positions are used unaltered. :type lons: Sequence of floating point numbers. :param lats: See description at *lons*. :type lats: Sequence of floating point numbers. :param stepsize: Step in degrees to calculate samples (applies to ellipse only) :type stepsize: Floating point number :param kwargs: Plot parameters :type kwargs: Matplotlib keyword arguments :Examples: :: annim.skypolygon('ellipse', cpos='10 20',major=3.50, minor=1.50, units='arcmin',fc='g', alpha=0.3, lw=3, ec='r')", annim.skypolygon('rectangle', cpos='-51.1898431 deg 60.1476405 deg', major=3.50, minor=1.50, units='arcmin',fc='g', pa=-20, alpha=0.3, lw=10, ec='r')", annim.skypolygon(lons=[10,20,30], lats=[2,4,3], units='PIX') """ #----------------------------------------------------------------- if cpos is not None: poswp = str2pos(cpos, self.projection, mixpix=self.mixpix, gridmode=self.gridmode) if poswp[3] != "": raise Exception(poswp[3]) world = poswp[0][0] # Only first element is used else: world = (xc, yc) spatials = [self.projection.lonaxnum, self.projection.lataxnum] spatialmap = self.axperm[0] in spatials and self.axperm[1] in spatials if not spatialmap: raise Exception("Can only plot a sky polygon in a spatial map") if prescription is None and not (units is None): if units.upper().startswith('PIX'): # User/programmer wants a polygon in grids (so we need to convert # the grids to pixels) lons, lats = self.projection.grid2pixel((lons,lats)) spoly = Skypolygon(projection=self.projection, prescription=prescription, xc=world[0], yc=world[1], major=major, minor=minor, nangles=nangles, pa=pa, units=units, lons=lons, lats=lats, stepsize=stepsize, **kwargs) self.objlist.append(spoly) return spoly
[docs] def Marker(self, pos=None, x=None, y=None, mode='', **kwargs): #----------------------------------------------------------------- """ Plot marker symbols at given positions. This method creates objects from class Marker. The constructor of that class needs positions in pixel coordinates. Here we allow positions to be defined in a string which can contain either world- or pixel coordinates (or a mix of both). If x and y coordinates are known, or read from file, one can also enter this data without parsing. The keyword arguments *x* and *y* can be used to enter pixel coordinates or world coordinates. :param pos: A definition of one or more positions for the current image. The string is parsed by :mod:`positions`. :type pos: String :param x: If keyword argument *pos* is not used, then this method expects numbers in parameters *x* and *y*. Advantage of using this parameter, is that it skips the position parser and therefore it is much faster. :type x: Float or a sequence of floating point numbers :param y: If keyword argument *pos* is not used, then this method expects numbers in parameters *x* and *y* :type y: Float or a sequence of floating point numbers :param mode: Flag to set the conversion mode. If the input string starts with 'w' od 'W' then the numbers in *x* and *y* are world coordinates. Else, if the string starts with 'p' or 'P', they are processed as pixel coordinates. :type mode: String :Returns: Object from class :class:`Marker` :Examples: In the first example we show 4 markers plotted in the projection center (given by header values CRPIX):: f = maputils.FITSimage("m101.fits") fig = plt.figure() frame = fig.add_subplot(1,1,1) annim = f.Annotatedimage(frame, cmap="binary") annim.Image() grat = annim.Graticule() annim.Marker(pos="pc", marker='o', markersize=10, color='r') annim.Marker(pos="ga 102.035415152 ga 59.772512522", marker='+', markersize=20, markeredgewidth=2, color='m') annim.Marker(pos="{ecl,fk4,J2000} 174.367462651 {} 59.796173724", marker='x', markersize=20, markeredgewidth=2, color='g') annim.Marker(pos="{eq,fk4-no-e,B1950,F24/04/55} 210.360200881 {} 54.587072397", marker='o', markersize=25, markeredgewidth=2, color='c', alpha=0.4) In the second example we show how to plot a sequence of markers. Note the use of the different keyword arguments and the role of flag *world* to force the given values to be processed in pixel coordinates:: # Use pos= keyword argument to enter sequence of # positions in pixel coordinates pos = "[200+20*sin(x/20) for x in range(100,200)], list(range(100,200))" annim.Marker(pos=pos, marker='o', color='r') # Use x= and y= keyword arguments to enter sequence of # positions in pixel coordinates xp = [400+20*numpy.sin(x/20.0) for x in range(100,200)] yp = list(range(100,200)) annim.Marker(x=xp, y=yp, mode='pixels', marker='o', color='g') # Single position in pixel coordinates annim.Marker(x=150, y=150, mode='pixels', marker='+', color='b') In the next example we show how to use method :meth:`positionsfromfile` in combination with this Marker method to read positions from a file and to plot them. The positions in the file are world coordinates. Method :meth:`positionsfromfile` returns pixel coordinates:: fn = 'smallworld.txt' xp, yp = annim.positionsfromfile(fn, 's', cols=[0,1]) annim.Marker(x=xp, y=yp, mode='pixels', marker=',', color='b') """ #----------------------------------------------------------------- if x is None and y is None and pos is None: # Nothing to do return None # For parameters *x* and *y* a *mode* should be given if pos is None: p = mode.upper().startswith('P') if p: w = False else: w = mode.upper().startswith('W') if not p and not w: raise Exception("Marker(): Mode not or incorrectly specified!") else: world = w if (x is None and y is not None) or (x is not None and y is None): raise Exception("Marker(): One of the arrays is None and the other is not!") if not (pos is None) and (not (x is None) or not (y is None)): raise Exception("Marker(): You cannot enter values for both pos= and x= and/or y=") if not pos is None: poswp = str2pos(pos, self.projection, mixpix=self.mixpix, gridmode=self.gridmode) if poswp[3] != "": raise Exception(poswp[3]) xp = poswp[1][:,0] yp = poswp[1][:,1] else: if world: xp, yp = self.topixel(x, y) else: xp, yp = x, y markers = Marker(xp, yp, **kwargs) self.objlist.append(markers) return markers
[docs] def Gridplot(self, gridsX, gridsY, **kwargs): #----------------------------------------------------------------- """ Use Matplotlib's 'plot' function for the curent grid coordinate system. All parameters are grid coordinates and depending on the header values of CRPIX, they are converted to pixel coordinates (1..N) and then plotted by Matplotlib's plot() function. :param gridsX: Sequence of X values in grids :type gridsX: Sequence of numbers, similar to what you enter in Matplotlib's plot() function :param gridsY: Sequence of Y values in grids :type gridsY: Sequence of numbers, similar to what you enter in Matplotlib's plot() function :param kwargs: All keyword arguments are transferred to Matplotlib's plot() function. Examples are found in e.g.: https://matplotlib.org/stable/api/_as_gen/matplotlib.lines.Line2D.html#matplotlib.lines.Line2D (alpha, color, linestype etc.) :type kwargs: Dictionary with keyword arguments :Returns: Object from class :class:`Gridplot` :Examples: :: f = maputils.FITSimage("m101.fits") fig = plt.figure() frame = fig.add_subplot(1,1,1) annim = f.Annotatedimage(frame, cmap="binary") annim.Image() annim.Gridplot([0,10,5], [3,4,6], color='r') """ #----------------------------------------------------------------- # The coordinates enter as grids and convert to pixels in function # gridplot xp, yp = self.projection.grid2pixel((gridsX,gridsY)) lines = Gridplot(xp, yp, **kwargs) self.objlist.append(lines) return lines
[docs] def Ruler(self, pos1=None, pos2=None, x1=None, y1=None, x2=None, y2=None, lambda0=0.5, step=None, world=False, angle=None, addangle=0.0, fmt=None, fun=None, fliplabelside=False, mscale=None, labelsintex=True, **kwargs): #----------------------------------------------------------------- """ This method prepares arguments for a call to function :func:`kapteyn.rulers.Ruler` in module :mod:`rulers` Note that this method sets a number of parameters which cannot be changed like *projection*, *mixpix*, *pxlim*, *pylim* and *aspectratio*, which are all derived from the properties of the current :class:`maputils.Annotatedimage` object. """ #----------------------------------------------------------------- ruler = rulers.Ruler(self.projection, self.mixpix, self.pxlim, self.pylim, aspectratio=self.aspect, pos1=pos1, pos2=pos2, x1=x1, y1=y1, x2=x2, y2=y2, lambda0=lambda0, step=step, world=world, angle=angle, addangle=addangle, fmt=fmt, fun=fun, fliplabelside=fliplabelside, mscale=mscale, labelsintex=labelsintex, gridmode=self.gridmode, **kwargs) self.objlist.append(ruler) return ruler
[docs] def plot(self): #----------------------------------------------------------------- """ Plot all objects stored in the objects list for this Annotated image. :Example: >>> fitsobj = maputils.FITSimage('m101.fits') >>> annim = fitsobj.Annotatedimage() >>> grat = annim.Graticule() >>> annim.plot() >>> plt.show() Plotting in this context implies that we call the *plot()* methods of all the objects found in this Annotatedimage object. The methods require an Axes object. That is usually trivial but Colorbar objects derive their Axes from a 'mother' Axes object. We use the AxesDivider technique to make the new Axes and Colorbar Axes relocatable. This keeps height and width of the Colorbar Axes in sync with the mother Axes. We copy the Bbox properties from the new Axes to the other existing Axes objects in a resize callback. This callback is always invoked at least once. """ #----------------------------------------------------------------- # Before locatable Axes, we used: self.cbframe = make_axes(self.frame, orientation=orientation)[0] for obj in self.objlist: try: pt = obj.ptype except: raise Exception("Unknown object. Cannot plot this!") if pt in ["Image", "Contour", "Graticule", "Insidelabels", "Pixellabels", "Beam", "Marker", "Ruler", "Skypolygon", "Gridplot"]: try: visible = obj.visible except: visible = True obj.plot(self.frame) # If we want to plot derived objects (e.g. ruler) and not the graticule # then set visible to False in the constructor. for obj in self.objlist: pt = obj.ptype if pt == "Colorbar": if not self.image is None: obj.plot(obj.frame, self.image.im) elif not self.contourset is None: obj.plot(obj.frame, self.contourset.CS) else: raise Exception("A color bar could not find an image or contour set!") self.cmap.add_frame(self.frame) # Add to list in mplutil # A colorbar is created. It gets its correct Axes after a resize event. # But if we save to a figure (e.g. te create plots in Sphinx), there is no # resize event, so for this situation we force one. (VOG 06-08-2019) #if self.divider: self.onresize(None)
[docs] def toworld(self, xp, yp, matchspatial=False): #-------------------------------------------------------------------- """ This is a helper method for method :meth:`kapteyn.wcs.Projection.toworld`. It converts pixel positions from a map to world coordinates. The difference with that method is that this method has its focus on maps, i.e. two dimensional structures. It knows about the missing spatial axis if your data slice has only one spatial axis. Note that pixels in FITS run from 1 to *NAXISn* and that the pixel coordinate equal to *CRPIXn* corresponds to the world coordinate in *CRVALn*. :param xp: Pixel value(s) corresponding to the x coordinate of a position. :type xp: Single Floating point number or sequence :param yp: A pixel value corresponding to the y coordinate of a position. :type yp: Single Floating point number or sequence :param matchspatial: If True then also return the world coordinate of the matching spatial axis. Usually this is an issue when the map is a slice with only one spatial axis (XV- or Position-Velocity map) :type matchspatial: Boolean :Note: If somewhere in the process an error occurs, then the return values of the world coordinates are all *None*. :Returns: Three world coordinates: *xw* which is the world coordinate for the x-axis, *yw* which is the world coordinate for the y-axis and (if *matchspatial=True*) *missingspatial* which is the world coordinate that belongs to the missing spatial axis. If there is not a missing spatial axis, then the value of this output parameter is *None*. So you don't need to know the structure of the map beforehand. You can test whether the last value is *None* or not *None* in the calling environment. :Examples: We have a test set with: * RA: crpix1=51 - crval1=-51,28208479590 * DEC: crpix2=51 - crval2=+60.15388802060 * VELO: crpix3=-20 - crval3=-243000 (m/s) Now let us try to find the world coordinates of a RA-VELO map at (crpix1, crpix3) at slice position DEC=51. We should get three numbers which are all equal to the value of *CRVALn* >>> from kapteyn import maputils >>> fig = figure() >>> fitsobject = maputils.FITSimage('ngc6946.fits') >>> fitsobject.set_imageaxes(1,3, slicepos=51) >>> annim = fitsobject.Annotatedimage() >>> annim.toworld(51,-20) (-51.282084795899998, -243000.0, 60.1538880206) >>> annim.topixel(-51.282084795899998, -243000.0) (51.0, -20.0) Or work with a sequence of numbers (list, tuple of NumPy ndarray object) as in this example:: from kapteyn import maputils f = maputils.FITSimage("ngc6946.fits") # Get an XV slice at DEC=51 f.set_imageaxes(1, 3, slicepos=51) annim = f.Annotatedimage() x = [10, 50, 300, 399] y = [1, 44, 88, 100] # Convert these to world coordinates lon, velo, lat = annim.toworld(x, y, matchspatial=True) print("lon, velo lat=", lon, velo, lat) # We are not interested in the pixel coordinate of the slice # because we know it is 51. Therefore we omit 'matchspatial' x, y = annim.topixel(lon, velo) print("Back to pixel coordinates: x, y =", x, y) #Output: #lon, velo lat= [-50.691745281033555, # -51.267685761904154, # -54.862775451370837, # -56.280231731192607] # [-154800.00401099998, # 25799.987775999994, # 210599.97937199997, # 260999.97707999998] # [ 60.152142940138205, # 60.153886982461088, # 60.089564526325667, # 60.028325686860846] #Back to pixel coordinates: x, y = [ 10. 50. 300. 399.] # [ 1. 44. 88. 100.] """ #-------------------------------------------------------------------- xw = yw = None missingspatial = None try: if (self.mixpix is None): xw, yw = self.projection.toworld((xp, yp)) else: if issequence(xp): mixpix = numpy.zeros(len(xp)) + self.mixpix else: mixpix = self.mixpix xw, yw, missingspatial = self.projection.toworld((xp, yp, mixpix)) except: pass if matchspatial: return xw, yw, missingspatial else: return xw, yw
[docs] def topixel(self, xw, yw, matchspatial=False): #-------------------------------------------------------------------- """ This is a helper method for method :meth:`kapteyn.wcs.Projection.topixel`. It knows about the missing spatial axis if a data slice has only one spatial axis. It converts world coordinates in units (given by the FITS header, or the spectral translation) from a map to pixel coordinates. Note that pixels in FITS run from 1 to *NAXISn*. :param xw: A world coordinate corresponding to the x coordinate of a position. :type xw: Floating point number :param yw: A world coordinate corresponding to the y coordinate of a position. :type yw: Floating point number :param matchspatial: If set to *True* then return the pixel coordinates and the value of the pixel on the missing spatial axis. :type matchspatial: Boolean :Returns: Two pixel coordinates: *x* which is the world coordinate for the x-axis and *y* which is the world coordinate for the y-axis. If somewhere in the proces an error occurs, then the return values of the pixel coordinates are all *None*. :Notes: This method knows about the pixel on the missing spatial axis (if there is one). This pixel is usually the pixel coordinate of the slice if the dimension of the data is > 2. :Examples: See example at :meth:`toworld` """ #-------------------------------------------------------------------- x = y = None try: if (self.mixpix is None): x, y = self.projection.topixel((xw, yw)) else: # Note that we have a fixed grid for the missing spatial axis, # but this does not imply a constant world coordinate. Therefore # we have to use the mixed transformation method. if issequence(xw): mixpix = numpy.zeros(len(xw)) + self.mixpix unknown = numpy.zeros(len(xw)) + numpy.nan else: mixpix = self.mixpix unknown = numpy.nan wt = (xw, yw, unknown) pixel = (unknown, unknown, mixpix) (wt, pixel) = self.projection.mixed(wt, pixel) x = pixel[0]; y = pixel[1] except: pass if matchspatial: return x, y, self.mixpix else: return x, y
[docs] def inside(self, x=None, y=None, pos=None, mode=''): #-------------------------------------------------------------------- """ This convenience method belongs to class :class:`Annotatedimage` which represents a two dimensional map which could be a slice (*slicepos*) from a bigger data structure and/or could be limited by limits on the pixel ranges of the image axes (*pxlim*, *pylim*). Then, for a sequence of coordinates in x and y, return a sequence with Booleans with *True* for a coordinate within the boundaries of this map and *False* when it is outside the boundaries of this map. This method can work with either sequences of coordinates (parameters *x* and *y*) or a string with a position (parameter *pos*). If parameters *x* and *y* are used then parameter *world* sets these coordinates to world- or pixel coordinates. :param x: Single number of a sequence representing the x coordinates of your input positions. These coordinates are world coordinates if *mode='world'* (or *mode='w'*) and pixel coordinates if *mode='pixels* (or *mode='p'*). :type x: Floating point number or sequence of floating point numbers. :param y: Single number or a sequence representing the x coordinates of your input positions. See description for parameter *x* :type y: Floating point number or sequence of floating point numbers. :param mode: Input in *x* and*y* represent either pixel coordinates or world coordinates. Is the first character is 'p' or 'P' then the mode is set to pixels. If it starts with 'w' or 'W' the input in *x* and *y* are world coordinates. :param pos: A description of one or a number of positions entered as a string. The syntax is described in module :mod:`positions`. The value of parameter *mode* is ignored. :type pos: String :param world: If parameters *x* and *y* are used then the step of coordinate interpretation as with *pos* is skipped. These coordinates can be either pixel- or world coordinates depending on the value of *world*. By default this value is *True*. :type world: Boolean :Raises: :exc:`Exception` *One of the arrays is None and the other is not!* :exc:`Exception` *You cannot enter values for both pos= and x= and/or y=* :Returns: * None -- there was nothing to do * Single Boolean -- Input was a single position * NumPy array of Booleans -- Input was a sequence of positions :Note: For programmers: note the similarity to method :meth:`Marker` with respect to the use of method :meth:`kapteyn.positions.str2pos`. This method is tested with script *mu_insidetest.py* which is part of the examples tar file. :Examples: >>> fitsobj = maputils.FITSimage("m101.fits") >>> fitsobj.set_limits((180,344), (100,200)) >>> annim = fitsobj.Annotatedimage() >>> pos="{} 210.870170 {} 54.269001" >>> print(annim.inside(pos=pos)) >>> pos="ga 101.973853, ga 59.816461" >>> print(annim.inside(pos=pos)) >>> x = list(range(180,400,40)) >>> y = list(range(100,330,40)) >>> print(annim.inside(x=x, y=y, mode='pixels')) >>> print(annim.inside(x=crval1, y=crval2, mode='w')) """ #-------------------------------------------------------------------- if x is None and y is None and pos is None: # Nothing to do return None # For parameters *x* and *y* a *mode* should be given if pos is None: p = mode.upper().startswith('P') if p: w = False else: w = mode.upper().startswith('W') if not p and not w: raise Exception("Inside(): Mode not or incorrectly specified!") else: world = w if (x is None and y is not None) or (x is not None and y is None): raise Exception("Inside(): One of the arrays is None and the other is not!") if not (pos is None) and (not (x is None) or not (y is None)): raise Exception("Inside(): You cannot enter values for both pos= and x= and/or y=") if not (pos is None): world, pixels, units, errmes = str2pos(pos, self.projection, mixpix=self.mixpix, gridmode=self.gridmode) if errmes != '': raise Exception(errmes) else: xp = pixels[:,0] yp = pixels[:,1] else: if world: # Allow world coordinates that cannot be converted to pixels # Nan's will be returned for those world coordinates. # This setting uses the allow_invalid flag. # Note that topixel() can still go wrong somehow. # In that case x and y will be set to None by topixel() # (method of Projection object) ainv = self.projection.allow_invalid self.projection.allow_invalid = True xp, yp = self.topixel(x, y) self.projection.allow_invalid = ainv # When conversions fail then None's are returned if not issequence(x): x = [x] if not issequence(y): y = [y] if xp is None: xp = numpy.array([numpy.nan]*len(x)) if yp is None: yp = numpy.array([numpy.nan]*len(y)) else: if not isinstance(x, (numpy.ndarray)): xp = numpy.array(x) else: xp = x if not isinstance(y, (numpy.ndarray)): yp = numpy.array(y) else: yp = y # At this stage we have an array for the x and y coordinates. # Now check whether they are inside a map b = numpy.where((xp>self.pxlim[0]-0.5) & (xp<self.pxlim[1]+0.5) & (yp>self.pylim[0]-0.5) & (yp<self.pylim[1]+0.5), True, False) if b.shape != () and len(b) == 1: b = b[0] return b
def positionmessage(self, x, y, posobj, parts=False): #-------------------------------------------------------------------- """ Display cursor position in pixels and world coordinates together with the image value in the message area of a Matplotlib window. This function is used internally by :meth:`on_move` and :meth:`on_click`. :param x: The x coordinate of a position in the image :type x: Floating point number :param y: The y coordinate of a position in the image :type y: Floating point number :param posobj: A description of how the string should be formatted is stored in this object. :type posobj: An object from class :class:`Positionmessage` :Returns: A formatted string with position information and the image value of the corresponding pixel. E.g. ``x,y= 56.91, 25.81 wcs=-51.366665, 59.973195 Z=-4.45e-04`` The WCS coordinates are not formatted in hms/dms. They are printed in the units of the FITS header. :Notes: This method knows about the object's missing axis if only one spatial axis was part of the image. It takes also care of NaNs in your map. """ #-------------------------------------------------------------------- s = '' if self.pxlim[0]-0.5 < x < self.pxlim[1]+0.5 and self.pylim[0]-0.5 < y < self.pylim[1]+0.5: if 1: #try: xw, yw, missingspatial = self.toworld(x, y, matchspatial=True) #xi = numpy.round(x) - (self.pxlim[0]-1) #yi = numpy.round(y) - (self.pylim[0]-1) xi = int(numpy.round(x - (self.pxlim[0]-1))) yi = int(numpy.round(y - (self.pylim[0]-1))) x -= self.pixoffset[0]; y -= self.pixoffset[1] spix = posobj.pix2str(x, y) swcs = posobj.wcs2str(xw, yw, missingspatial) swcsuf = posobj.wcs2str(xw, yw, missingspatial, unformatted=True) if self.rgbs is None: if self.data is None: z = numpy.nan else: z = self.data[yi-1, xi-1] else: z = (self.rgbs[0][yi-1, xi-1], self.rgbs[1][yi-1, xi-1], self.rgbs[2][yi-1, xi-1]) s = '' if not spix is None: s = "x,y=%s" % spix if not swcs is None: s += " wcs=%s" % swcs sz = None if not (self.rgbs is None and self.data is None): sz = posobj.z2str(z) s += " z=%s" % sz else: #except: spix = self.posobj.pix2str(x,y) s = "pix:%s" % spix if parts: return(spix, swcs, swcsuf, sz) else: return s.strip() def mouse_toolbarinfo(self, axesevent): #-------------------------------------------------------------------- """ *Display position information:* Note that the events (motion_notify_event) are blocked from the moment you changed mode in the toolbar to zoom or pan mode. So we display a message in our modified Matplotlib routines that deal with callbacks for zoom- and pan actions. """ #-------------------------------------------------------------------- s = '' x, y = axesevent.xdata, axesevent.ydata self.X_lastvisited = x self.Y_lastvisited = y try: mode = self.figmanager.toolbar.mode except: return if mode != '': return s = self.positionmessage(x, y, axesevent.posobj) if s != '': if self.canvasmessenger and not backend.startswith('QT'): # For a non QT canvas with width 19.5 cm, there is a fixed width for # toolbar buttons (7.5 cm = 2.95 inch). For the rest (12 cm) we have # space to set a message. Unfortunately we don't have any information # about the real width of the string with information. # In this 12 cm we have 57 characters. On average the size of a # character is 12/57/2.54 inch (0.083 inch). charw = 0.12 # Average character width (better than 0.083) xsize = self.frame.figure.get_figwidth() messagemax = xsize - 2.5 if len(s)*charw > messagemax: l = int(messagemax/charw) s = s[:l] self.messenger(s)
[docs] def interact_toolbarinfo(self, pixfmt="%.1f", wcsfmt="%.3e", zfmt="%+.3e", hmsdms=True, dmsprec=1): #-------------------------------------------------------------------- """ Allow this :class:`Annotatedimage` object to interact with the user. It reacts to mouse movements. A message is prepared with position information in both pixel coordinates and world coordinates. The world coordinates are in the units given by the (FITS) header. :param pixfmt: Python number format for pixel coordinates :type pixfmt: String :param wcsfmt: Python number format for wcs coordinates if the coordinates are not spatial or if parameter *hmsdms* is False. :type wcsfmt: String :param zfmt: Python number format for image value(s) :type pixfmt: String :param hmsdms: If True (default) then spatial coordinates will be formatted in hours/degrees, minutes and seconds according to the current sky system. The precision in seconds is entered with parameter *dmsprec*. :type hmsdms: Boolean :param dmsprec: Number of decimal digits in seconds for coordinates formatted in in HMS/DMS :type dmsprec: Integer :Notes: If a format is set to *None*, its corresponding number(s) will not appear in the informative message. If a message does not fit in the toolbar then only a part is displayed. We don't have control over the maximum size of that message because it depends on the backend that is used (GTK, QT,...). If nothing appears, then a manual resize of the window will suffice. :Example: Attach to an object from class :class:`Annotatedimage`: >>> annim = f.Annotatedimage(frame) >>> annim.interact_toolbarinfo() or: >>> annim.interact_toolbarinfo(wcsfmt=None, zfmt="%g") A more complete example:: from kapteyn import maputils from matplotlib import pyplot as plt f = maputils.FITSimage("m101.fits") fig = plt.figure(figsize=(9,7)) frame = fig.add_subplot(1,1,1) annim = f.Annotatedimage(frame) ima = annim.Image() annim.Pixellabels() annim.plot() annim.interact_toolbarinfo() plt.show() """ #-------------------------------------------------------------------- posobj = Positionmessage(self.projection.skysys, self.skyout, self.projection.types) posobj.pixfmt = pixfmt posobj.wcsfmt = wcsfmt posobj.zfmt = zfmt posobj.hmsdms = hmsdms posobj.dmsprec = dmsprec self.toolbarkey = AxesCallback(self.mouse_toolbarinfo, self.frame, 'motion_notify_event', posobj=posobj) #t("Add mouse toolbar callback %d for object %d"%(id(self.toolbarkey), id(self))) self.AxesCallback_ids.append(self.toolbarkey)
def disconnectCallbacks(self): #-------------------------------------------------------------------- """ Disconnect callbacks made with AxecCallback() The id's are stored in a list 'AxesCallback_ids' """ #-------------------------------------------------------------------- #flushprint("\n\n------------------\nRemoving Axescallbacks from %d"%(id(self))) for acid in self.AxesCallback_ids: acid.deschedule() #flushprint("Removing Axescallback %d"%(id(acid))) del acid self.AxesCallback_ids = [] # Reset the list # self.frame.figure.canvas.mpl_disconnect(self.resize_id) def mouse_imagecolors(self, axesevent): #-------------------------------------------------------------------- """ *Reset color limits:* If you move the mouse in this image and press the **right mouse button** at the same time, then the color limits for image and colorbar are set to a new value. :param axesevent: AxesCallback event object with pixel position information. :type axesevent: AxesCallback instance :Example: Given an object from class :class:`Annotatedimage` called *annim*, register this callback function for the position information with: >>> annim.interact_imagecolors() :Notes: See *interact_imagecolors()* for more info. """ #-------------------------------------------------------------------- # Keys are often used as modifiers. So require an empty key if axesevent.event.button == 3 and axesevent.event.key in ['', None]: x, y = axesevent.xdata, axesevent.ydata if self.image is None or self.image.im is None: # There is no image to adjust return # 1. event.xdata and event.ydata are the coordinates of the mouse location in # data coordinates (i.e. in screen pixels) # 2. transData converts these coordinates to display coordinates # 3. The inverse of transformation transAxes converts display coordinates to # normalized coordinates for the current frame. xy = self.frame.transData.transform((x,y)) self.image.xyn_mouse = self.frame.transAxes.inverted().transform(xy) x , y = self.image.xyn_mouse slope = Annotatedimage.slopetrans * x # i.e. at center: slope=0.5*slopetrans sloperads = numpy.tan(numpy.radians(slope)) offset = y - Annotatedimage.shifttrans # i.e. at center: offset=0-shifttrans self.cmap.modify(sloperads, offset) self.callback('slope', slope) self.callback('offset', offset) def key_imagecolors(self, axesevent, externalkey=None): #-------------------------------------------------------------------- """ This method catches keys which change the color setting of an image. See also documentation at *interact_imagecolors()*. :param axesevent: AxesCallback event object with pixel position information. :type axesevent: AxesCallback instance :Examples: If *image* is an object from :class:`Annotatedimage` then register this function with: >>> annim = f.Annotatedimage(frame) >>> annim.interact_imagecolors() """ #-------------------------------------------------------------------- if externalkey is not None: eventkey = externalkey else: eventkey = axesevent.event.key if eventkey is None: # This can happen when the caps lock is on. return # Use the class variables ! scales = Annotatedimage.lutscales scalekeys = Annotatedimage.scalekeys scales_default = Annotatedimage.scales_default # Request for another color map with page up/down keys if eventkey in ['pageup', 'pagedown']: lm = len(cmlist.colormaps) if eventkey == 'pagedown': self.cmindx += 1 if self.cmindx >= lm: self.cmindx = 0 if eventkey == 'pageup': self.cmindx -= 1 if self.cmindx < 0: self.cmindx = lm - 1 newcolormapstr = cmlist.colormaps[self.cmindx] self.messenger(newcolormapstr) self.cmap.set_source(newcolormapstr) # Keep original object, just change the lut self.callback('lut', self.cmindx) #self.cmap.update() # Request for another scale, linear, logarithmic etc. elif eventkey in scalekeys: key = eventkey #self.messenger(scales[key]) s_indx = scalekeys[key] self.cmap.set_scale(scales[s_indx]) mes = "Color map scale set to '%s'" % scales[s_indx] self.messenger(mes) self.callback('scale', s_indx) # Invert the color map colors #elif eventkey.upper() == 'I': elif eventkey == '9': if self.cmap.invrt == -1.0: # see mplutil #inverse: self.cmap.set_inverse(False) self.cmapinverse = False mes = "Color map not inverted" else: self.cmap.set_inverse(True) self.cmapinverse = True mes = "Color map inverted!" self.messenger(mes) self.callback('inverse', self.cmapinverse) # Reset all color map parameters #elif eventkey.upper() == 'R': elif eventkey == '0': # Reset all at once self.cmap.auto = False # Postpone updates of the canvas. self.messenger('Reset color map to default') #self.cmap.set_source(self.startcmap) self.cmap.set_source(cmlist.colormaps[self.startcmindx]) self.cmap.modify(1.0, 0.0) if self.cmap.invrt == -1.0: #inverse: self.cmap.set_inverse(False) self.cmapinverse = False #colmap_start = cmlist.colormaps[self.image.startcmap] self.cmindx = self.startcmindx self.cmap.set_scale(scales[0]) self.blankcol = Annotatedimage.blankcols[0] self.cmap.set_bad(self.blankcol) self.cmap.auto = True self.cmap.update() # Update all self.callback('lut', self.cmindx) self.callback('inverse', False) self.callback('scale', scales_default) self.callback('slope', 45.0) # i.e. 45 degrees self.callback('offset', 0.0) self.callback('blankcol', 0) """ # Template for user interaction to set clip levels in existing image elif eventkey.upper() == 'C': cmin = self.clipmin + 200.0 cmax = self.clipmax - 200.0 print("cmin, max voor norm:", cmin, cmax) self.set_norm(cmin, cmax) """ elif eventkey.upper() == 'B': # Toggle colors for bad pixels (blanks) blankcols = Annotatedimage.blankcols try: indx = blankcols.index(self.blankcol) except: indx = 0 if indx + 1 == len(blankcols): # Start at the beginning again indx = 0 else: indx += 1 self.blankcol = blankcols[indx] self.cmap.set_bad(self.blankcol) mes = "Color of bad pixels changed to '%s'" % (Annotatedimage.blanknames[indx]) self.messenger(mes) self.callback('blankcol', indx) elif eventkey.upper() == 'M': stamp = datetime.now().strftime("%d%m%Y_%H%M%S") filename = self.basename + "_" + stamp + ".lut" self.write_colormap(filename) mes = "Save color map to file [%s]" % filename self.messenger(mes) self.callback('savelut', filename) elif eventkey.upper() == 'H': # Set data to histogram equalized version if self.histogram: # Back to normal self.set_histogrameq(False) self.messenger('Original image displayed') self.callback('histeq', False) else: if self.data_hist is None: self.messenger('Calculating histogram') self.set_histogrameq() self.messenger('Histogram eq. image displayed') self.callback('histeq', True) elif eventkey.upper() == 'X': # Change the smoothing factor and go to blur mode. self.blurindx += 1 if self.blurindx >= 10: self.blurindx = 1 self.blurfac = self.blurindx * 0.5 mes = "Blur index: %d blur sigma: %g" % (self.blurindx, self.blurfac) self.messenger(mes) self.set_blur(nx=self.blurfac, new=True) self.callback('blurfac', self.blurfac) elif eventkey.upper() == 'Z': # Set data to blurred version if self.blurred: # Back to normal self.set_blur(False) self.messenger('Original image displayed') self.callback('blurred', False) else: if self.data_blur is None: self.messenger('Calculating smoothed version') self.set_blur(nx=self.blurfac) self.messenger('Smoothed eq. image displayed') self.callback('blurred', True) def set_histogrameq(self, on=True): if not on: # Back to normal self.data = self.data_orig self.histogram = False else: if self.data_hist is None: #self.data_hist = self.histeq() self.histeq() # It sets attribute data_hist to new image self.data = self.data_hist self.histogram = True #self.norm = Normalize(vmin=self.clipmin, vmax=self.clipmax) if self.image.im is not None: self.image.im.set_data(self.data) else: # An image was not yet 'plotted'. Then we adjust some # parameters first to prepare for the new image. self.image.data = self.data self.cmap.update() def set_blur(self, on=True, nx=10, ny=None, new=False): if not on: # Back to normal self.data = self.data_orig self.blurred = False else: if self.data_blur is None or new: self.blur(nx, ny) self.data = self.data_blur self.blurred = True #self.norm = Normalize(vmin=self.clipmin, vmax=self.clipmax) if not self.image.im is None: self.image.im.set_data(self.data) #self.image.im.changed() else: # An image was not yet 'plotted'. Then we adjust some # parameters first to prepare for the new image. self.image.data = self.data #print("In blur id image, image.im, self.data", id(self.image), id(self.image.im), id(self.data)) self.cmap.update()
[docs] def interact_imagecolors(self): #-------------------------------------------------------------------- """ Add mouse interaction (right mouse button) and keyboard interaction to change the colors in an image. **MOUSE** If you move the mouse in the image for which you did register this callback function and press the **right mouse button** at the same time, then the color limits for image and colorbar are set to a new value. The new color setting is calculated as follows: first the position of the mouse (x, y) is transformed into normalized coordinates (i.e. between 0 and 1) called (xn, yn). These values are used to set the slope and offset for a function that sets an color for an image value according to the relations: ``slope = tan(89 * xn); offset = yn - 0.5``. The minimum and maximum values of the image are set by parameters *clipmin* and *clipmax*. For a mouse position exactly in the center (xn,yn) = (0.5,0.5) the slope is 1.0 and the offset is 0.0 and the colors will be divided equally between *clipmin* and *clipmax*. **KEYBOARD** * **page-down** move forwards through a list with known color maps. * **page-up** move backwards through a list with known color maps. * **0** resets the colors to the original colormap and scaling. The default color map is 'jet'. * **i** (or 'I') toggles between **inverse** and normal scaling. * **1** sets the colormap scaling to **linear** * **2** sets the colormap scaling to **logarithmic** * **3** sets the colormap scaling to **exponential** * **4** sets the colormap scaling to **square root** * **5** sets the colormap scaling to **square** * **b** (or 'B') changes color of **bad** pixels. * **h** (or 'H') replaces the current data by a **histogram equalized** version of this data. This key toggles between the original data and the equalized data. * **z** (or 'Z') replaces the current data by a **smoothed** version of this data. This key is a toggle between the original data and the blurred version, smoothed with a value of sigma set by key 'x'. Pressing 'x' repeatedly increases the smoothing factor. Note that Not a Number (NaN) values are smoothed to 0.0. * **x** (or 'X") increases the smoothing factor. The number of steps is 10. Then is starts again with step 1. * **m** (or 'M') saves current colormap look up data to a file. The default name of the file is the name of file from which the data was extracted or the name given in the constructor. The name is appended with '.lut'. This data is written in the right format so that it can be be (re)used as input colormap. This way you can fix a color setting and reproduce the same setting in another run of a program that allows one to enter a colormap from file. If *annim* is an object from class :class:`Annotatedimage` then activate color editing with: >>> fits = maputils.FITSimage("m101.fits") >>> fig = plt.figure() >>> frame = fig.add_subplot(1,1,1) >>> annim = fits.Annotatedimage(frame) >>> annim.Image() >>> annim.interact_imagecolors() >>> annim.plot() """ #-------------------------------------------------------------------- self.imagecolorsmouse = AxesCallback(self.mouse_imagecolors, self.frame, 'motion_notify_event') self.imagecolorskey = AxesCallback(self.key_imagecolors, self.frame, 'key_press_event') #self.imagecolorskey = AxesCallback(self.key_imagecolors, self.frame, 'motion_notify_event') #self.imagecolorsmouse = AxesCallback(self.mouse_imagecolors, self.frame, 'motion_notify_event') # Set interaction with colormap on. We postponed this until now because # setting it before plot operations ruins your graticule frames with versions # of Matplotlib >= 0.99. This seems to be caused by an unvoluntary canvas.draw() # in module mplutil for color map updates for a sequence of images with the same # color map (subsets/slices). The constructor of the Annotatedimage class # sets auto to False. Here we set it to True because only here we require # interaction. self.cmap.auto = True #flushprint("Add imagecolors mouse and key callbacks %d %d for object %d"%(id(self.imagecolorskey), id(self.imagecolorsmouse), id(self))) self.AxesCallback_ids.append(self.imagecolorskey) self.AxesCallback_ids.append(self.imagecolorsmouse)
def mouse_writepos(self, axesevent): #-------------------------------------------------------------------- """ Print position information of the position where you clicked with the right mouse button while pressing the SHIFT key. Print the info on the command line or GIPSY command line cq. Log file. Register this function with an event handler, see example. :param axesevent: AxesCallback event object with pixel position information. :type axesevent: AxesCallback instance :Example: Register this callback function for object *annim* with: >>> annim.interact_writepos() """ #-------------------------------------------------------------------- condition = axesevent.event.button in [1,2,3] and axesevent.event.key == 'shift' if not condition: return if self.figmanager.toolbar.mode == '': if axesevent.event.button == 1: x, y = axesevent.xdata, axesevent.ydata spix, swcs, swcsuf, sz = self.positionmessage(x, y, axesevent.posobj, parts=True) s = ' ' # One space to separate multiple entries on one line if spix: s += spix if swcs: if s: s += ' ' s += swcs if swcsuf: # The unformatted world coordinate if s: s += ' ' s += swcsuf if sz: if s: s += ' ' s += sz if s == ' ': return if axesevent.event.button == 2: # Added at June 9, 2021: With the shift + middle mouse button we # send a string 'DEL' to the command line. This can be used # to take an action in the current active task that recieves the # positions on the command line. An example can be found in ELLFIT s = 'DEL' if axesevent.event.button == 3: # Added at June 9, 2021: With the shift + right mouse button we # send an carriage return only. An external program can react to this # e.g. in ELLFIT to close a polygon. s = '' if axesevent.gipsy and gipsymod: if axesevent.g_appendcr: s += '\r' # Add carriage return if axesevent.g_typecli: typecli(s) # Write to Hermes command line if axesevent.g_tolog: anyout(s) # Write to Hermes log file and screen else: print(s)
[docs] def interact_writepos(self, pixfmt="%.1f", dmsprec=1, wcsfmt="%.3g", zfmt="%.3e", hmsdms=True, grids=True, world=True, worlduf=False, imval=True, gipsy=False, g_typecli=False, g_tolog=False, g_appendcr=False ): #-------------------------------------------------------------------- """ Add mouse interaction (left mouse button) to write the position of the mouse to screen. The position is written both in pixel coordinates and world coordinates. :param pixfmt: Python number format for pixel coordinates :type pixfmt: String :param wcsfmt: Python number format for wcs coordinates if the coordinates are not spatial or if parameter *hmsdms* is False. :type wcsfmt: String :param zfmt: Python number format for image value(s) :type pixfmt: String :param hmsdms: If True (default) then spatial coordinates will be formatted in hours/degrees, minutes and seconds according to the current sky system. The precision in seconds is entered with parameter *dmsprec*. :type hmsdms: Boolean :param dmsprec: Number of decimal digits in seconds for coordinates formatted in in HMS/DMS :type dmsprec: Integer :param gipsy: If set to True, the output is written with GIPSY function anyout() to screen and a log file. :type gipsy: Boolean :param typecli: If True then write the positions on the Hermes command line instead of the log file and screen. :type typecli: Boolean :Example: >>> fits = maputils.FITSimage("m101.fits") >>> fig = plt.figure() >>> frame = fig.add_subplot(1,1,1) >>> annim = fits.Annotatedimage(frame) >>> annim.Image() >>> annim.interact_writepos() >>> annim.plot() For a formatted output one could add parameters to *interact_writepos()*. The next line writes no pixel coordinates, writes spatial coordinates in degrees (not in HMS/DMS format) and adds a format for the world coordinates and the image value(s). >>> annim.interact_writepos(pixfmt=None, wcsfmt="%.12f", zfmt="%.3e", hmsdms=False) """ #-------------------------------------------------------------------- posobj = Positionmessage(self.projection.skysys, self.skyout, self.projection.types) posobj.pixfmt = posobj.wcsfmt = posobj.wcsuffmt = posobj.zfmt = None if grids: posobj.pixfmt = pixfmt if world: posobj.wcsfmt = wcsfmt if worlduf: # User wants world coordinates in decimal degrees if wcsfmt: posobj.wcsuffmt = wcsfmt if imval: posobj.zfmt = zfmt posobj.hmsdms = hmsdms posobj.dmsprec = dmsprec if not gipsy: g_typecli = False self.writeposmouse = AxesCallback(self.mouse_writepos, self.frame, 'button_press_event', gipsy=gipsy, g_typecli=g_typecli, g_tolog=g_tolog, g_appendcr=g_appendcr, posobj=posobj) self.AxesCallback_ids.append(self.writeposmouse)
#flushprint("Add mouse writeposmouse callback %d for object %d"%(id(self.writeposmouse), id(self))) def motion_events(self): #-------------------------------------------------------------------- """ Allow this :class:`Annotatedimage` object to interact with the user. It reacts on mouse movements. During these movements, a message with information about the position of the cursor in pixels and world coordinates is displayed on the toolbar. """ #-------------------------------------------------------------------- # self.cidmove = AxesCallback(self.on_move, self.frame, 'motion_notify_event') self.AxesCallback_ids.append(self.cidmove) def key_events(self): #-------------------------------------------------------------------- """ Allow this :class:`Annotatedimage` object to interact with the user. using keys. """ #-------------------------------------------------------------------- #self.cidkey = self.fig.canvas.mpl_connect('key_press_event', self.key_pressed) self.cidkey = AxesCallback(self.key_pressed, self.frame, 'key_press_event') self.AxesCallback_ids.append(self.cidkey) def click_events(self): #-------------------------------------------------------------------- """ Allow this :class:`Annotatedimage` object to interact with the user. It reacts on pressing the left mouse button and prints a message to stdout with information about the position of the cursor in pixels and world coordinates. """ #-------------------------------------------------------------------- # self.cidclick = self.fig.canvas.mpl_connect('button_press_event', self.on_click) self.cidclick = AxesCallback(self.on_click, self.frame, 'button_press_event') self.AxesCallback_ids.append(self.cidclick)
[docs] def positionsfromfile(self, filename, comment, skyout=None, **kwargs): #-------------------------------------------------------------------- """ Read positions from a file with world coordinates and convert them to pixel coordinates. The interface is exactly the same as from method :meth:`kapteyn.tabarray.readColumns()` It expects that the first column you specify contains the longitudes and the second column that is specified the latitudes. :param filename: Name (and pahth if necessary) of the file which contains longitudes and latitudes. :type filename: String :param comment: Comment characters. If a line starts with a comment character, it will be skipped. :type comment: String :param skyout: Tell the system in what sky system your longitudes and latitudes are. :type skyout: Sky definition :param kwargs: Keywords for Tabarray's method readColumns. :type kwargs: Python keyword arguments :Examples: >>> fn = 'smallworld.txt' >>> xp, yp = annim.positionsfromfile(fn, 's', cols=[0,1]) >>> frame.plot(xp, yp, ',', color='#FFDAAA') Or: your graticule is equatorial but the coordinates in the file are galactic: >>> xp, yp = annim.positionsfromfile(fn, 's', skyout='ga', cols=[0,1]) """ #-------------------------------------------------------------------- ainv = self.projection.allow_invalid if not skyout is None: skyout_old = self.projection.skyout self.projection.skyout = skyout self.projection.allow_invalid = True lon, lat= readColumns(filename, comment, **kwargs) xp, yp = self.projection.topixel((lon,lat)) xp = numpy.ma.masked_where(numpy.isnan(xp) | (xp > self.pxlim[1]) | (xp < self.pxlim[0]), xp) yp = numpy.ma.masked_where(numpy.isnan(yp) | (yp > self.pylim[1]) | (yp < self.pylim[0]), yp) self.projection.allow_invalid = ainv # Reset status for invalid transformations if not skyout is None: # Restore self.projection.skyout = skyout_old return xp, yp
def getflux(self, xy, pixelstep=None): # Return Area in pixels and sum of image values in # polygon defined by xy if pixelstep is None: pixelstep = self.pixelstep poly = numpy.asarray(xy) mm = poly.min(0) xmin = mm[0]; ymin = mm[1] mm = poly.max(0) xmax = mm[0]; ymax = mm[1] xmin = numpy.floor(xmin); xmax = numpy.ceil(xmax) ymin = numpy.floor(ymin); ymax = numpy.ceil(ymax) Y = numpy.arange(ymin,ymax+1, pixelstep) X = numpy.arange(xmin,xmax+1, pixelstep) l = int(xmax-xmin+1); b = int(ymax-ymin+1) numpoints = len(X)*len(Y) x, y = numpy.meshgrid(X, Y) pos = list(zip(x.flatten(), y.flatten())) pos = numpy.asarray(pos) if mploldversion: mask = nxutils.points_inside_poly(pos, poly) else: polypath = path.Path(xy) mask = polypath.contains_points(pos) # Get indices for positions inside shape using mask i = numpy.arange(len(pos), dtype='int') # Get a filtered array posm = pos[i[numpy.where(mask)]] xy = posm.T x = xy[0] y = xy[1] # Check inside pixels: # self.frame.plot(x, y, 'r') # Correction of array index consists of three elements: # 1) The start position of the box must map on the start position of the array # 2) Positions all > 0.5 so add 0.5 and take int to avoid the need of a round function # 3) The data could be a limited part of the total, but the mouse positions # are relative to (1,1). xcor = self.pxlim[0] - 0.5 ycor = self.pylim[0] - 0.5 x = numpy.asarray(x-xcor, dtype='int') y = numpy.asarray(y-ycor, dtype='int') # Compose the array with the intensities at positions inside the polygon z = self.data[y,x] area = len(posm)*pixelstep*pixelstep sum = z.sum()*(pixelstep*pixelstep) """ # Old algorithm count = 0 sum = 0.0 for i, xy in enumerate(pos): if mask[i]: xp = int(xy[0] - xcor) yp = int(xy[1] - ycor) z = self.data[yp,xp] sum += z count += 1 c1 = time.clock() print("2: Calculated in %f cpu seconds" % ((c1-c0))) print(count*(pixelstep*pixelstep), sum*(pixelstep*pixelstep)) """ return area, sum
[docs]class FITSaxis(object): #----------------------------------------------------------------- """ This class defines objects which store WCS information from a FITS header. It includes axis number and alternate header information in a FITS keyword. :param axisnr: FITS axis number. For this number the relevant keys in the header are read. :type axisnr: Integer :param hdr: FITS header :type hdr: pyfits.NP_pyfits.Header instance :Methods: .. automethod:: printattr .. automethod:: printinfo """ #-------------------------------------------------------------------- def __init__(self, axisnr, hdr, alter): def makekey(key, alternate=True): s = "%s%d" %(key, axisnr) if alter != '' and alternate: s += alter return s ax = makekey("CTYPE") self.ctype = 'Unknown' self.axname = 'Unknown' self.axext = '' if ax in hdr: self.ctype = hdr[ax].upper() info = hdr[ax].split('-') self.axname = info[0].upper() if len(info) > 1: ext = info[-1].upper() # Last splitted string must be extension. Skip the hyphens extlist = { 'ARC':'Zenithal equidistant projection', 'AIR':'Airy projection', 'AZP':'Slant zenithal (azimuthal) perspective projection', 'AIT':"Hammer Aitoff projection", 'BON':"Bonne's equal area projection", 'CAR':'Plate Carree', 'CEA':"Lambert's equal area projection", 'COD':"Conic equidistant projection", 'COE':"Conic equal area projection", 'COO':"Conic orthomorfic projection", 'COP':"Conic perspective projection", 'CSC':"COBE quadrilateralized spherical cube projection", 'CYP':"Gall's stereographic projection", 'MER':"Mercator's projection", 'MOL':"Mollweide's projection", 'NCP':"Northern celestial pole projection", 'PAR':'Parabolic projection', 'PCO':"Polyconic", 'POL':"Polyconic projection", 'QSC':"Quadrilateralized spherical cube projection", 'SIN':'Slant orthograpic projection', 'SFL':'Sanson-Flamsteed projection', 'STG':'Stereographic projection', 'SZP':'Slant zenithal perspective', 'TAN':'Gnomonic projection', 'TSC':"Tangential spherical cube projection", 'ZEA':'Zenith equal area projection', 'ZPN':'Zenithal polynomial projection'} self.axext = ext #flushprint("ext, haskey=%s %s"%(ext, str(extlist.has_key(ext)))) if ext in extlist: self.axext += ' (' + extlist[ext] +')' else: self.ctype = "X%d"%axisnr self.axname = self.ctype ai = makekey("NAXIS", alternate=False) self.axlen = hdr[ai] self.axisnr = axisnr self.axstart = 1 self.axend = self.axlen ai = makekey("CDELT") if ai in hdr: self.cdelt = hdr[ai] else: self.cdelt = 1.0 ai = makekey("CRVAL") if ai in hdr: self.crval = hdr[ai] else: self.crval = 1.0 ai = makekey("CUNIT") # Is sometimes omitted, so check first. if ai in hdr: self.cunit = hdr[ai] else: self.unit = '?' ai = makekey("CRPIX") if ai in hdr: self.crpix = hdr[ai] else: self.crpix = self.axlen/2 ai = makekey("CROTA") # Not for all axes self.crota = 0 if ai in hdr: self.crota = hdr[ai] self.wcstype = None self.wcsunits = None self.outsidepix = None
[docs] def printattr(self): #---------------------------------------------------------- """ Print formatted information for this axis. :Examples: >>> from kapteyn import maputils >>> fitsobject = maputils.FITSimage('rense.fits') >>> fitsobject.hdr <pyfits.NP_pyfits.Header instance at 0x1cae3170> >>> ax1 = maputils.FITSaxis(1, fitsobject.hdr) >>> ax1.printattr() axisnr - Axis number: 1 axlen - Length of axis in pixels (NAXIS): 100 ctype - Type of axis (CTYPE): RA---NCP axname - Short axis name: RA cdelt - Pixel size: -0.007165998823 crpix - Reference pixel: 51.0 crval - World coordinate at reference pixel: -51.2820847959 cunit - Unit of world coordinate: DEGREE wcstype - Axis type according to WCSLIB: None wcsunits - Axis units according to WCSLIB: None outsidepix - A position on an axis that does not belong to an image: None If we set the image axes in *fitsobject* then the WCS attributes will get a value also. This object stores its FITSaxis objects in a list called *axisinfo[]*. The index is the required FITS axis number. >>> fitsobject.set_imageaxes(1, 2, 30) >>> fitsobject.axisinfo[1].printattr() .......... wcstype - Axis type according to WCSLIB: longitude wcsunits - Axis units according to WCSLIB: deg outsidepix - A position on an axis that does not belong to an image: None """ #---------------------------------------------------------- s = "axisnr - Axis number: %s\n" % self.axisnr \ + "axlen - Length of axis in pixels (NAXIS): %s\n" % self.axlen \ + "axstart - First pixel coordinate: %s\n" % self.axstart \ + "axend - Last pixel coordinate: %s\n" % self.axend \ + "ctype - Type of axis (CTYPE): %s\n" % self.ctype \ + "axname - Short axis name: %s\n" % self.axname \ + "axext - Axis extension: : %s\n" % self.axext \ + "cdelt - Pixel size: %s\n" % self.cdelt \ + "crpix - Reference pixel: %s\n" % self.crpix \ + "crval - World coordinate at reference pixel: %s\n" % self.crval \ + "cunit - Unit of world coordinate: %s\n" % self.cunit \ + "crota - Rotation of axis: %s\n" % self.crota \ + "wcstype - Axis type according to WCSLIB: %s\n" % self.wcstype \ + "wcsunits - Axis units according to WCSLIB: %s\n" % self.wcsunits \ + "outsidepix - A position on an axis that does not belong to an image: %s\n" % self.outsidepix return s
[docs] def printinfo(self): #---------------------------------------------------------- """ Print formatted information for this axis. :Examples: >>> from kapteyn import maputils >>> fitsobject = maputils.FITSimage('rense.fits') >>> ax1 = maputils.FITSaxis(1, fitsobject.hdr) >>> ax1.printinfo() Axis 1: RA---NCP from pixel 1 to 512 {crpix=257 crval=178.779 cdelt=-0.0012 (DEGREE)} {wcs type=longitude, wcs unit=deg} Axis 2: DEC--NCP from pixel 1 to 512 {crpix=257 crval=53.655 cdelt=0.00149716 (DEGREE)} {wcs type=latitude, wcs unit=deg} :Notes: if attributes for a :class:`maputils.FITSimage` object are changed then the relevant axis properties are updated. So this method can return different results depending on when it is used. """ #---------------------------------------------------------- a = self s = "Axis %d: %-9s from pixel %5d to %5d\n {crpix=%d crval=%G cdelt=%g (%s)}\n {wcs type=%s, wcs unit=%s}" % ( a.axisnr, a.ctype, a.axstart, a.axend, a.crpix, a.crval, a.cdelt, a.cunit, a.wcstype, a.wcsunits) return s
[docs]class FITSimage(object): #----------------------------------------------------------------- """ This class extracts 2D image data from FITS files. It allows for external functions to prompt users for relevant input like the name of the FITS file, which header in that file should be used, the axis numbers of the image axes, the pixel limits and a spectral translation if one of the selected axes is a spectral axis. All the methods in this class that allow these external functions for prompting can also be used without these functions. Then one needs to know the properties of the FITS data beforehand. :param filespec: A default file either to open directly or to be used in a prompt as default file. This variable should have a value if no external function is used to prompt a user. :type filespec: String :param promptfie: A user supplied function which should prompt a user for some data, opens the FITS file and returns the hdu list and a user selected index for the header from this hdu list. An example of a function supplied by :mod:`maputils` is function :func:`prompt_fitsfile` :type promptfie: Python function :param hdunr: A preset of the index of the header from the hdu list. If this variable is set then it should not prompted for in the user supplied function *promptfie*. :type hdunr: Integer :param alter: Selects an alternate header for the world coordinate system. Default is the standard header. Keywords in alternate headers end on a character A..Z :type alter: Empty or a single character. Input is case insensitive. :param memmap: Set the memory mapping for PyFITS. The default is copied from the default in your version of PyFITS. If you want to be sure it is on then specify *memmap=1* :type memmap: Boolean :param externalheader: If defined, then it is a header from an external source e.g. a user defined header. :type externalheader: Python dictionary :param externaldata: If defined, then it is data from an external source e.g. user defined data or processed data in a numpy array. A user/programmer should check if the shape of the numpy array fits the sizes given in FITS keywords *NAXISn*. :type externaldata: Numpy array :param parms: Extra parameters for PyFITS's *open()* method, such as *uint16*, *ignore_missing_end*, *checksum*, see PyFITS documentation for their meaning. :type parms: keyword arguments :Attributes: .. attribute:: filename Name of the FITS file (read-only). .. attribute:: hdr Header as read from the header (read-only). .. attribute:: naxis Number of axes (read-only). .. attribute:: dat The raw image data (not sliced, swapped or limited in range). The required sliced image data is stored in attribute :attr:`boxdat`. This is a read-only attribute. .. attribute:: axperm Axis permutation array. These are the (FITS) axis numbers of your image x & y axis. .. attribute:: wcstypes Type of the axes in this data. The order is the same as of the axes. The types ara strings and are derived from attribute wcstype of the Projection object. The types are: 'lo' is longitude axis. 'la' is latitude axis, 'sp' is spectral axis. 'li' is a linear axis. Appended to 'li' is an underscore and the ctype of that axis (e.g. 'li_stokes'). .. attribute:: mixpix The missing pixel if the image has only one spatial axis. The other world coordinate could be calculated with a so called *mixed* method which allows for one world coordinate and one pixel. .. attribute:: axisinfo A list with :class:`FITSaxis` objects. One for each axis. The index is an axis number (starting at 1). .. attribute:: slicepos A list with positions on axes in the FITS file which do not belong to the required image. .. attribute:: pxlim Axis limit in pixels. This is a tuple or list (xlo, xhi). .. attribute:: pylim Axis limit in pixels. This is a tuple or list (xlo, xhi). .. attribute:: boxdat The image data. Possibly sliced, axis swapped and limited in axis range. .. attribute:: imshape Sizes of the 2D array in :attr:`boxdat`. .. attribute:: spectrans A string that sets the spectra translation. If one uses the prompt function for the image axes, then you will get a list of possible translations for the spectral axis in your image. .. attribute:: proj An object from :class:`wcs.Projection`. This object is the result of the call: ``proj = wcs.Projection(self.hdr)``, so it is the Projection object that involves all the axes in the FITS header. .. attribute:: convproj An object from :class:`wcs.Projection`. This object is needed to be able to use methods *toworld()* and *topixel()* for the current image. .. attribute:: figsize A suggested figure size (inches) in X and Y directions. .. attribute:: aspectratio Plot a circle in **world coordinates** as a circle. That is, if the pixel size in the FITS header differs in X and Y, then correct the (plot) size of the pixels with value *aspectratio* so that features in an image have the correct sizes in longitude and latitude in degrees. :Notes: The object is initialized with a default position for a data slice if the dimension of the FITS data is > 2. This position is either the value of CRPIX from the header or 1 if CRPIX is outside the range [1, NAXIS]. Values -inf and +inf in a dataset are replaced by NaN's (not a number number). We know that Matplotlib's methods have problems with these values, but these methods can deal with NaN's. :Examples: PyFITS allows URL's to retrieve FITS files. It can also read gzipped files e.g.: >>> f = 'http://www.atnf.csiro.au/people/mcalabre/data/WCS/1904-66_ZPN.fits.gz' >>> fitsobject = maputils.FITSimage(f) >>> print(fitsobject.str_axisinfo()) Axis 1: RA---ZPN from pixel 1 to 192 {crpix=-183 crval=0 cdelt=-0.0666667 (Unknown)} {wcs type=longitude, wcs unit=deg} Axis 2: DEC--ZPN from pixel 1 to 192 {crpix=22 crval=-90 cdelt=0.0666667 (Unknown)} {wcs type=latitude, wcs unit=deg} Use Maputil's prompt function :func:`prompt_fitsfile` to get user interaction for the FITS file specification. >>> fitsobject = maputils.FITSimage(promptfie=maputils.prompt_fitsfile) :Methods: .. automethod:: set_imageaxes .. automethod:: set_limits .. automethod:: set_spectrans .. automethod:: set_skyout .. automethod:: Annotatedimage .. automethod:: get_pixelaspectratio .. automethod:: get_figsize .. automethod:: str_header .. automethod:: str_axisinfo .. automethod:: str_wcsinfo .. automethod:: str_spectrans .. automethod:: get_dataminmax .. automethod:: slice2world .. automethod:: header2classic .. automethod:: reproject_to .. automethod:: writetofits """ #-------------------------------------------------------------------- def __init__(self, filespec=None, promptfie=None, prompt=True, hdunr=None, alter='', memmap=None, externalheader=None, externaldata=None, externalname="artificial", **parms): #---------------------------------------------------- # Usually the required header and data are extracted # from a FITS file. But it is also possible to provide # header and data from an external source. Then these # must be processed instead of other keyword parameters #----------------------------------------------------- if externalheader is not None: self.hdr = externalheader self.bitpix, self.bzero, self.bscale, self.blank = getscale(self.hdr) self.filename = externalname if externaldata is not None: if 'int' in externaldata.dtype.name: # So this is integer data without bscale, bzero or blank self.dat = externaldata.astype(numpy.float32) #flushprint("I just converted data to float for externaldata") else: self.dat = externaldata else: self.dat = None #self.dat = externaldata else: # Not an external header, so a file is given or user wants to be prompted. if promptfie: if memmap is None: # Use default of current PyFITS version hdulist, hdunr, filename, alter = promptfie(filespec, prompt, hdunr, alter) else: hdulist, hdunr, filename, alter = promptfie(filespec, prompt, hdunr, alter, memmap) else: try: if memmap is None: hdulist = pyfits.open(filespec, **parms) else: hdulist = pyfits.open(filespec, memmap=memmap, **parms) filename = filespec except IOError as xxx_todo_changeme: (errno, strerror) = xxx_todo_changeme.args print("Cannot open FITS file: I/O error(%s): %s" % (errno, strerror)) raise except: print("Cannot open file, unknown error!") raise if hdunr is None: hdunr = 0 self.hdunr = hdunr hdu = hdulist[hdunr] self.filename = filename self.hdr = hdu.header self.bitpix, self.bzero, self.bscale, self.blank = getscale(self.hdr) # If an input array is of type integer then it is converted to # float32. Then we can use NaN's in the data as a replacement for BLANKS # Note that if scaled data is read then the scaling is applied first # and the type is converted to float32 # Due to added code in PyFITS 1.3 to deal with # blanks, the conversion also takes place if we have # integer data and keyword BLANK is available. In that case # the integer array is automatically converted to float64 and # the BLANK values are converted to NaN. if externaldata is None: if 'int' in hdu.data.dtype.name: # So this is integer data without bscale, bzero or blank self.dat = hdu.data.astype(numpy.float32) else: self.dat = hdu.data else: #Do not check type self.dat = externaldata hdulist.close() # Close the FITS file # IMPORTANT # Some FITS writers write their -32 bitpix blanks as -inf, # but there can also be other sources that insert +inf or -inf # values in a data set. Most methods in Matplotlib (e.g. to # create images or contours) cannot cope with these -inf and inf # values. So to be save, we treat those as NaN and therefore # replace +/-inf's by NaN's """ try: self.dat[-numpy.isfinite(self.dat)] = numpy.nan except: pass """ # An alternate header can also be specified for an external header self.alter = alter.upper() # Test on the required minimum number of axes (2) self.naxis = self.hdr['NAXIS'] if self.naxis < 2: print("You need at least two axes in your FITS file to extract a 2D image.") print("Number of axes in your FITS file is %d" % (self.naxis,)) hdulist.close() raise Exception("Number of data axes must be >= 2.") self.axperm = [1,2] self.mixpix = None # self.convproj = wcs.Projection(self.hdr).sub(self.axperm) axinf = {} for i in range(self.naxis): # Note that here we define the relation between index (i) and # the axis number (i+1) # The axinf dictionary has this axis number as key values axisnr = i + 1 axinf[axisnr] = FITSaxis(axisnr, self.hdr, self.alter) self.axisinfo = axinf slicepos = [] # Set default positions (CRPIXn) on axes outside image for slicing data sliceaxnames = [] sliceaxnums = [] n = self.naxis if (n > 2): for i in range(n): axnr = i + 1 if axnr not in self.axperm: crpix = self.axisinfo[axnr].crpix # If CRPIX in the header is negative (e.g. in a # manipulated header) then this corresponds to a non-existing data # slice. In that case we take the first slice in # a sequence as the default. if crpix < 1 or crpix > self.axisinfo[axnr].axlen: crpix = 1 slicepos.append(crpix) sliceaxnames.append(self.axisinfo[axnr].axname) sliceaxnums.append(axnr) self.slicepos = slicepos self.sliceaxnames = sliceaxnames self.sliceaxnums = sliceaxnums n1 = self.axisinfo[self.axperm[0]].axlen n2 = self.axisinfo[self.axperm[1]].axlen self.pxlim = [1, n1] self.pylim = [1, n2] self.proj = wcs.Projection(self.hdr, alter=self.alter) # Attribute 'gridmode' is False by default for i in range(n): ax = i + 1 # The index for axinfo starts with 1. self.axisinfo[ax].wcstype = self.proj.types[i] self.axisinfo[ax].wcsunits = self.proj.units[i] #self.axisinfo[ax].cdelt = self.proj.cdelt[i] #if self.alter != '': self.axisinfo[ax].cdelt = self.proj.cdelt[i] self.axisinfo[ax].crval = self.proj.crval[i] self.axisinfo[ax].ctype = self.proj.ctype[i] self.axisinfo[ax].cunit = self.proj.cunit[i] self.axisinfo[ax].crpix = self.proj.crpix[i] # Set the type of axis in the original system. wcstypes = [] for i in range(n): ax = i + 1 if not self.proj.lonaxnum is None and ax == self.proj.lonaxnum: wcstypes.append('lo') elif not self.proj.lataxnum is None and ax == self.proj.lataxnum: wcstypes.append('la') elif not self.proj.specaxnum is None and ax == self.proj.specaxnum: wcstypes.append('sp') else: # To distinguish linear types we append the ctype of this axis. wcstypes.append('li_' + str(self.proj.ctype[i])) self.wcstypes = wcstypes self.spectrans = None # Set the spectral translation self.skyout = None # Must be set before call to set_imageaxes self.boxdat = self.dat # We set two axes as the default axes of an image. Then the attribute # 'convproj' becomes available after a call to set_imageaxes() self.set_imageaxes(self.axperm[0], self.axperm[1], self.slicepos) self.aspectratio = None self.pixelaspectratio = self.get_pixelaspectratio() self.figsize = None # TODO is dit nog belangrijk??
[docs] def slice2world(self, skyout=None, spectra=None, userunits=None): #----------------------------------------------------------------- """ Given the pixel coordinates of a slice, return the world coordinates of these pixel positions and their units. For example in a 3-D radio cube with axes RA-DEC-FREQ one can have several RA-DEC images as function of FREQ. This FREQ is given in pixels coordinates in attribute *slicepos*. The world coordinates are calculated using the Projection object which is also an attribute. :param skyout: Set current projection object in new output sky mode :type skyout: String or tuple representing sky definition :param spectra: Use this spectral translation for the output world coordinates :type spectra: String :param userunits: A sequence of units as the user wants to have it appear in the slice info string. The order of these units must be equal to the order of the axes outside the slice/subset. Both the world coordinates and the units are adjusted. :type userunits: String :Returns: A tuple with two elements: *world* and *units*. Element *world* is either an empty list or a list with one or more world coordinates. The number of coordinates is equal to the number of axes in a data set that do not belong to the extracted data which can be a slice. For each world coordinate there is a unit in element *units*. :Note: This method first calculates a complete set of world coordinates. Where it did not define a slice position, it takes the header value *CRPIXn*. So if a map is defined with only one spatial axes and the missing spatial axis is found in *slicepos* than we have two matching pixel coordinates for which we can calculate world coordinates. So by definition, if a slice is a function of a spatial coordinate, then its world coordinate is found by using the matching pixel coordinate which, in case of a spatial map, corresponds to the projection center. :Example: >>> vel, uni = fitsobj.slice2world(spectra="VOPT-???") >>> velinfo = "ch%d = %.1f km/s" % (ch, vel[0]/1000.0) or: >>> vel, uni = fitsobj.slice2world(spectra="VOPT-???", userunits="km/s") """ #----------------------------------------------------------------- # Is there something to do? if self.slicepos is None: return None, None pix = [] units = [] world = [] j = 0 skyout_old = self.proj.skyout if skyout is not None: self.proj.skyout = skyout if spectra is not None: newproj = self.proj.spectra(spectra) else: newproj = self.proj for i in range(self.naxis): if (i+1) in self.axperm: pix.append(self.proj.crpix[i]) else: # Here we assume that slicepos is always a sequence, # even when there is only one repeat axis pix.append(self.slicepos[j]) j += 1 # Note that we assumed the axis order in slicepos is the same as in the Projection object #s = "TEST set 1dim tuple pix=%s"%str(tuple(pix)) #flushprint(s) wor = newproj.toworld(tuple(pix)) for i in range(self.naxis): if not (i+1) in self.axperm: world.append(wor[i]) units.append(newproj.cunit[i]) if skyout is not None: self.proj.skyout = skyout_old # This method accepts a tuple with units given by a user. # The units are entered in the same order as the axes outside a subset/slice # Scaled versions if not userunits is None: sworld = [] sunits = [] if not issequence(userunits): userunits = [userunits] for w, u, uu in zip(world, units, userunits): if not uu is None: uf, errmes = unitfactor(u, uu) if uf is None: raise ValueError(errmes) else: w *= uf u = uu sworld.append(w) sunits.append(u) else: # The original (native) values and units sworld = world sunits = units return sworld, sunits
[docs] def get_dataminmax(self, box=False): #------------------------------------------------------------ """ Get minimum and maximum value of data in entire data structure defined by the current FITS header or in a slice. These values can be important if you want to compare different images from the same source (e.g. channel maps in a radio data cube). :param box: Find min, max in data or if set to True in data slice (with limits). :type box: Boolean :Returns: min, max, two floating point numbers representing the minimum and maximum data value in data units of the header (*BUNIT*). :Note: We assume here that the data is read when a FITSobject was created. Then the data is filtered and the -inf, inf values are replaced by NaN's. :Example: Note the difference between the min, max of the entire data or the min, max of the slice (limited by a box):: fitsobj = maputils.FITSimage('ngc6946.fits') vmin, vmax = fitsobj.get_dataminmax() for i, ch in enumerate(channels): fitsobj.set_imageaxes(lonaxnum, lataxnum, slicepos=ch) print("Min, max in this channel: ", fitsobj.get_dataminmax(box=True)) """ #------------------------------------------------------------ if box: if self.boxdat is None: return 0, 1 #mi = numpy.nanmin(self.boxdat) #ma = numpy.nanmax(self.boxdat) mask = numpy.isfinite(self.boxdat) mi = numpy.min(self.boxdat[mask]) ma = numpy.max(self.boxdat[mask]) else: #mi = numpy.nanmin(self.dat) #ma = numpy.nanmax(self.dat) mask = numpy.isfinite(self.dat) mi = numpy.min(self.dat[mask]) ma = numpy.max(self.dat[mask]) return mi, ma
[docs] def str_header(self): #------------------------------------------------------------ """ Print the meta information from the selected header. Omit items of type *HISTORY*. It prints both real FITS headers and headers given by a dictionary. :Returns: A string with the header keywords :Examples: If you think a user needs more information from the header than can be provided with method :meth:`str_axisinfo` it can be useful to display the contents of the selected FITS header. This is the entire header and not a selected alternate header. >>> from kapteyn import maputils >>> fitsobject = maputils.FITSimage('rense.fits') >>> print(fitsobject.str_header()) SIMPLE = T / SIMPLE FITS FORMAT BITPIX = -32 / NUMBER OF BITS PER PIXEL NAXIS = 3 / NUMBER OF AXES NAXIS1 = 100 / LENGTH OF AXIS NAXIS2 = 100 / LENGTH OF AXIS NAXIS3 = 101 / LENGTH OF AXIS BLOCKED = T / TAPE MAY BE BLOCKED CDELT1 = -7.165998823000E-03 / PRIMARY PIXEL SEPARATION CRPIX1 = 5.100000000000E+01 / PRIMARY REFERENCE PIXEL CRVAL1 = -5.128208479590E+01 / PRIMARY REFERENCE VALUE CTYPE1 = 'RA---NCP ' / PRIMARY AXIS NAME CUNIT1 = 'DEGREE ' / PRIMARY AXIS UNITS etc. etc. """ #------------------------------------------------------------ st = '' if isinstance(self.hdr, dict): keylist = list(self.hdr.keys()) keylist.sort() for k in keylist: s = self.hdr[k] if not str(s).startswith('HISTORY'): if isinstance(s, str): val = "'" + "%-18s"%s + "'" else: val = "%+20s"%str(s) st += "%-8s"%k + "= " + val + " /\n" else: for s in self.hdr.cards: #VOG: previously ascardlist(): if not str(s).startswith('HISTORY'): st += "%s\n" % s return st
[docs] def str_axisinfo(self, axnum=None, long=False): #------------------------------------------------------------ """ For each axis in the FITS header, return a string with the data related to the World Coordinate System (WCS). :param axnum: A list with axis numbers for which one wants to print information. These axis numbers are FITS numbers i.e. in range [1,NAXIS]. To display information about the two image axes one should use attribute :attr:`maputils.FITSimage.axperm` as in the second example below. :type axnum: None, Integer or list with Integers :param long: If *True* then more verbose information is printed. :type long: Boolean :Returns: A string with WCS information for each axis in *axnum*. :Examples: Print useful header information after the input of the FITS file and just before the specification of the image axes: >>> from kapteyn import maputils >>> fitsobject = maputils.FITSimage('rense.fits') >>> print(fitsobject.str_axisinfo()) Axis 1: RA---NCP from pixel 1 to 100 {crpix=51 crval=-51.2821 cdelt=-0.007166 (DEGREE)} {wcs type=longitude, wcs unit=deg} Axis 2: DEC--NCP from pixel 1 to 100 {crpix=51 crval=60.1539 cdelt=0.007166 (DEGREE)} {wcs type=latitude, wcs unit=deg} Axis 3: VELO-HEL from pixel 1 to 101 {crpix=-20 crval=-243 cdelt=4200 (km/s)} {wcs type=spectral, wcs unit=m/s} Print extended information for the two image axes only: >>> print(str_axisinfo(axnum=fitsobject.axperm, long=True)) :Notes: For axis numbers outside the range of existing axes in the FITS file, nothing will be printed. No exception will be raised. """ #------------------------------------------------------------ if axnum is None: axnum = list(range(1, self.naxis+1)) if not issequence(axnum): axnum = [axnum] s = '' l = len(axnum) for i, ax in enumerate(axnum): # Note that the dictionary is unsorted. We want axes 1,2,3,... if ax >= 1 and ax <= self.naxis: a = self.axisinfo[ax] if int: s += a.printattr() else: s += a.printinfo() if i < l-1: s += '\n' return s
[docs] def str_wcsinfo(self): #------------------------------------------------------------ """ Compose a string with information about the data related to the current World Coordinate System (WCS) (e.g. which axes are longitude, latitude or spectral axes) :Returns: String with WCS information for the current Projection object. :Examples: Print information related to the world coordinate system: >>> print(fitsobject.str_wcsinfo()) Current sky system: Equatorial reference system: ICRS Output sky system: Equatorial Output reference system: ICRS projection's epoch: J2000.0 Date of observation from DATE-OBS: 2002-04-04T09:42:42.1 Date of observation from MJD-OBS: None Axis number longitude axis: 1 Axis number latitude axis: 2 Axis number spectral axis: None Allowed spectral translations: None """ #------------------------------------------------------------ s = '' sys, ref, equinox, epoch = skyparser(self.convproj.skysys) if sys is not None: s += "Native sky system: %s\n" % skyrefsystems.id2fullname(sys) if ref is not None: s += "Native reference system: %s\n" % skyrefsystems.id2fullname(ref) if equinox is not None: s += "Native Equinox: %s\n" % equinox if epoch is not None: s += "Native date of observation: %s\n" % epoch sys, ref, equinox, epoch = skyparser(self.convproj.skyout) if sys is not None: s += "Output sky system: %s\n" % skyrefsystems.id2fullname(sys) if ref is not None: s += "Output reference system: %s\n" % skyrefsystems.id2fullname(ref) if equinox is not None: s += "Output Equinox: %s\n" % equinox if epoch is not None: s += "Output date of observation: %s\n" % epoch s += "Projection's epoch: %s\n" % self.convproj.epoch s += "Date of observation from DATE-OBS: %s\n" % self.convproj.dateobs s += "Date of observation from MJD-OBS: %s\n" % self.convproj.mjdobs s += "Axis number longitude axis: %s\n" % self.convproj.lonaxnum s += "Axis number latitude axis: %s\n" % self.convproj.lataxnum s += "Axis number spectral axis: %s\n" % self.convproj.specaxnum s += "Selected spectral translation: %s\n" % self.spectrans return s
[docs] def str_spectrans(self): #------------------------------------------------------------ """ Compose a string with the possible spectral translations for this data. :Returns: String with information about the allowed spectral translations for the current Projection object. :Examples: Print allowed spectral translations: >>> print(fitsobject.str_spectrans()) """ #------------------------------------------------------------ s = '' isspectral = False for ax in self.axperm: if self.axisinfo[ax].wcstype == 'spectral': isspectral = True if not isspectral: return # Silently i = 0 for st, un in self.convproj.altspec: s += "%d %s (%s)\n" % (i, st, un) i += 1 return s
def getaxnumberbyname(self, axname): #-------------------------------------------------------------- """ Given an axis specification, convert to the corresponding axis number. If the input was a number, then return this number. Note that there is no check on multiple matches of the minimal matched string. The string matching is case insensitive. :param axname: The name of one of the axis in the header of this object. :type axname: String or Integer """ #-------------------------------------------------------------- if not isinstance(axname, str): # Probably an integer return axname n = self.naxis # Get the axis number axnumber = None for i in range(n): ax = i + 1 str2 = self.axisinfo[ax].axname if str2.find(axname.upper(), 0, len(axname)) > -1: axnumber = ax break # Check on validity if axnumber is None: raise ValueError("Axis name [%s] is not found in the header"%axname) return axnumber
[docs] def set_imageaxes(self, axnr1=None, axnr2=None, slicepos=None, promptfie=None): #-------------------------------------------------------------- """ A FITS file can contain a data set of dimension n. If n < 2 we cannot display the data without more information. If n == 2 the data axes are those in the FITS file, Their numbers are 1 and 2. If n > 2 then we have to know the numbers of those axes that are part of the image. For the other axes we need to know a pixel position so that we are able to extract a data slice. Attribute :attr:`dat` is then always a 2D array. :param axnr1: Axis number of first image axis (X-axis). If it is a string, then the number of the first axis which matches is returned. The string match is minimal and case insensitive. :type axnr1: Integer or String :param axnr2: Axis number of second image axis (Y-axis). If it is a string, then the number of the first axis which matches is returned. The string match is minimal and case insensitive. :type axnr2: Integer or String :param slicepos: list with pixel positions on axes outside the image at which an image is extracted from the data set. Applies only to data sets with dimensions > 2. The length of the list must be equal to the number of axes in the data set that are not part of the image. :type slicepos: Integer or sequence of integers :param spectrans: The spectral translation to convert between different spectral types if one of the image axes has spectral type. :type spectrans: Integer :param promptfie: A Function that for in an Interactive Environment (fie), supplied by the user, that can prompt a user to enter the values for axnr1, axnr2 and slicepos. An example of a function supplied by :mod:`maputils` is function :func:`prompt_imageaxes` :Raises: :exc:`Exception` *One axis number is missing and no prompt function is given!* :exc:`Exception` *Missing positions on axes outside image!* -- Somehow there are not enough elements in parameter *slicepos*. One should supply as many pixel positions as there are axes in the FITS data that do not belong to the selected image. :exc:`Exception` *Cannot find a matching axis for the spatial axis!* -- The matching spatial axis for one of the image axes could not be found in the FITS header. It will not be possible to get useful world coordinates for the spatial axis in your image. **Modifies attributes:** .. attribute:: axisinfo :noindex: A dictionary with objects from class :class:`FITSaxis`. One object for each axis. The dictionary keys are the axis numbers. See also second example at method :meth:`FITSaxis.printattr`. .. attribute:: allowedtrans A list with strings representing the spectral translations that are possible for the current image axis selection. .. attribute:: spectrans :noindex: The selected spectral translation .. attribute:: slicepos :noindex: One integer or a list with integers that represent pixel positions on axes in the data set that do not belong to the image. At these positions, a slice with image data is extracted. .. attribute:: map Image data from the selected FITS file. It is always a 2D data slice and its size can be found in attribute :attr:`imshape`. .. attribute:: imshape :noindex: The shape of the array :attr:`map`. .. attribute:: mixpix :noindex: Images with only one spatial axis, need another spatial axis to produces useful world coordinates. This attribute is extracted from the relevant axis in attribute :attr:`slicepos`. .. attribute:: convproj :noindex: An object from class Projection as defined in :mod:`wcs`. .. attribute:: axperm :noindex: The axis numbers corresponding with the X-axis and Y-axis in the image. :Note: The aspect ratio is reset (to *None*) after each call to this method. :Examples: Set the image axes explicitly: >>> fitsobject = maputils.FITSimage('rense.fits') >>> fitsobject.set_imageaxes(1,2, slicepos=30) Set the images axes in interaction with the user using a prompt function: >>> fitsobject = maputils.FITSimage('rense.fits') >>> fitsobject.set_imageaxes(promptfie=maputils.prompt_imageaxes) Enter (part of) the axis names. Note the minimal matching and case insensitivity. >>> fitsobject = maputils.FITSimage('rense.fits') >>> fitsobject.set_imageaxes('ra','d', slicepos=30) """ #----------------------------------------------------------------- axnr1 = self.getaxnumberbyname(axnr1) axnr2 = self.getaxnumberbyname(axnr2) n = self.naxis if n >= 2: if (axnr1 is None or axnr2 is None) and promptfie is None: if (axnr1 is None and axnr2 is None): axnr1 = self.axperm[0] axnr2 = self.axperm[1] else: raise Exception("One axis number is missing and no prompt function is given!") if slicepos is None and promptfie is None: slicepos = self.slicepos # If there is a spectral axis in the FITS file, then get allowed # spectral translations self.allowedtrans = self.proj.altspec if promptfie is not None: axnr1, axnr2, self.slicepos = promptfie(self, axnr1, axnr2) else: if slicepos is not None and n > 2: if issequence(slicepos): self.slicepos = slicepos else: self.slicepos = [slicepos] k = 0 for i in range(n): axnr = i + 1 if axnr not in [axnr1, axnr2]: self.sliceaxnames[k] = self.axisinfo[axnr].axname k += 1 axperm = [axnr1, axnr2] wcsaxperm = [axnr1, axnr2] # User could have changed the order of which these axes appear # in the FITS header. Sort them to assure the right order. axperm.sort() if n > 2: # Get the axis numbers of the other axes if len(self.slicepos) != n-2: raise Exception("Missing positions on axes outside image!") j = 0 for i in range(n): axnr = i + 1 if axnr != axnr1 and axnr != axnr2: axperm.append(axnr) self.axisinfo[axnr].outsidepix = self.slicepos[j] j += 1 # Create a slice if the data has more dimensions than 2 # Example: Enter pixel position on RA: 30 # Slice = [slice(None, None, None), slice(None, None, None), slice(30, 31, None)] # Fastest axis last if n > 2: sl = [] for ax in range(n,0,-1): # e.g. ax = 3,2,1 if (ax == axnr1 or ax == axnr2): sl.append(slice(None)) else: g = int(self.axisinfo[ax].outsidepix) sl.append(slice(g-1, g)) # Slice indices start with 0, pixels with 1 # Reshape the array, assuming that the other axes have length 1 # You can reshape with the shape attribute or with NumPy's squeeze method. With # squeeze there is no way to find images axis which have also length 1 so we use # the reshape() method. if self.dat is not None: axlen1 = int(self.axisinfo[axnr1].axlen) axlen2 = int(self.axisinfo[axnr2].axlen) if axperm[0] != wcsaxperm[0]: #self.boxdat = self.dat[sl].reshape((self.axisinfo[axnr1].axlen,self.axisinfo[axnr2].axlen)) self.boxdat = self.dat[tuple(sl)].reshape( (axlen1,axlen2) ) else: #self.boxdat = self.dat[sl].reshape((self.axisinfo[axnr2].axlen,self.axisinfo[axnr1].axlen)) self.boxdat = self.dat[tuple(sl)].reshape( (axlen2,axlen1) ) else: self.boxdat = self.dat if self.boxdat is not None: self.imshape = self.boxdat.shape if axperm[0] != wcsaxperm[0]: # The x-axis should be the y-axis vv. self.boxdat = numpy.swapaxes(self.boxdat, 0,1) # Swap the x- and y-axes self.imshape = self.boxdat.shape if axperm[0] != wcsaxperm[0]: # The x-axis should be the y-axis vv. axperm[0] = wcsaxperm[0] # Return the original axis permutation array axperm[1] = wcsaxperm[1] n1 = self.axisinfo[axperm[0]].axlen n2 = self.axisinfo[axperm[1]].axlen self.pxlim = [1, n1] self.pylim = [1, n2] #------------------------------------------------------------------ # Here we do some analysis of the axes. We can think of many # combinations of axes, but not all will be valid for WCSlib # WCSlib recognizes a longitude axis, a latitude axis, a spectral # axis and other types are identified with None. A spatial axis # ALWAYS needs a matching spatial axis. For the missing axis # we only know the pixel position and we use the mixed projection # method to find the corresponding world coordinate. # We distinguish the following situations: # 1) (Lon,Lat) or (Lat,Lon) ==> Nothing to do # 2) (Lon,X) or (X,Lon) ==> Look in axis dict for Lat axis # 3) (Lat,X) or (X,Lat) ==> Look in axis dict for Lon axis # 4) (X,Spec) or (Spec,X) or (X,Y). ==> Nothing to do # In cases 1 and 4 return first two elements from axperm array # In cases 2 and 3 return first two elements from axperm array # and the third element is the axis number of the missing axis. #------------------------------------------------------------------ self.mixpix = None ap = wcsaxperm # which elements could have been swapped mix = False matchingaxnum = None if n > 2: ax1 = wcsaxperm[0]; ax2 = wcsaxperm[1] if ax1 == self.proj.lonaxnum and ax2 != self.proj.lataxnum: matchingaxnum = self.proj.lataxnum mix = True elif ax1 == self.proj.lataxnum and ax2 != self.proj.lonaxnum: matchingaxnum = self.proj.lonaxnum mix = True if ax2 == self.proj.lonaxnum and ax1 != self.proj.lataxnum: matchingaxnum = self.proj.lataxnum mix = True elif ax2 == self.proj.lataxnum and ax1 != self.proj.lonaxnum: matchingaxnum = self.proj.lonaxnum mix = True if mix: if matchingaxnum is not None: self.mixpix = self.axisinfo[matchingaxnum].outsidepix ap = (axperm[0], axperm[1], matchingaxnum) else: raise Exception("Cannot find a matching axis for the spatial axis!") else: ap = (axperm[0], axperm[1]) # If a spectral translation is needed, then we must apply the spectra() # method on the original projection object, which contains the spectral axis # The method does not work if the projection object is restricted to # two non spectral axes. if self.spectrans is not None: self.proj.spectra(self.spectrans) self.convproj = self.proj.sub(ap) # Projection object for selected image only #if self.spectrans != None: # self.convproj = self.convproj.spectra(self.spectrans) if self.skyout is not None: self.convproj.skyout = self.skyout self.axperm = wcsaxperm # We need only the numbers of the first two axes self.aspectratio = None # Reset the aspect ratio because we could have another image now
[docs] def set_spectrans(self, spectrans=None, promptfie=None): #-------------------------------------------------------------- """ Set spectral translation or ask user to enter a spectral translation if one of the axes in the current FITSimage is spectral. :param spectrans: A spectral translation e.g. to convert frequencies to optical velocities. :type spectrans: String :param promptfie: A function, supplied by the user, that can prompt a user to enter a sky definition. :type promptfie: A Python function without parameters. It returns a string with the spectral translation. An example of a function supplied by :mod:`maputils` is function :func:`prompt_spectrans` :Examples: Set a spectral translation using 1) a prompt function, 2) a spectral translation for which we don't know the code for the conversion algorithm and 3) set the translation explicitly: >>> fitsobject.set_spectrans(promptfie=maputils.prompt_spectrans) >>> fitsobject.set_spectrans(spectrans="VOPT-???") >>> fitsobject.set_spectrans(spectrans="VOPT-V2W") """ #-------------------------------------------------------------- isspectral = False for ax in range(1,self.naxis+1): if self.axisinfo[ax].wcstype == 'spectral': isspectral = True if not isspectral: return # Silently if promptfie is None: #if spectrans is None: # raise Exception, "No spectral translation given!" #else: # Allow for None self.spectrans = spectrans else: self.spectrans = promptfie(self) if self.spectrans is not None: self.convproj = self.convproj.spectra(self.spectrans)
[docs] def set_skyout(self, skyout=None, promptfie=None): #-------------------------------------------------------------- """ Set the output sky definition. Mouse positions and coordinate labels will correspond to the selected definition. The method will only work if both axes are spatial axes. :param skyout: The output sky definition for sky system, reference system, equinox and date of observation. For the syntax of a sky definition see the description at :meth:`kapteyn.celestial.skymatrix` :type skyout: A single value or tuple. :param promptfie: A function, supplied by the user, that can prompt a user to enter a sky definition. :type promptfie: A Python function without parameters. It returns the sky definition. An example of a function supplied by :mod:`maputils` is function :func:`prompt_skyout` :Notes: The method sets an output system only for data with two spatial axes. For XV maps the output sky system is always the same as the native system. """ #-------------------------------------------------------------- spatials = [self.proj.lonaxnum, self.proj.lataxnum] spatialmap = self.axperm[0] in spatials and self.axperm[1] in spatials if not spatialmap: return # Silently if promptfie is None: if skyout is None: raise Exception("No definition for the output sky is given!") else: self.skyout = skyout else: self.skyout = promptfie(self) if self.skyout is not None: self.convproj.skyout = self.skyout
[docs] def set_limits(self, pxlim=None, pylim=None, promptfie=None): #--------------------------------------------------------------------- """ This method sets the image box. That is, it sets the limits of the image axes in pixels. This can be a useful feature if one knows which part of an image contains the interesting data. :param pxlim: Two integer numbers which should not be smaller than 1 and not bigger than the header value *NAXISn*, where n represents the x axis. :type pxlim: Tuple with two integers :param pylim: Two integer numbers which should not be smaller than 1 and not bigger than the header value *NAXISn*, where n represents the y axis. :type pylim: Tuple with two integers :param promptfie: An external function with parameters *pxlim*, *pylim*, *axnameX*, and *axnameY* which are used to compose a prompt. If a function is given then there is no need to enter *pxlim* and *pylim*. The prompt function must return (new) values for *pxlim* and *pylim*. An example of a function supplied by :mod:`maputils` is function :func:`prompt_box` :type promptfie: Python function :Examples: Ask user to enter limits with prompt function :func:`prompt_box` >>> fitsobject = maputils.FITSimage('rense.fits') >>> fitsobject.set_imageaxes(1,2, slicepos=30) # Define image in cube >>> fitsobject.set_limits(promptfie=maputils.prompt_box) """ #--------------------------------------------------------------------- n1 = int(self.axisinfo[self.axperm[0]].axlen) n2 = int(self.axisinfo[self.axperm[1]].axlen) npxlim = [None,None] npylim = [None,None] if pxlim is None: npxlim = [1, n1] else: if not issequence(pxlim): raise Exception("pxlim must be tuple or list!") npxlim[0] = int(nint(pxlim[0])) npxlim[1] = int(nint(pxlim[1])) if pylim is None: npylim = [1, n2] else: if not issequence(pylim): raise Exception("pylim must be tuple or list!") npylim[0] = int(nint(pylim[0])) npylim[1] = int(nint(pylim[1])) if promptfie is not None: axname1 = self.axisinfo[self.axperm[0]].axname axname2 = self.axisinfo[self.axperm[1]].axname npxlim, npylim = promptfie(self.pxlim, self.pylim, axname1, axname2) # Check whether these values are within the array limits if npxlim[0] < 1: npxlim[0] = 1 if npxlim[1] > n1: npxlim[1] = n1 if npylim[0] < 1: npylim[0] = 1 if npylim[1] > n2: npylim[1] = n2 # Get the subset from the (already) 2-dim array if self.boxdat is not None: self.boxdat = self.boxdat[npylim[0]-1:npylim[1], npxlim[0]-1:npxlim[1]] # map is a subset of the original (squeezed into 2d) image self.imshape = self.boxdat.shape self.pxlim = npxlim self.pylim = npylim self.axisinfo[self.axperm[0]].axstart = npxlim[0] self.axisinfo[self.axperm[0]].axend = npxlim[1] self.axisinfo[self.axperm[1]].axstart = npylim[0] self.axisinfo[self.axperm[1]].axend = npylim[1]
[docs] def get_pixelaspectratio(self): #--------------------------------------------------------------------- """ Return the aspect ratio of the pixels in the current data structure defined by the two selected axes. The aspect ratio is defined as *pixel height / pixel width*. :Example: >>> fitsobject = maputils.FITSimage('m101.fits') >>> print(fitsobject.get_pixelaspectratio()) 1.0002571958 :Note: If a header has only a cd matrix and no values for CDELT, then these values are set to 1. This gives an aspect ratio of 1. """ #--------------------------------------------------------------------- a1 = self.axperm[0]; a2 = self.axperm[1]; cdeltx = self.axisinfo[a1].cdelt cdelty = self.axisinfo[a2].cdelt nx = float(self.pxlim[1] - self.pxlim[0] + 1) ny = float(self.pylim[1] - self.pylim[0] + 1) # TODO do a better job calculating the aspectratio from a CD matrix. if self.proj.lonaxnum in self.axperm and self.proj.lataxnum in self.axperm: # Then is it a spatial map aspectratio = abs(cdelty/cdeltx) else: aspectratio = nx/ny self.pixelaspectratio = aspectratio return aspectratio
[docs] def get_figsize(self, xsize=None, ysize=None, cm=False): #--------------------------------------------------------------------- """ Usually a user will set the figure size manually with Matplotlib's figure(figsize=...) construction. For many plots this is a waste of white space around the plot. This can be improved by taking the aspect ratio into account and adding some extra space for labels and titles. For aspect ratios far from 1.0 the number of pixels in x and y are taken into account. A handy feature is that you can enter the two values in centimeters if you set the flag *cm* to True. If you have a plot which is higher than its width and you want to fit in on a A4 page then use: >>> f = maputils.FITSimage(externalheader=header) >>> figsize = f.get_figsize(ysize=21, cm=True) >>> fig = plt.figure(figsize=figsize) >>> frame = fig.add_subplot(1,1,1) """ #--------------------------------------------------------------------- if xsize is not None and not cm: xsize *= 2.54 if ysize is not None and not cm: ysize *= 2.54 if xsize is not None and ysize is not None: return (xsize/2.54, ysize/2.54) a1 = self.axperm[0]; a2 = self.axperm[1]; cdeltx = self.axisinfo[a1].cdelt cdelty = self.axisinfo[a2].cdelt nx = float(self.pxlim[1] - self.pxlim[0] + 1) ny = float(self.pylim[1] - self.pylim[0] + 1) aspectratio = abs(cdelty/cdeltx) if aspectratio > 10.0 or aspectratio < 0.1: aspectratio = nx/ny extraspace = 3.0 # cm if xsize is None and ysize is None: if abs(nx*cdeltx) >= abs(ny*cdelty): xsize = 21.0 # A4 width else: ysize = 21.0 if xsize is not None: # abs(nx*cdeltx) >= abs(ny*cdelty): xcm = xsize # The extra space is to accommodate labels and titles ycm = xcm * (ny/nx) * aspectratio + extraspace else: ycm = ysize xcm = ycm * (nx/ny) / aspectratio + extraspace return (xcm/2.54, ycm/2.54)
[docs] def header2classic(self): #-------------------------------------------------------------------- """ If a header contains PC or CD elements, and not all the 'classic' elements for a WCS then a number of FITS readers could have a problem if they don't recognize a PC and CD matrix. What can be done is to derive the missing header items, CDELTn and CROTA from these headers and add them to the header. What is a 'classic' FITS header? (See also http://fits.gsfc.nasa.gov/fits_standard.html) For the transformation between pixel coordinates and world coordinates, FITS supports three conventions. First some definitions: An intermediate pixel coordinate :math:`q_i` is calculated from a pixel coordinates :math:`p` with: .. math:: q_i = \sum_{j=1}^N m_{ij}(p_j-r_j) Rj are the pixel coordinate elements of a reference point (FITS header item CRPIXj), j is an index for the pixel axis and i for the world axis The matrix :math:`m_{ij}` must be non-singular and its dimension is NxN where N is the number of world coordinate axes (given by FITS header item NAXIS). The conversion of :math:`q_i` to intermediate world coordinate :math:`x_i` is a scale :math:`s_i`: .. math:: x_i = s_i q_i **Formalism 1 (PC keywords)** Formalism 1 encodes :math:`m_{ij}` in so called PCi_j keywords and scale factor :math:`s_i` are the values of the CDELTi keywords from the FITS header. It is obvious that the value of CDELT should not be 0.0. **Formalism 2 (CD keywords)** If the matrix and scaling are combined we get for the intermediate WORLD COORDINATE :math:`x_i`: .. math:: x_i = \sum_{j=1}^N (s_i m_{ij})(p_j-r_j) FITS keywords CDi_j encodes the product :math:`s_i m_{ij}`. The units of :math:`x_i` are given by FITS keyword CTYPEi. **Formalism 3 (Classic)** This is the oldest but now deprecated formalism. It uses CDELTi for the scaling and CROTAn for a rotation of the image plane. n is associated with the latitude axis so often one sees CROTA2 in the header if the latitude axis is the second axis in the dataset Following the FITS standard, a number of rules is set: 1. CDELT and CROTA may co-exist with the CDi_j keywords but must be ignored if an application supports the CD formalism. 2. CROTAn must not occur with PCi_j keywords 3. CRPIXj defaults to 0.0 4. CDELT defaults to 1.0 5. CROTA defaults to 0.0 6. PCi_j defaults to 1 if i==j and to 0 otherwise. The matrix must not be singular 7. CDi_j defaults to 0.0. The matrix must not be singular. 8. CDi_j and PCi_j must not appear together in a header. **Alternate WCS axis descriptions** A World Coordinate System (WCS) can be described by an alternative set of keywords. For this keywords a character in the range [A..Z] is appended. In our software we made the assumption that the primary description contains all the necessary information to derive a 'classic' header and therefore we will ignore alternate header descriptions. **Conversion to a formalism 3 ('classic') header** Many FITS readers from the past are not upgraded to process FITS files with headers written using formalism 1 or 2. The purpose of this application is to convert a FITS file to a file that can be read and interpreted by old FITS readers. For GIPSY we require FITS headers to be written using formalism 3. If keywords are missing, they will be derived and a comment will be inserted about the keyword not being an original keyword. The method that converts the header, tries to process it first with WCSLIB (tools to interpret the world coordinate system as described in a FITS header). If this fails, then we are sure that the header is incorrect and we cannot proceed. One should use a FITS tool like 'fv' (the Interactive FITS File Editor from Nasa) to repair the header. The conversion process starts with exploring the spatial part of the header. It knows which axes are spatial and it reads the corresponding keywords (CDELTi, CROTAn, CDi_j, PCi_j and PC00i_00j (old format for PC elements). If there is no CD or PC matrix, then the conversion routine returns the unaltered original header. If it finds a PC matrix and no CD matrix then the header should contain CDELT keywords. With the values of these keywords we create a CD matrix: .. math:: \\begin{bmatrix}cd_{11} & cd_{12}\\\ cd_{21} & cd_{22}\\end{bmatrix} = \\begin{bmatrix}cdelt_1 & 0\\\ 0 & cdelt_2 \\end{bmatrix} \\begin{bmatrix}pc_{11} & pc_{12}\\\ pc_{21} & pc_{22}\\end{bmatrix} Notes: * We replaced notation i_j by ij so cd11 == CD1_1 * For the moment we restricted the problem to the 2 dim. spatial case because that is what we need to retrieve a value for CROTA, the rotation of the image.) * We assumed that the PC matrix did not represent transposed axes as in: .. math:: PC = \\begin{bmatrix}0 & 1 & 0\\\ 0 & 0 & 1\\\ 1 & 0 & 0 \\end{bmatrix} If cd12 == 0.0 and cd12 == 0.0 then CROTA is obviously 0. There is no rotation and CDELT1 = cd11, CDELT2 = cd22 If one or both values of cd12, cd21 is not zero then we expect a value for CROTA unequal to zero and/or skew. We calculate the scaling parameters CDELT with:: CDELT1 = sqrt(cd11*cd11+cd21*cd21) CDELT2 = sqrt(cd12*cd12+cd22*cd22) The determinant of the matrix is:: det = cd11*cd22 - cd12*cd21 This value cannot be 0 because we required that the matrix is non-singular. Further we distinguish two situations: a determinant < 0 and a determinant > 0 (zero was already excluded). Then we derive two rotations. If these are equal, the image is not skewed. If they are not equal, we derive the rotation from the average of the two calculated angles. As a measure of skew, we calculated the difference between the two rotation angles. Here is a piece of the actual code:: sign = 1.0 if det < 0.0: cdeltlon_cd = -cdeltlon_cd sign = -1.0 rot1_cd = atan2(-cd21, sign*cd11) rot2_cd = atan2(sign*cd12, cd22) rot_av = (rot1_cd+rot2_cd)/2.0 crota_cd = degrees(rot_av) skew = degrees(abs(rot1_cd-rot2_cd)) New values of CDELT and CROTA will be inserted in the new 'classic' header only if they were not part of the original header. The process continues with non spatial axes. For these axes we cannot derive a rotation. We only need to find a CDELTi if for axis i no value could be found in the header. If this value cannot be derived from the a CD matrix (usually with diagonal element CDi_i) then the default 1.0 is assumed. Note that there will be a warning about this in the comment string for the corresponding keyword in the new 'classic' FITS header. Finally there is some cleaning up. First all CD/PC elements are removed for all the axes in the data set. Second, some unwanted keywords are removed. The current list is: ["XTENSION", "EXTNAME", "EXTEND"] See also: Calabretta & Greisen: 'Representations of celestial coordinates in FITS', section 6 :param -: No parameters :Returns: A tuple with three elements: * *hdr* - A modified copy of the current header. The CD and PC elements are removed. * *skew* - Difference between the two calculated rotation angles If this number is bigger then say 0.001 then there is considerable skew in the data. One should reproject the data so that it fits a non skewed version with only a CROTA in the header * *hdrchanged* - A list with keywords the are changed when a 'classic' header is required. :Notes: This method is tested with FITS files: * With classic header * With only CD matrix * With only PC matrix * With PC and CD matrix * With CD matrix and NAXIS > 2 * With sample files with skew :Example: :: from kapteyn import maputils, wcs import pyfits Basefits = maputils.FITSimage(promptfie=maputils.prompt_fitsfile) newheader, skew, hdrchanged = Basefits.header2classic() if len(hdrchanged): print(newheader) if skew != 0.0: print("found skew:", skew) """ #-------------------------------------------------------------------- skew = 0.0 hdrchanged = [] hdr = self.hdr.copy() if type(hdr) == 'dict': dicttype = True else: dicttype = False comment = "Appended by Kapteyn Package module Maputils %s" % datetime.now().strftime("%dd%mm%Yy%Hh%Mm%Ss") lonaxnum = self.proj.lonaxnum lataxnum = self.proj.lataxnum spatial = (lonaxnum is not None and lataxnum is not None) if spatial: cdeltlon = None cdeltlat = None crota = None key = "CDELT%d" % lonaxnum if key in hdr: cdeltlon = hdr[key] key = "CDELT%d" % lataxnum if key in hdr: cdeltlat = hdr[key] key = "CROTA%d" % lataxnum if key in hdr: crota = hdr[key] cd11 = cd12 = cd21 = cd22 = None CD = [0.0]*4 # Unspecified elements default to 0.0 G+C paper I k = 0 cdmatrix = False for i in [lonaxnum, lataxnum]: for j in [lonaxnum, lataxnum]: key = "CD%d_%d" % (i, j) if key in hdr: CD[k] = hdr[key] del hdr[key] hdrchanged.append(key) cdmatrix = True k += 1 cd11, cd12, cd21, cd22 = CD # Clean up CROTA's, often there are more than one in the same header key = "CROTA%d" % (lonaxnum) if key in hdr: del hdr[key] pcmatrix = False PC = [None]*4 k = 0 for i in [lonaxnum, lataxnum]: for j in [lonaxnum, lataxnum]: # Set the defaults (i.e. 1 if i==j else 0) if i == j: PC[k] = 1.0 else: PC[k] = 0.0 key = "PC%d_%d" % (i, j) if key in hdr: PC[k] = hdr[key] del hdr[key] hdrchanged.append(key) pcmatrix = True k += 1 pc11, pc12, pc21, pc22 = PC pcoldmatrix = False # If no PC found then try legacy numbering if not pcmatrix: PCold = [None]*4 k = 0 for i in [lonaxnum, lataxnum]: for j in [lonaxnum, lataxnum]: if i == j: PCold[k] = 1.0 else: PCold[k] = 0.0 key = "PC%03d%03d" % (i, j) # Like 001, 002 if key in hdr: PCold[k] = hdr[key] del hdr[key] hdrchanged.append(key) pcoldmatrix = True k += 1 pco11, pco12, pco21, pco22 = PCold if not pcmatrix and pcoldmatrix: PC = PCold pc11, pc12, pc21, pc22 = PC pcmatrix = True # If the CD is empty but the PC exists, # then use the PC to create a CD. if not cdmatrix: if pcmatrix and None in [cdeltlon, cdeltlat]: # A PC matrix cannot exist without values for CDELT raise ValueError("Header does not contain necessary CDELT's!") if pcmatrix: # Found no CD but use PC to create one # |cd11 cd12| = |cdelt1 0| * |pc11 pc12| # |cd21 cd22| |0 cdelt2| |pc21 pc22| cd11 = cdeltlon*pc11 cd12 = cdeltlon*pc12 cd21 = cdeltlat*pc21 cd22 = cdeltlat*pc22 CD = [cd11, cd12, cd21, cd22] cdmatrix = True # PC and CD should not appear both in the same header # but if it does, use the CD. # If there is no CD/PC matrix then there is nothing to do if not cdmatrix: return hdr, 0, [] else: from math import sqrt from math import atan2 from math import degrees, radians from math import cos, sin, acos if cd12 == 0.0 and cd21 == 0.0: crota_cd = 0.0 cdeltlon_cd = cd11 cdeltlat_cd = cd22 else: cdeltlon_cd = sqrt(cd11*cd11+cd21*cd21) cdeltlat_cd = sqrt(cd12*cd12+cd22*cd22) det = cd11*cd22 - cd12*cd21 if det == 0.0: raise ValueError("Determinant of CD matrix == 0") sign = 1.0 if det < 0.0: cdeltlon_cd = -cdeltlon_cd sign = -1.0 rot1_cd = atan2(-cd21, sign*cd11) rot2_cd = atan2(sign*cd12, cd22) rot_av = (rot1_cd+rot2_cd)/2.0 crota_cd = degrees(rot_av) skew = degrees(abs(rot1_cd-rot2_cd)) """ print("Angles from cd matrix:", degrees(rot1_cd), degrees(rot2_cd), crota_cd) print("Cdelt's from cd matrix:", cdeltlon_cd, cdeltlat_cd) print("Difference in angles (deg)", skew) """ # At this stage we have values for the CDELTs and CROTA derived # from the CD/PC matrix. If the corresponding header items are # not available, then put them in the header and flag the header as # altered. if cdeltlon is None: cdeltlon = cdeltlon_cd key = "CDELT%d" % lonaxnum # Create new one if necessary if dicttype: hdr.update(key=cdeltlon) else: hdr.update[key] = (cdeltlon, comment) hdrchanged.append(key) if cdeltlat is None: cdeltlat = cdeltlat_cd key = "CDELT%d" % lataxnum if dicttype: hdr.update(key=cdeltlat) else: hdr.update[key] = (cdeltlat, comment) hdrchanged.append(key) if crota is None: crota = crota_cd key = "CROTA%d" % lataxnum if dicttype: hdr.update(key=crota) else: hdr.update[key] = (crota, comment) hdrchanged.append(key) # What if the data was not spatial and what do we do with # the other non spatial axes in the header? # Start looping over other axes: wasdefault = False naxis = hdr['NAXIS'] if (spatial and naxis > 2) or not spatial: for k in range(naxis): axnum = k + 1 key = "CDELT%d" % axnum if not spatial or (spatial and axnum != lonaxnum and axnum != lataxnum): if not (key in hdr): key_cd = "CD%d_%d"%(axnum, axnum) # Diagonal elements only! if key_cd in hdr: newval = hdr[key_cd] else: # We have a problem. For this axis there is no CDELT # nor a CD matrix element. Set CDELT to 1.0 (FITS default) newval = 1.0 wasdefault = True if dicttype: hdr[key] = newval else: if wasdefault: s = "Inserted default 1.0! " + comment else: s = comment hdr.update[key] = (newval, s) hdrchanged.append(key) # Clean up left overs for i in range(naxis): for j in range(naxis): axi = i + 1; axj = j + 1 keys = ["CD%d_%d"%(axi, axj), "PC%d_%d"%(axi, axj), "PC%03d%03d"%(axi, axj)] for key in keys: if key in hdr: del hdr[key] hdrchanged.append(key) # Exceptions etc.: ekeys = ["XTENSION", "EXTNAME", "EXTEND"] # Possibly EXTEND will be re-inserted when writing the FITS file for key in ekeys: if key in hdr: del hdr[key] return hdr, skew, hdrchanged
[docs] def reproject_to(self, reprojobj=None, pxlim_dst=None, pylim_dst=None, plimlo=None, plimhi=None, interpol_dict = None, rotation=None, insertspatial=None, **fitskeys): #--------------------------------------------------------------------- """ The current FITSimage object must contain a number of spatial maps. This method then reprojects these maps so that they conform to the input header. Imagine an image and a second image of which you want to overlay contours on the first one. Then this method uses the current data to reproject to the input header and you will end up with a new FITSimage object which has the spatial properties of the input header and the reprojected data of the current FITSimage object. Also more complicated data structures can be used. Assume you have a data cube with axes RA, Dec and Freq. Then this method will reproject all its spatial subsets to the spatial properties of the input header. The current FITSimage object tries to keep as much of its original FITS keywords. Only those related to spatial data are copied from the input header. The size of the spatial map can be limited or extended. The axes that are not spatial are unaltered. The spatial information for both data structures are extracted from the headers so there is no need to specify the spatial parts of the data structures. The image that is reprojected can be limited in size with parameters pxlim_dst and pylim_dst. If the input is a FITSimage object, then these parameters (pxlim_dst and pylim_dst) are copied from the axes lengths set with method :meth:`set_limits()` for that FITSimage object. :param reprojobj: * The header which provides the new information to reproject to. The size of the reprojected map is either copied from the NAXIS keywords in the header or entered with parameters *pxlim_dst* and *pylim_dst*. The reprojections are done for all spatial maps in the current FITSimage object or for a selection entered with parameters *plimlo* and *plimhi* (see examples). * The FITSimage object from which relevant information is extracted like the header and the new sizes of the spatial axes which otherwise should have been provided in parameters *pxlim_dst* and *pylim_dst*. The reprojection is restricted to one spatial map and its slice information is copied from the current FITSimage. This option is selected if you want to overlay e.g. contours from the current FITSimage data onto data from another WCS. * If None, then the current header is used. Modifications to this header are done with keyword arguments. :type reprojobj: Python dictionary or PyFITS header. Or a :class:`maputils.FITSimage` object :param pxlim_dst: Limits in pixels for the reprojected box. :type pxlim_dst: Tuple of integers :param plimlo: One or more pixel coordinates corresponding to axes outside the spatial map in order as found in the header 'reprojobj'. The values set the lower limits of the axes. There is no need to specify all limits but the order is important. :type plimlo: Integer or tuple of integers :param plimhi: The same as plimhi, but now for the upper limits. :type plimhi: Integer or tuple of integers :param interpol_dict: This parameter is a dictionary with parameters for the interpolation routine which is used to reproject data. The interpolation routine is based on SciPy's *map_coordinates*. The most important parameters with the maputils defaults are: .. tabularcolumns:: |p{10mm}|p{100mm}| ========= =============================================================== order : Integer, optional The order of the spline interpolation, default is 1. The order has to be in the range 0-5. mode : String, optional Points outside the boundaries of the input are filled according to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). Default is 'constant'. cval : scalar, optional Value used for points outside the boundaries of the input if mode='constant'. Default is numpy.NaN ========= =============================================================== :type interpol_dict: Python dictionary :param rotation: Sets a rotation angle. If this method encounters this keyword, it will create a so called 'classic' header. That is a header without CD or PC elements. Then the rotation angle of the current spatial map is only given by FITS keyword *CROTAn*. The value of *rotation* is added to *CROTAn* to create a new value which is inserted in the new header. Note that values for *CROTAn* in the *fitskeys* parameter list overwrite this calculated value. :type rotation: Floating point number or *None* :param insertspatial: If True, then replace spatial part of current header by spatial part of new (destination) header. Assume you start with a data cube with a number of spatial maps as function of frequency (the third axis in the data set). If you use the header of another FITS file as the definition of a new world coordinate system, then it could be that this header represents a two dimensional data set. This is not an incompatible set because we only require a description of the spatial part. To keep the description of the original three dimensional structure we insert the new spatial(only) information into the current header. The default then is *insertspatial=True*. In other cases where we use the original header as the base header, one just inserts new values and there is no need to set insert something, so then the default is *insertspatial=False*. A user can change the default. This can be useful. For example in the maputils tutorial we have a script mu_reproj2classic.py where we use the header of a FITS file to make some changes and use the changed header as an external header to re-project to. The default then is *insertspatial=True*, but the external header is already a complete header, so there is no need to insert something. :type insertspatial: Boolean :param fitskeys: Parameters containing FITS keywords and values which are written in the reprojection header. :type fitskeys: Python keyword arguments. :Examples: -Set limits for axes outside the spatial map. Assume a data structure with axes RA-DEC-FREQ-STOKES for which the RA-DEC part is reprojected to a set RA'-DEC'-FREQ-STOKES. The ranges for FREQ and STOKES set the number of spatial maps in this data structure. One can limit these ranges with *plimlo* and *plimhi*. * *plimlo=(20,2)*, *plimhi=(40,2)* we restrict the reprojections for spatial maps at frequencies 20 to 40 at one position on the STOKES axis (at pixel coordinate 2). * *plimlo=(None,2)*, *plimhi=(None,2)* If one wants to reproject all the maps at all frequencies but only for STOKES=2 and 3 then use: *plimlo=(None,2)* and *plimhi=(None,2)* where None implies no limits. * *plimlo=40* No *plimhi* is entered. Then there are no upper limits. Only one value (40) is entered so this must represent the FREQ axis at pixel coordinate 40. It represents all spatial maps from FREQ pixel coordinate 40 to the end of the FREQ range, repeated for all pixels on the STOKES axis. * *plimlo=(55,1)*, *plimhi=(55,1)* This reprojects just one map at FREQ pixel coordinate 55 and STOKES pixel coordinate 1. This enables a user/programmer to extract one spatial map, reproject it and write it as a single map to a FITS file while no information about the FREQ and STOKES axes is lost. The dimensionality of the new data remains 4 but the length of the 'repeat axes' is 1. Note that if the data structure was represented by axes FREQ-RA-STOKES-DEC then the examples above are still valid because these set the limits on the repeat axes FREQ and POL whatever the position of these axes in the data structure. -Use and modify the current header to change the data. The example shows how to rotate an image and display the result. :: Basefits = maputils.FITSimage("m101.fits") Rotfits = Basefits.reproject_to(rotation=40.0, naxis1=800, naxis2=800, crpix1=400, crpix2=400) # If copy on disk required: # Rotfits.writetofits("m10rot.fits", overwrite=True, append=False) annim = Rotfits.Annotatedimage() annim.Image() annim.Graticule() annim.interact_toolbarinfo() maputils.showall() -Use an external header and change keywords in that header before the re-projection: >>> Rotfits = Basefits.reproject_to(externalheader, naxis1=800, naxis2=800, crpix1=400, crpix2=400) -Use the FITS file's own header. Change it and use it as an external header :: from kapteyn import maputils, wcs Basefits = maputils.FITSimage("m101.fits") classicheader, skew, hdrchanged = Basefits.header2classic() lat = Basefits.proj.lataxnum key = "CROTA%d"%lat classicheader[key] = 0.0 # New value for CROTA Basefits.reproject_to(classicheader, insertspatial=False) fnew = maputils.FITSimage(externalheader=classicheader, externaldata=Basefits.dat) fnew.writetofits("classic.fits", overwrite=True, append=False) :Tests: 1) The first test was a reprojection of data of *map1* to the spatial header of *map2*. One should observe that the result of the reprojection (*reproj*) has its spatial structure from *map2* and its non spatial structure (i.e. the repeat axes) from *map1*. Note that the order of longitude, latitude in *map1* is swapped in *map2*. map1: CTYPE: RA - POL - FREQ - DEC NAXIS 35 5 16 41 map2: CTYPE: DEC - POL - FREQ - RA NAXIS 36 4 17 30 reproj = map1.reproject_to(map2) reproj: CTYPE: RA - POL - FREQ - DEC NAXIS 36 5 16 30 2) Tested with values for the repeat axes 3) Tested with values for the output box 4) Tested with a new CTYPE (GLON-TAN, GLAT-TAN) and new CRVAL .. note:: If you want to align an image with the direction of the north, then the value of *CROTAn* (e.g. CROTA2) should be set to zero. To ensure that the data will be rotated, use parameter *rotation* with a dummy value so that the header used for the re-projection is a 'classic' header: e.g.: >>> Rotfits = Basefits.reproject_to(rotation=0.0, crota2=0.0) Todo: If CTYPE's change, then also LONPOLE and LATPOLE should change .. warning:: Values for *CROTAn* in parameter *fitskeys* overwrite values previously set with keyword *rotation*. .. warning:: Changing values of *CROTAn* will not always result in a rotated image. If the world coordinate system was defined using CD or PC elements, then changing *CROTAn* will only add the keyword but it is never read because CD & PC transformations have precedence. """ #--------------------------------------------------------------------- # Create a new header based on the current one. # Get the relevant keywords for the longitude axis # in the input header. Copy them in the new header with # the right number representing longitude in the copied header. # Create a data structure with as many spatial maps as there are # in the current data structure. # Correct the CRPIX and NAXIS values to support the new limits # Create a new FITSimage object with this dummy data and new header # Loop over all spatial maps and reproject them. # Store the reprojected data in the new data array at the appropriate # location # Return the result. # What are the axis numbers of the spatial axes in order of their axis numbers? def flatten(seq): res = [] for item in seq: if (isinstance(item, (tuple, list))): res.extend(flatten(item)) else: res.append(item) return res plimLO = plimHI = None fromheader = True # We need to distinguish header and FITSimage objects if isinstance(reprojobj, FITSimage): #flushprint("reproject_to slicepos=%s"%(str(self.slicepos))) # It's a FITSimage object # Copy its attributes that are relevant in this context pxlim_dst = reprojobj.pxlim pylim_dst = reprojobj.pylim slicepos = self.slicepos plimLO = plimHI = slicepos repheader = reprojobj.hdr.copy() fromheader = False if insertspatial is None: insertspatial = True else: # It's a plain header or None if reprojobj is None: # Then destination is original header. Modify later. repheader = self.hdr.copy() if insertspatial is None: insertspatial = False else: # A Python dict or a PyFITS header repheader = reprojobj.copy() if insertspatial is None: insertspatial = True # For a rotation (only) we convert the header to a 'classic' header if reprojobj is None and not rotation is None: # Get rid of skew and ambiguous rotation angles # by converting cd/pc matrix to 'classic' header, so that CROTA can # be adjusted. repheader, skew, hdrchanged = self.header2classic() # Look for the right CROTA (associated with latitude) key = "CROTA%d"%self.proj.lataxnum if key in repheader: crotanew = repheader[key] + rotation # Rotation of latitude axis + user rot. repheader[key] = crotanew else: crotanew = rotation # No CROTA, then assume CROTA=0.0 if type(repheader) == 'dict': repheader[key] = crotanew else: repheader[key] = crotanew if len(fitskeys) > 0: # Note that a ROTATION= keyword changes the value of CROTAn # but we can overwrite this with a user supplied keyword CROTAn # In effect, this is a trigger that CROTA changes and a 'classic' # header is required. change_header(repheader, **fitskeys) p1 = wcs.Projection(self.hdr) #flushprint(str(self.hdr)) naxis = len(p1.naxis) axnum = [] axnum_out = [] for i in range(1, naxis+1): #flushprint("i, p1.lon/lataxnum=%d %d %d"%(i, p1.lonaxnum, p1.lataxnum)) if i in [p1.lonaxnum, p1.lataxnum]: axnum.append(i) else: axnum_out.append(i) #flushprint("reproject+to axnum_out =%s"%(str(axnum_out))) if len(axnum) != 2: raise Exception("No spatial maps to reproject in this data structure!") naxisout = len(axnum_out) len1 = p1.naxis[axnum[0]-1]; len2 = p1.naxis[axnum[1]-1] # Now we are sure that we have a spatial map and the # axis numbers of its axes, in the order of the data structure # i.e. if lon, lat are swapped in the header then the # first axis in the data is lat. # Create a projection object for # the current spatial maps p1_spat = p1.sub(axnum) # Get a Projection object for a spatial map defined in the # input header, the destination p2 = wcs.Projection(repheader) naxis_2 = len(p2.naxis) axnum2 = [] for i in range( 1, naxis_2+1): if i in [p2.lonaxnum, p2.lataxnum]: axnum2.append(i) if len(axnum2) != 2: raise Exception("The input header does not contain a spatial data structure!") p2_spat = p2.sub(axnum2) # Determine the size and shape for a new data array. The new # shape is the shape of the current FITSimage object with # the spatial axes length replaced by the values from the input header. lenXnew = p2.naxis[axnum2[0]-1]; lenYnew = p2.naxis[axnum2[1]-1] lonaxnum2 = p2.lonaxnum; lataxnum2 = p2.lataxnum shapenew = [0]*naxis # Uitbreiden met meer flexibiliteit if pxlim_dst is None: pxlim_dst = [1]*2 pxlim_dst[1] = lenXnew if pylim_dst is None: pylim_dst = [1]*2 pylim_dst[1] = lenYnew nx = pxlim_dst[1] - pxlim_dst[0] + 1 ny = pylim_dst[1] - pylim_dst[0] + 1 N = nx * ny # Next we process the ranges on the repeat axes (i.e. axes # outside the spatial map). See the documentation for what # is allowed to enter #flushprint("reproject+to naxisout=%d"%(naxisout)) if naxisout > 0: #flushprint("reproject+to voor de tijdpxlimHI/LO=%s %s"%(str(plimLO), str(plimHI))) if plimLO is None or plimHI is None: plimLO = [0]*(naxis-2) plimHI = [0]*(naxis-2) for i, axnr in enumerate(axnum_out): plimLO[i] = 1 plimHI[i] = p1.naxis[axnr-1] #flushprint("Reproject_to pxlimLO/HI=%d %d"%(plimLO[i],plimHI[i])) # Make sure user given limits are list or tuple if plimlo is not None: if not issequence(plimlo): plimlo = [plimlo] if len(plimlo) > naxisout: raise ValueError("To many values in plimlo, max=%d"%naxisout) for i, p in enumerate(plimlo): plimLO[i] = p #flushprint("reproject+to p lo=%d"%(d)) if plimhi is not None: if not issequence(plimhi): plimhi = [plimhi] if len(plimhi) > naxisout: raise ValueError("To many values in plimhi, max=%d"%naxisout) for i, p in enumerate(plimhi): plimHI[i] = p #flushprint("reproject+to p hi=%d"%(d)) #flushprint("reproject+to pxlimHI/LO=%s %s"%(str(plimLO), str(plimHI))) # Use the sizes of the original/current non spatial axes # to calculate size and shape of the new data array for axnr, lo , hi in zip(axnum_out, plimLO, plimHI): n = hi - lo + 1 shapenew[axnr-1] = n N *= n shapenew[axnum[0]-1] = nx shapenew[axnum[1]-1] = ny shapenew = shapenew[::-1] # Invert list with lengths so that it represents a shape newdata = numpy.zeros(N, dtype=self.dat.dtype) # Data is always float newdata.shape = shapenew # The following code inserts all keywords related to the spatial information # of the destination header (to which you want to reproject) into a new header # which is a copy of the current FITSimage object. The spatial axes in the # current header are replaced by the spatial axes in the input header # both in order of their axis number. if insertspatial: #flushprint("Type repheader=%s"%(type(repheader))) newheader = self.insertspatialfrom(repheader, axnum, axnum2) #lonaxnum2, lataxnum2) else: newheader = repheader # In the new header the length of spatial and the repeat axes could have been # changed. Adjust the relevant keywords in the new header if nx != lenXnew: ax = axnum2[0] newheader['NAXIS%d'%ax] = nx newheader['CRPIX%d'%ax] += -pxlim_dst[0] + 1 if ny != lenYnew: ax = axnum2[1] newheader['NAXIS%d'%ax] = ny newheader['CRPIX%d'%ax] += -pylim_dst[0] + 1 if naxisout > 0: for axnr, lo, hi in zip(axnum_out, plimLO, plimHI): n = hi - lo + 1 newheader['NAXIS%d'%axnr] = n newheader['CRPIX%d'%axnr] += -lo + 1 # Also add CD elements if they are missing in the current # structure but available in the input header. key1 = "CD%d_%d" % (p1.lonaxnum, p1.lonaxnum) key2 = "CD%d_%d" % (axnum_out[0], axnum_out[0]) if key1 in newheader and not (key2 in newheader): # Obviously there is a CD matrix in the input header # for the spatial axes, but these elements are not available # for the other axes in the current header for axnr in axnum_out: key2 = "CD%d_%d" % (axnr, axnr) # NO CD element then CDELT must exist newheader[key2] = newheader["CDELT%d"%axnr] # Process the dictionary for the interpolation options if interpol_dict is not None: if not ('order' in interpol_dict): interpol_dict['order'] = 1 if not ('cval' in interpol_dict): interpol_dict['cval'] = numpy.NaN else: interpol_dict = {} interpol_dict['order'] = 1 interpol_dict['cval'] = numpy.NaN # Create the coordinate map needed for the interpolation. # Use the limits given in pxlim_dst, pylim_dst to set shape and offset. # Note that pxlim_dst is 1 based and the offsets are 0 based. dst_offset = (int(pylim_dst[0]-1), int(pxlim_dst[0]-1)) # Note the order! # In a previous version we had an offset in the source: # src_offset = (self.pylim[0]-1, self.pxlim[0]-1) # but this is not necessary if we keep 'boxdat' as big as # the entire spatial map. Besides that, it is not sure that # pxlim[0] and pylim[0] are set to the limits of the spatial axes. # The new solution therefore is better. src_offset = (0,0) coords = numpy.around(wcs.coordmap(p1_spat, p2_spat, dst_shape=(ny,nx), dst_offset=dst_offset, src_offset=src_offset)*512.0)/512.0 # Next we iterate over all possible slices. if naxisout == 0: # We have a two dimensional data structure and we # need to reproject only this one. boxdat = self.dat newdata = map_coordinates(boxdat, coords, **interpol_dict) else: perms = [] for lo, hi in zip(plimLO, plimHI): #for axnr in axnum_out: #pixellist = range(1, p1.naxis[axnr-1]+1) pixellist = list(range(lo, hi+1)) perms.append(pixellist) # Get all permutations. Last axis is slowest axis z = perms[0] for i in range(1, len(perms)): z = [[x,y] for y in perms[i] for x in z] # Extend the list with permutations Z = [] for l in z : Z.append(flatten(l)) # Flatten the lists in the list z = Z for tup in z: sl = [] # Initialize slice list for current data slnew = [] # Initialize slice list for new, reprojected data nout = len(axnum_out) - 1 for ax in range(naxis,0,-1): # e.g. ax = 3,2,1 if ax in [p1.lonaxnum, p1.lataxnum]: sl.append(slice(None)) slnew.append(slice(None)) else: if issequence(tup): g = tup[nout] else: g = tup sl.append(slice(g-1, g)) g2 = g - plimLO[nout] + 1 slnew.append(slice(g2-1, g2)) nout -= 1 #print("slice=", sl) #print(self.dat.min(), self.dat.max()) boxdat = self.dat[tuple(sl)].squeeze() boxdatnew = newdata[tuple(slnew)].squeeze() # Find (interpolate) the data in the source map at the positions # given by the destination map and 'insert' it in the # data structure as a copy. reprojecteddata = map_coordinates(boxdat, coords, **interpol_dict) boxdatnew[:] = reprojecteddata fi = FITSimage(externalheader=newheader, externaldata=newdata) if not fromheader: # The input header was a FITSimage object. Then only one spatial map is # reprojected. We should bring the new FITSimage object in the same # state that corresponds to axis numbers and slice position of # the input FITSimage object. In the calling environment the # 'boxdat' attributes are comparable and compatible then. # Note that in the output there is only one slice and its # pixel position is alway 1 for each repeat axis. # In the header the crpix values are adjusted so that these # pixel positions 1 on the repeat axes still correspond to # the right world coordinates slicepos = [1 for x in slicepos] fi.set_imageaxes(reprojobj.axperm[0], reprojobj.axperm[1], slicepos) return fi
def insertspatialfrom(self, header, axnum1, axnum2): #--------------------------------------------------------------------- """ Utility function, used in the context of method *reproject_to()*, which returns a new header based on the current header, but where its spatial information is replaced by the spatial information of the input header *header*. The axis numbers of the spatial axes in *header* must be entered in *lon2* and *lat2*. :param header: The header from which the spatial axis information must be copied into the current structure. :type header: PyFITS header object or Python dictionary :param axnum1: The axis numbers of the spatial axes in the input header :type axnum2: Tuple of two integers :param axnum2: The axis numbers of the spatial axes in the destination header :type axnum2: Tuple of two integers :Notes: For reprojections we use only the primary header. The alternate header is NOT removed from the header. If the input header has a CD matrix for its spatial axes, and the current data structure has not a CD matrix, then we add missing elements in the current header. If the CDELT's are missing for the construction of CD elements, the value 1 is assumed for diagonal elements. """ #--------------------------------------------------------------------- #newheader = self.hdr.copy() comment = True; history = True #flushprint("Type self.hdr in insertspatial=%s"%(type(self.hdr))) newheader = fitsheader2dict(self.hdr, comment, history) #flushprint("Type header in insertspatial=%s"%(type(header))) repheader = fitsheader2dict(header, comment, history) lon1 = axnum1[0] # Could also be a lat. from a lat-lon map lat1 = axnum1[1] lon2 = axnum2[0] # Could also be a lat. from a lat-lon map lat2 = axnum2[1] naxis = len(self.proj.naxis) axnum_out = [] for i in range( 1, naxis+1): if i not in [self.proj.lonaxnum, self.proj.lataxnum]: axnum_out.append(i) wcskeys = ['NAXIS', 'CRPIX', 'CRVAL', 'CTYPE', 'CUNIT', 'CDELT', 'CROTA'] # Clean up first in new header (which was a copy) for k in wcskeys: for ax in (lon1, lat1): key1 = '%s%d'%(k,ax) if key1 in newheader: del newheader[key1] for k in wcskeys: for ax in ((lon1,lon2), (lat1,lat2)): key1 = '%s%d'%(k,ax[0]) key2 = '%s%d'%(k,ax[1]) if key2 in repheader: newheader[key1] = repheader[key2] # Process lonpole and latpole keywords wcskeys = ['LONPOLE', 'LATPOLE', 'EQUINOX', 'EPOCH', 'RADESYS', 'MJD-OBS', 'DATE-OBS'] for key in wcskeys: if key in repheader: newheader[key] = repheader[key] else: if key in newheader: del newheader[key] # Process PV and other i_j elements in the input header # Clean up the new header first wcskeys = ['PC', 'CD', 'PV', 'PS'] for key in list(newheader.keys()): for wk in wcskeys: if key.startswith(wk) and key.find('_') != -1: try: i,j = key[2:].split('_') i = int(i); j = int(j) # Cope with old style PC001_002 etc. for ax in (lon2,lat2): if i == ax: del newheader[key] except: pass # Insert ['PC', 'CD', 'PV', 'PS'] if they are in the 'insert header' cdmatrix = False pcmatrix = False for key in list(repheader.keys()): for wk in wcskeys: if key.startswith(wk) and key.find('_'): try: i,j = key[2:].split('_') i = int(i); j = int(j) ii = jj = None if i == lon2: ii = lon1 if i == lat2: ii = lat1 if j == lon2: jj = lon1 if j == lat2: jj = lat1 if not ii is None and not jj is None: newkey = "%s%d_%d" % (wk,ii, jj) newheader[newkey] = repheader[key] if wk == 'CD': cdmatrix = True if wk == 'PC': pcmatrix = True except: pass # Fill in missing CD elements if base FITS had none. if cdmatrix: for i in axnum_out: newkey = "CD%d_%d"%(i,i) if not (newkey in newheader): # We changed to CD formalism, but the base header # does not have a CD element for the non spatial axes. # So we contruct one using the CDELT. cdeltkey = "CDELT%d"%i if cdeltkey in newheader: cdelt = newheader[cdeltkey] else: cdelt = 1.0 newheader[newkey] = cdelt # E.g. CD3_3 = CDELT3 elif pcmatrix: for i in axnum_out: newkey = "PC%d_%d"%(i,i) if not (newkey in newheader): # We changed to CD formalism, but the base header # does not have a CD element for the non spatial axes. # So we contruct one using the CDELT. newheader[newkey] = 1.0 # E.g. CD3_3 = 1 # Clean up ALL old style PC elements in current header for i in [1, naxis]: for j in [1, naxis]: key = "PC%03d%03d" % (i, j) if key in newheader: del newheader[key] # If 'insert header' has pc elements then copy them. If a cd # matrix is available then do not copy these elements because # a mix of both is not allowed. # VOG: But PC elements were already copied, so next lines are not # necessary. """ if not cdmatrix: for i2, i1 in zip([lon2,lat2], [lon1,lat1]): for j2, j1 in zip([lon2,lat2], [lon1,lat1]): key = "PC%03d%03d" % (i2, j2) newkey = "PC%03d%03d" % (i1, j1) if repheader.has_key(key): newheader[newkey] = repheader[key] """ return newheader
[docs] def writetofits(self, filename=None, comment=True, history=True, bitpix=None, bzero=None, bscale=None, blank=None, overwrite=False, append=False, extname=''): #--------------------------------------------------------------------- """ This method copies current data and current header to a FITS file on disk. This is useful if either header or data comes from an external source. If no file name is entered then a file name will be composed using current date and time of writing. The name then start with 'FITS'. :param filename: Name of new file on disk. If omitted the default name is 'FITS' followed by a date and a time (in hours, minutes seconds). :type filename: String :param comment: If you do not want to copy comments, set parameter to False :type comment: Boolean :param history: If you do not want to copy history, set parameter to False :type history: Boolean :param bitpix: Write FITS data in another format (8, 16, 32, -32, -64). If no *bitpix* is entered then -32 is assumed. Parameters *bzero*, *bscale* and *blank* are ignored then. :type bitpix: Integer :param bzero: Offset in scaled data. If bitpix is not equal to -32 and the values for bscale and bzero are None, then the data is scaled between the minimum and maximum data values. For this scaling the method scale() from PyFITS is used with ``option='minmax'``. However PyFITS 1.3 generates an error due to a bug. :type bzero: Float :param bscale: Scale factor for scaled data. If bitpix is not equal to -32 and the values for bscale and bzero are None, then the data is scaled between the minimum and maximum data values. For this scaling the method scale() from PyFITS is used with ``option='minmax'``. However PyFITS 1.3 generates an error due to a bug. :type bscale: Float :param blank: Value that represents a blank. Usually only for scaled data. :type blank: Float/Integer :param overwrite: If a file on disk already exists then an exception is raised. With *overwrite=True* an existing file will be overwritten. We don't attempt to suppres PyFITS warnings because its warning mechanism depends on the Python version. :type overwrite: Boolean :param append: Append image data in new HDU to existing FITS file :type append: Boolean :param extname: Name of image extension if append=True. Default is empty string. :type extname: String :Raises: :exc:`ValueError` You will get an exception if the shape of your external data in parameter 'boxdat' is not equal to the current sliced data with limits. :Examples: Artificial header and data: :: # Example 1. From a Python dictionary header header = {'NAXIS' : 2, 'NAXIS1': 800, 'NAXIS2': 800, 'CTYPE1' : 'RA---TAN', 'CRVAL1' :0.0, 'CRPIX1' : 1, 'CUNIT1' : 'deg', 'CDELT1' : -0.05, 'CTYPE2' : 'DEC--TAN', 'CRVAL2' : 0.0, 'CRPIX2' : 1, 'CUNIT2' : 'deg', 'CDELT2' : 0.05, } x, y = numpy.mgrid[-sizex1:sizex2, -sizey1:sizey2] edata = numpy.exp(-(x**2/float(sizex1*10)+y**2/float(sizey1*10))) f = maputils.FITSimage(externalheader=header, externaldata=edata) f.writetofits() # Example 2. From an external header and dataset. # In this example we try to copy the data format from the input file. # PyFITS removes header items BZERO and BSCALE because it reads its # data in a NumPy array that is compatible with BITPIX=-32. # The original values for *bitpix*, *bzero*, *bscale* and *blank* # are retrieved from the object attributes with the same name. from kapteyn import maputils fitsobject = maputils.FITSimage(promptfie=maputils.prompt_fitsfile) header = fitsobject.hdr edata = fitsobject.dat f = maputils.FITSimage(externalheader=header, externaldata=edata) f.writetofits(history=True, comment=True, bitpix=fitsobject.bitpix, bzero=fitsobject.bzero, bscale=fitsobject.bscale, blank=fitsobject.blank, overwrite=True) # Example 3. Write a FITS file in the default format, BITPIX=-32 # and don't bother about FITS history and comment cards. f.writetofits(history=False, comment=False) """ #--------------------------------------------------------------------- # Suppress user warnings for PyFITS actions #warnings.resetwarnings() #warnings.filterwarnings('ignore', category=UserWarning, append=True) if filename is None: filename = getfilename('mu', 'fits') append = False # Cannot append if FITS file does not exists if append: try: f = pyfits.open(filename) f.close() except: append = False hdu = pyfits.PrimaryHDU(self.dat) pythondict = fitsheader2dict(self.hdr, comment, history) for key, val in pythondict.items(): if key=='HISTORY' or key=='COMMENT': if (history and key=='HISTORY'): for v in val: hdu.header.add_history(v) elif (comment and key=='COMMENT'): for v in val: hdu.header.add_comment(v) else: # The Python dictionary is extended with comments that # correspond to keywords in a header. The comments are an # attribute and identified by the key. #print("key=", key) #print("val=", val) #print("comment=", pythondict.comment[key]) #flushprint("----") # We added a try/except here because sometimes a first record # seems empty but has a lot of spaces and generates an exception in # the newer PyFITS versions try: if pyfitsoldversion: # Is deprecated in newer PyFITS versions (from v. 3.0) hdu.header.update(key, val, pythondict.comment[key]) else: hdu.header[key] = (val, pythondict.comment[key]) except: pass if bitpix is not None: # User wants to scale code = hdu.NumCode[bitpix] # Undocumented PyFITS function if bzero is None and bscale is None: # Option below does not work for (at least) int16 # because PyFITS (1.3) has a bug in its scaling method. hdu.scale(code, option='minmax') else: if bzero is None: bzero = 0.0 if bscale is None: bscale = 1.0 hdu.scale(code, bzero=bzero, bscale=bscale) if blank is not None: hdu.header['BLANK'] = self.blank else: # The output format is copied from the Numpy array # Default we use a format (to read the data) which # corresponds to BITPIX=-32. Then there should be no # keyword BLANK in the header, because these numbers # are identified by NaN's. if 'BLANK' in hdu.header: del hdu.header['BLANK'] if append: if not extname: extname = "KP"+ datetime.now().strftime("%Y%m%d%Hh%Mm%Ss") if pyfitsoldversion: hdu.header.update('EXTNAME', extname) else: hdu.header['EXTNAME'] = extname pyfits.append(filename, self.dat, header=hdu.header) else: hdulist = pyfits.HDUList([hdu]) # If there is a problem, try to fix it but suppress a warning hdulist.writeto(filename, overwrite=overwrite, output_verify='silentfix') hdulist.close()
# Turn warnings on #warnings.resetwarnings() #warnings.filterwarnings('always', category=UserWarning, append=True)
[docs] def Annotatedimage(self, frame=None, **kwargs): #--------------------------------------------------------------------- """ This method couples the data slice that represents an image to a Matplotlib Axes object (parameter *frame*). It returns an object from class :class:`Annotatedimage` which has only attributes relevant for Matplotlib. :param frame: Plot the current image in this Matplotlib Axes object. If omitted, a default frame will be set using Matplotlib's method *add_subplot()* :type frame: A Matplotlib Axes instance :param kwargs: These parameters are keyword arguments for the constructor of :class:`Annotatedimage`. All of them get a default value in this routine. The ones for which it can be useful to change are: * skyout: The sky definition for graticule and world coordinates * spectrans: The spectral translation for the spectral axis * aspect: The aspect ratio of the pixels * basename: A name for a file on disk e.g. to store a color lut * cmap: A color map * blankcolor: The color of bad pixels, * clipmin: Scale colors between image values clipmin and clipmax * clipmax: Scale colors between image values clipmin and clipmax * gridmode: Set modus of str2pos() to pixels or grids :type kwargs: Python keyword arguments :Attributes: See documentation at :class:`Annotatedimage` :Returns: An object from class :class:`Annotatedimage` :Examples: >>> f = maputils.FITSimage("ngc6946.fits") >>> f.set_imageaxes(1, 3, slicepos=51) >>> annim = f.Annotatedimage() or:: from kapteyn import maputils from matplotlib import pyplot as plt f = maputils.FITSimage("m101.fits") fig = plt.figure() frame = fig.add_subplot(1,1,1) annim = f.Annotatedimage(frame) annim.Image() annim.Graticule() annim.plot() plt.show() """ #--------------------------------------------------------------------- ar = self.get_pixelaspectratio() if not ('basename' in kwargs): basename = self.filename.rsplit('.')[0] kwargs['basename'] = basename # Append because it must be available else: basename = kwargs['basename'] # Note the use of self.boxdat instead of self.dat !! if frame is None: fig = figure() frame = fig.add_subplot(1,1,1, frameon=False) # Give the axis types in the order of the axes in the Annotatedimage. wcstypes = [self.wcstypes[self.axperm[0]-1], self.wcstypes[self.axperm[1]-1]] mplimage = Annotatedimage(frame, self.hdr, self.pxlim, self.pylim, self.boxdat, self.convproj, self.axperm, wcstypes, skyout=self.skyout, spectrans=self.spectrans, alter=self.alter, mixpix=self.mixpix, aspect=ar, slicepos=self.slicepos, sliceaxnames=self.sliceaxnames, sourcename=self.filename, **kwargs) # The kwargs are for cmap, blankcolor, clipmin, clipmax for which # a FITSimage object does not need to set defaults because they # are used in another context (e.g. image display). # gridmode should also be set by a user return mplimage
[docs]class MovieContainer(object): #----------------------------------------------------------------- """ This class is a container for objects from class :class:`maputils.Annotatedimage`. For this container there are methods to alter the visibility of the stored objects to get the effect of a movie loop. The objects are appended to a list with method :meth:`MovieContainer.append`. With method :meth:`MovieContainer.movie_events` the movie is started and keys 'P', '<', '>', '+' and '-' are available to control the movie. * 'P' : Pause/resume movie loop * '<' : Step 1 image back in the sequence of images. Key ',' and arrow down has the same effect. * '>' : Step 1 image forward in the sequence of images. Key '.' and arrow up has the same effect. * '+' : Increase the speed of the loop. The speed is limited by the size of the image and the hardware in use. * '-' : Decrease the speed of the movie loop One can also control the movie with method :meth:`MovieContainer.controlpanel` Usually one creates a movie container with class class:`Cubes` :param helptext: Allow or disallow methods to set an informative text about the keys in use. :type helptext: Boolean :param imageinfo: Allow or disallow methods to set an informative text about which image is displayed and, if available, it prints information about the pixel coordinate(s) of the slice if the image was extracted from a data cube. :type imageinfo: Boolean :param slicemessages: A list with messages, associated with the frames in your movie. Usually one uses this option to gain some efficiency because there is no need to construct a message each time a frame is changed. :type slicemessages: List with strings. :Attributes: .. attribute:: annimagelist List with objects from class :class:`maputils.Annotatedimage`. .. attribute:: indx Index in list with objects of object which represents the current image. .. attribute:: framespersec A value in seconds, representing the interval of refreshing an image in the movie loop. .. attribute:: info An object of class infoObject which has an attribute for the index of the current frame ('indx') and an attribute with an informative message about the current displayed slice. :Examples: Use of this class as a container for images in a movie loop: .. literalinclude:: EXAMPLES/mu_movie.py Skip informative text on the display: >>> movieimages = maputils.MovieContainer(helptext=False, imageinfo=False) :Methods: .. automethod:: append .. automethod:: movie_events .. automethod:: controlpanel .. automethod:: setmessages .. automethod:: setimage .. automethod:: imageloop .. automethod:: toggle_images """ #-------------------------------------------------------------------- class infoObject(object): #--------------------------------------------------------------------- """ Object which is a parameter of a registered callback function It returns the index of the current frame in attribute 'indx' and a message with physical coordinates of the slice position in attribute 'mes' """ #--------------------------------------------------------------------- def __init__(self): self.indx = 0 self.mes = "" self.cubenr = None self.slicemessage = '' # Class variables record_fps_in = 10 record_fps_out = 25 record_frameformat = 'png' record_bitrate = 1800 # VOG: 28-06-2023, not used anymore. record_outfile = "visionsmovie.mp4" record_fnameformatstr = '%s%%07d.%s' record_basename = "_tmp_visions" record_removetmpfiles = True # The calling environment could request for the defaults of the parameters # for ffmpeg before any object is created recdefs = dict(outfile=record_outfile, bitrate=record_bitrate, tmpprefix=record_basename, removetmp=record_removetmpfiles, fps=record_fps_in, fpso=record_fps_out) def __init__(self, fig, helptext=True, imageinfo=True, slicemessages=[], toolbarinfo=False, sliceinfoobj=None, callbackslist={}): self.annimagelist = [] # The list with Annotatedimage objects self.indx = 0 # Sets the current image in the list self.fig = fig # Current Matplotlib figure instance self.textid = None # Plot and erase text on canvas using this id. self.helptext = helptext self.imageinfo = imageinfo # Flag. When on, display image number self.slicemessages = slicemessages # Pre defined messages self.numimages = 0 self.callbackinfo = self.infoObject() # Object that returns info to calling environment self.pause = True self.forward = True self.toolbarinfo = toolbarinfo self.sliceinfoobj = None self.currentcubenr = None self.movieframelist = [] self.movielistindx = 0 # First element in movieframelist self.oldim = None # Administration for cleaning up images self.compareim = None # Background image for transparency actions self.callbackslist = callbackslist self.framespersec = 20 self.flushcounter = 0 # Attributes for creating a movie on disk self.recording = False self.record_counter = 0 self.set_recorddefaults() if self.helptext: delta = 0.01 self.helptextbase = "Use keys 'p' to Pause/Resume. '+','-' to increase/decrease movie speed. '<', '>' to step in Pause mode." speedtext = " Speed=%d im/s"% (self.framespersec) self.helptext_id = self.fig.text(0.5, delta, self.helptextbase+speedtext+'\n'+colornavigation_info(), color='g', fontsize=8, ha='center') else: self.helptextbase = '' speedtext = '' self.helptext_id = None self.canvas = self.fig.canvas # Setup of the movieloop timer #self.pause = False self.framespersec = 20 self.cidkey = None self.cidscroll = None self.movieloop = TimeCallback(self.imageloop, 1.0/self.framespersec) # Do not start automatically, but wait for start signal #self.pause = True self.movieloop.deschedule() self.movieloop.once = False def addcallbacks(self, cbs): #----------------------------------------------------------------- """ Helper function for to add one or more callbacks to the dictionary with registered callbacks. Parameter cbs should also be a dictionary. """ #----------------------------------------------------------------- self.callbackslist.update(cbs) def callback(self, cbid, *arg): #----------------------------------------------------------------- """ Helper function for registered callbacks """ #----------------------------------------------------------------- if cbid in self.callbackslist: self.callbackslist[cbid](*arg)
[docs] def append(self, annimage, visible=True, cubenr=0, slicemessage=None): #--------------------------------------------------------------------- """ Append object from class :class:`Annotatedimage`. First there is a check for the class of the incoming object. If it is the first object that is appended then from this object the Matplotlib figure instance is copied. :param annimage: Add an image to the list. :type annimage: An object from class :class:`Annotatedimage`. :param visible: Set the data in this object to visible or invisible. Usually one sets the first image in a movie to visible and the others to invisible. :Raises: 'Container object not of class maputils.Annotatedimage!' An object was not recognized as a valid object to append. """ #--------------------------------------------------------------------- if not isinstance(annimage, Annotatedimage): raise TypeError("Container object not of class maputils.Annotatedimage!") annimage.origdata = None annimage.image.im.set_visible(visible) self.annimagelist.append(annimage) self.numimages = len(self.annimagelist) #flushprint("Loaded %d images in moviecontainer"%self.numimages) if slicemessage is None: s = "im #%d=slice:%s"%(self.numimages-1, annimage.slicepos) annimage.slicemessage = s else: annimage.slicemessage = slicemessage annimage.cubenr = cubenr
[docs] def setmessages(self, slicemessages): #--------------------------------------------------------------------- """ Set a list with messages that will be used to display information when a frame is changed. This message is either printed on the image or returned, using a callback function, to the calling environment. List may be smaller than the number of images in the movie. A message should correspond to the movie index number. """ #--------------------------------------------------------------------- self.slicemessages = slicemessages
def set_movieframes(self, framelist): #--------------------------------------------------------------------- """ For INTERACTIVE use. Set the movie frame numbers that you want to display in a movie loop. If the list is None or empty, all images are part of the movieloop. The order of the numbers is arbitrary so many settings are possible (e.g. reverse order (100:0:-1) or skip images (0:100:2). Note: We don't perfrom any checking here. The only check is in the toggle_image method where we check whether the frame number in this list is a valid index. The entered list is associated to the movie container not to specific cubes. """ #--------------------------------------------------------------------- self.movieframelist = framelist
[docs] def movie_events(self, allow=True): #--------------------------------------------------------------------- """ Connect keys for movie control and start the movie. Note that we defined the callbacks to trigger on the entire canvas. We don't need to connect it to a frame (AxesCallback). We have to disconnect the callbacks explicitly. """ #--------------------------------------------------------------------- if len(self.annimagelist) == 0: return # For the interaction with the movie, we want key and mouse # to work on the entire canvas, not just a frame. if allow: # Then disconnect first if the callbacks still exists if self.cidkey: self.fig.canvas.mpl_disconnect(self.cidkey) if self.cidscroll: self.fig.canvas.mpl_disconnect(self.cidscroll) # and create new ones self.cidkey = self.fig.canvas.mpl_connect('key_press_event', self.controlpanel) self.cidscroll = self.fig.canvas.mpl_connect('scroll_event', self.controlpanel) else: if self.cidkey: self.fig.canvas.mpl_disconnect(self.cidkey) if self.cidscroll: self.fig.canvas.mpl_disconnect(self.cidscroll) self.cidkey = self.cidscroll = None
[docs] def controlpanel(self, event, externalkey=''): #--------------------------------------------------------------------- """ Process the key and scroll events for the movie container. An external process (i.e. not the Matplotlib canvas) can access the control panel. It uses parameter 'externalkey'. In the calling environment one needs to set 'event' to None. """ #--------------------------------------------------------------------- self.movieloop.once = False # Extra attribute (not in constructor (yet) if event is None and externalkey == '': return if event is not None: try: key = event.key.upper() # Intercept keys like 'escape' if key.startswith('SH'): # Do not interfere with SHIFT+arrow to move crosshair return # Catch the arrow key. The event is generated for the entire canvas # and is not bound to a Matplotlib Axes object. To prevent interfering # with the crosshair events with the SHIFT arrow keys, we check first # whether the event starts with SH (note that only 'S' is allowed) # as in SHIFT+DOWN if key=='UP': # Translate keyboard arrow keys 'up' and 'down' key = '.' elif key=='DOWN': key = ',' except AttributeError: # It could be a scroll event if event.button=='up': # 'up' is previous image. Seems more suitable using scroll wheel key = '.' elif event.button=='down': key = ',' except: return # Do nothing for unknown events or keys else: key = externalkey.upper() # Pause button is toggle if key == 'P': if self.pause: self.movieloop.schedule() self.pause = False else: self.movieloop.deschedule() self.fig.canvas.draw() # Restore also graticules etc. self.pause = True self.callback('moviepause', self.pause, self.forward) elif key == 'START': if not self.pause: self.movieloop.deschedule() if not self.forward: self.forward = True self.movieloop.schedule() else: self.pause = True else: self.forward = True self.movieloop.schedule() self.pause = False elif key == 'STARTBACK': if not self.pause: self.movieloop.deschedule() if self.forward: # Change direction without stopping self.forward = False self.movieloop.schedule() else: self.pause = True else: self.forward = False self.movieloop.schedule() self.pause = False elif key == 'STOP' and not self.pause: self.movieloop.deschedule() self.fig.canvas.draw() # Restore also graticules etc. self.pause = True elif key == 'ONCE': self.forward = True self.movieloop.once = True self.movieloop.counter = 0 if self.movieframelist: self.movieloop.maxcount = len(self.movieframelist) else: self.movieloop.maxcount = len(self.annimagelist) self.movieloop.schedule() self.pause = False # Increase speed of movie elif key in ['+', '=']: self.framespersec = min(self.framespersec+1, 200) # Just to be save self.movieloop.set_interval(1.0/self.framespersec) if self.helptext: speedtxt = " Speed=%d im/s"% (self.framespersec) self.helptext_id.set_text(self.helptextbase+speedtxt) self.callback('speedchanged', self.framespersec) # Decrease in speed of movie elif key in ['-', '_']: self.framespersec = max(self.framespersec-1, 1) self.movieloop.set_interval(1.0/self.framespersec) if self.helptext: speedtxt = " Speed=%d im/s"% (self.framespersec) self.helptext_id.set_text(self.helptextbase+speedtxt) self.callback('speedchanged', self.framespersec) elif key in [',','<', 'PREV']: if not self.pause: self.movieloop.deschedule() self.pause = True self.toggle_images(next=False) elif key in ['.','>', 'NEXT']: if not self.pause: self.movieloop.deschedule() self.pause = True self.toggle_images(next=True)
#self.fig.canvas.flush_events() def set_recorddefaults(self, outfile=None, fps=None, fpso=None, bitrate=None, removetmpfiles=None): #--------------------------------------------------------------------------- """ This method sets or overwrites the default parameters used to make a movie on disk of the recorded images (saved with savefig()) Notes: The values cannot be expressions """ #--------------------------------------------------------------------------- if fps is None: self.record_fps_in = MovieContainer.record_fps_in else: mes = "" try: fps = int(fps) if fps < 0: mes = "Frames per second must be positive" raise else: self.record_fps = fps except: if not mes: mes = "Invalid expression for frames per second for the input frames" raise Exception(mes) if fpso is None: self.record_fps_out = MovieContainer.record_fps_out else: mes = "" try: fpso = int(fpso) if fpso < 0: mes = "Frames per second must be positive" raise else: self.record_fps_out = fpso except: if not mes: mes = "Invalid expression for frames per second for the output frames" raise Exception(mes) self.record_frameformat = MovieContainer.record_frameformat # bitrate is not used after 28-06-2024 update if bitrate is None: self.record_bitrate = MovieContainer.record_bitrate else: mes = "" try: bitrate = int(bitrate) if bitrate < 0: mes = "Bitrate must be a positive integer" raise else: self.record_bitrate = bitrate except: if not mes: mes = "Invalid expression for bitrate in kbit/s" raise Exception(mes) if outfile is None: self.record_outfile = MovieContainer.record_outfile else: try: self.record_outfile = str(outfile) f = open(self.record_outfile, "w+") f.close() except: raise Exception("Cannot write this movie on disk (invalid directory?)") self.record_fnameformatstr = MovieContainer.record_fnameformatstr self.record_basename = MovieContainer.record_basename if removetmpfiles is None: self.record_removetmpfiles = MovieContainer.record_removetmpfiles else: try: self.record_removetmpfiles = bool(removetmpfiles) except: raise Exception("Invalid value for remove tmp file option") @staticmethod def get_recorddefaults(): #--------------------------------------------------------------------------- """ Return a dictionary with defaults for recording a movue on disk. The method is a static method so that the calling environment is able to obtain these defaults before any MovieContainer object is created i.e. in a GUI where we present the defaults before any image is recorded. """ #--------------------------------------------------------------------------- return MovieContainer.recdefs def set_recording(self, recording): #--------------------------------------------------------------------------- """ Method to set a flag to start or stop recording what is on screen using savefig(). Currently figures are recorded only if images are toggled. """ #--------------------------------------------------------------------------- if recording: self.recording = True else: self.recording = False def create_recording(self): #--------------------------------------------------------------------- """ Method creates a movie from screendumps on disk. The utility needed to create a movie is 'ffmpeg'. ffmpeg: -r fps-> Set frame rate (Hz value, fraction or abbreviation), (default = 25) -y -> Overwrite existing filename """ #--------------------------------------------------------------------- from glob import glob if self.record_counter == 0: raise Exception("No images available to create a movie!<br> Start recording first.") cmd = "ffmpeg" basename = self.record_fnameformatstr % (self.record_basename, self.record_frameformat) #args = [cmd, '-vframes', str(self.record_counter), '-i', basename, # '-framerate', str(self.record_fps)] args = [cmd, '-framerate', str(self.record_fps_in), '-i', basename, '-r', str(self.record_fps_out)] # args = [cmd, '-framerate', str(5), '-i', basename, '-r', str(self.record_fps)] # Not used anymore after 28-06-2024 #if self.record_bitrate > 0: # args.extend(['-r', '%d' % self.record_bitrate]) args.extend(['-y', self.record_outfile]) ss = ' '.join(args) anyout(ss) try: #proc = Popen(args, shell=False, stdout=PIPE, stderr=PIPE) proc = check_call(args, shell=False, stdout=PIPE, stderr=PIPE) #proc.wait() except OSError: raise Exception("Cannot find ffmpeg to create movie") except CalledProcessError as message: raise Exception("Something wrong in parameters for command ffmpeg") except: raise Exception("Unknown error creating a movie with ffmpeg") # Remove all tmp files used to build the movie. One needs the glob module # to parse the asterisk. An alternative could be to maintain a list with # the file names of the screen dumps. if self.record_removetmpfiles: files = glob("%s*.%s"%(self.record_basename, self.record_frameformat)) for f in files: remove(f) pics = self.record_counter self.record_counter = 0 # Reset to be able to create a new movie return pics, " ".join(args) # The calling environment could need the number of images in a movie
[docs] def imageloop(self, cb): #--------------------------------------------------------------------- """ Helper method to get movie loop :param cb: Mouse event object with pixel position information. :type cb: Callback object based on matplotlib.backend_bases.MouseEvent instance """ #--------------------------------------------------------------------- self.toggle_images(self.forward)
def setspeed(self, framespersec): #--------------------------------------------------------------------- """ Change movie speed by changing timer interval. Note that speed can be < 1.0 """ #--------------------------------------------------------------------- self.framespersec = max(framespersec, 0) #max(framespersec-1, 1) self.framespersec = min(self.framespersec, 200) self.movieloop.set_interval(1.0/self.framespersec) #flushprint("interval in sec=%f"%(1.0/self.framespersec)) if self.helptext: speedtxt = " Speed=%d im/s"% (self.framespersec) self.helptext_id.set_text(self.helptextbase+speedtxt)
[docs] def setimage(self, i, force_redraw=False): #--------------------------------------------------------------------- """ Set a movie frame by index number. If the index is invalid, then do nothing. :param i: Index of movie frame which is any number between 0 and the number of loaded images. """ #--------------------------------------------------------------------- # If the index is invalid, then do nothing (also no warning) if i is None: return #flushprint("MAPUTILS setimage imnr, numimages=%d %d"%(i,self.numimages)) if 0 <= i < self.numimages: self.toggle_images(indx=i, force_redraw=force_redraw)
[docs] def toggle_images(self, next=True, indx=None, force_redraw=False): #--------------------------------------------------------------------- """ Toggle the visible state of images either by a timed callback function or by keys. This toggle works if one stacks multiple images in one frame with method :meth:`MovieContainer.append`. Only one image gets status visible=True. The others are set to visible=False. This toggle changes this visibility for images and the effect, is a movie. :param next: Step forward through list if next=True. Else step backwards. :type next: Boolean :param indx: Set image with this index :type indx: Integer :param force_redraw: Boolean flag to force everything to be repainted, which would otherwise only occur if there is a change of cubes """ #--------------------------------------------------------------------- if self.movieloop.once: # Was part of a single loop of which the first image was marked. if self.movieloop.counter == self.movieloop.maxcount: self.oldim.once = False self.movieloop.once = False self.movieloop.deschedule() self.fig.canvas.draw() # Restore also graticules etc. self.pause = True return self.movieloop.counter += 1 cubechanged = False xd = None # A position in the previous image yd = None if indx is not None: # User/programmer required an explicit image, not a next or # previous one newmovieframeindx = indx self.indx = newmovieframeindx else: # If there is not yet a list, we cannot change an image if not self.annimagelist: return # We get a request to show a next or a previous image fromlist = self.movieframelist if fromlist: # This was not a requested image (with given indx) but a request # to show the next image, the next in a user defined # sequence ('movieframelist'). Note that if we have a user defined # list with movie frames, then we have another index. This index # 'movielistindx' is related to the list with movie frames. movielistindx = self.movielistindx # Retrieve if next: if movielistindx + 1 >= len(self.movieframelist): movielistindx = 0 else: movielistindx += 1 else: if movielistindx - 1 < 0: movielistindx = len(self.movieframelist) - 1 else: movielistindx -= 1 newmovieframeindx = self.movieframelist[movielistindx] self.movielistindx = movielistindx # Store for next # What if the index from the movie frame list is not valid? # Then there is nothing to do because there is no associated image. if newmovieframeindx >= len(self.annimagelist) or newmovieframeindx < 0: return else: currindx = self.indx # Retrieve if next: if currindx + 1 >= len(self.annimagelist): currindx = 0 else: currindx += 1 else: if currindx - 1 < 0: currindx = len(self.annimagelist) - 1 else: currindx -= 1 newmovieframeindx = currindx self.indx = newmovieframeindx # Store for next oldim = self.oldim # TODO: Resetten van images na transparantie acties. Moet nog verder uitgewerkt # worden. # Make sure that the image used to compare to the current is reset to invisible resets = 0 if self.compareim: self.compareim.image.im.set_alpha(1.0) self.compareim.image.im.set_visible(False) self.compareim = None if oldim: resets += 1 if oldim is not None: # If the old image was splitted (or transparent), it does not contain the original data if oldim.origdata is not None: oldim.data[:] = oldim.origdata[:] oldim.origdata = None oldim.image.im.set_data(oldim.data) # Necessary! # Note that we need to restore the blank color and transparency # both in one time. We are sure that the attribute old_blankcolor # exists because we have splitted the image (it has a non empty # 'origdata' attribute). oldim.set_blankcolor(oldim.old_blankcolor[0], oldim.old_blankcolor[1]) splitimage = self.annimagelist[oldim.splitmovieframe] splitimage.image.im.set_visible(False) resets += 1 if oldim.blurred: oldim.set_blur(False) resets += 1 if oldim.histogram: oldim.set_histogrameq(False) resets += 1 if resets: # If the calling environment needs to know if we did reset # an (e.g. blurred) image to its original data. # The calling environment (e.g. a GUI) can reset its # sliders etc. self.callback('imagereset') # Make the old image invisible and disconnect existing callbacks #oldim.image.im.set_alpha(1.0) oldim.image.im.set_visible(False) oldim.disconnectCallbacks() #flushprint("Toggle images: Made oldim (%d) invisible and no callbacks"%(id(oldim))) if self.toolbarinfo: # Deschedule the old toolbarinfo method, but first, store # the mouse position in display coordinates, so that we can # re-use them for another frame. Note that the movie can consist of # frames from different cubes and each cube is associated with # a separate frame (i.e. mpl axes object) x = oldim.X_lastvisited y = oldim.Y_lastvisited if not (None in [x,y]): xd, yd = oldim.frame.transData.transform((x,y)) #flushprint("Toolbarinfo voor oldim %s DISCONnected"%(str(id(oldim)))) """ if None in [x,y]: flushprint("Toolbarinfo ziet None in x,y") else: flushprint("Toolbarinfo ziet valide x,y") """ #flushprint("Setimage movie index=%d"%(newmovieframeindx)) newim = self.annimagelist[newmovieframeindx] if newim.backgroundImageNr: backgroundimage = self.annimagelist[newim.backgroundImageNr] newim.image.im.set_visible(False) backgroundimage.image.im.set_visible(True) else: newim.image.im.set_visible(True) info = newim.infoobj if info: info.set_text(newim.slicemessage) # Handle lines and text if one changes cubes. oldcubenr = -1 if (newim.cubenr != self.currentcubenr) or force_redraw: oldcubenr = self.currentcubenr cubechanged = True #flushprint("KUBUS WISSELING?? Current cube nr, newcubenr=%s %d"%(str(self.currentcubenr),newim.cubenr)) self.currentcubenr = newim.cubenr # currentcubenr is None for the first image if oldim is not None: for li in oldim.linepieces: li.set_visible(False) for tl in oldim.textlabels: tl.set_visible(False) if oldim.infoobj: oldim.infoobj.set_visible(False) if oldim.hascolbar and oldim.cbframe: oldim.cbframe.set_visible(False) if oldim.splitcb: oldim.splitcb.deschedule() if oldim.contourSet: for col in oldim.contourSet.collections: col.set_visible(False) for li in newim.linepieces: li.set_visible(True) for tl in newim.textlabels: tl.set_visible(True) #flushprint("Ik zet in toggle_images textlabel %d %s op True"%(id(tl), str(tl))) if newim.contours_on and newim.contourSet: for col in newim.contourSet.collections: col.set_visible(True) if newim.infoobj: newim.infoobj.set_visible(True) if newim.splitcb: newim.splitcb.schedule() #flushprint("force_redraw=%s"%(str(force_redraw))) if newim.cbframe: if newim.hascolbar: newim.cbframe.set_visible(True) else: newim.cbframe.set_visible(False) self.canvas.draw() # Draw all doflush = True #doflush = False # TODO: Flushen is tot nu toe alleen storend geweest self.callback('cubechanged', newim) else: if oldim and oldim.contourSet: # Clean the contours in the previous data by making them invisible for col in oldim.contourSet.collections: col.set_visible(False) doflush = False #flushprint("Current cube nr, newcubenr=%d %d"%(self.currentcubenr,newim.cubenr)) #flushprint("They are equal so blit now") if newim.backgroundImageNr: newim.frame.draw_artist(backgroundimage.image.im) else: newim.frame.draw_artist(newim.image.im) if info: newim.frame.draw_artist(info) # Restore the graticule grid! Note that 'zorder' could not help us # because blitting the image always puts the image on top. for li in newim.linepieces: #flushprint("newim li=%d"%(id(li))) newim.grat.frame.draw_artist(li) for tl in newim.textlabels: #flushprint("newim tl=%d %s"%(id(tl), str(tl.get_text()))) newim.frame.draw_artist(tl) if newim.contours_on: if not newim.contourSet or newim.contours_changed: if newim.contours_cmap: cs = newim.Contours(newim.clevels, cmap=cm.get_cmap(newim.contours_cmap)) else: # Not a color map. Is it a plain colour then? if not newim.contours_colour: newim.contours_colour = 'r' # Then set a default (red) cs = newim.Contours(newim.clevels, colors=newim.contours_colour) cs.plot(newim.frame) newim.contourSet = cs.CS newim.contours_changed = False # Flag it for toggle_images() for col in newim.contourSet.collections: col.set_visible(True) newim.frame.draw_artist(col) self.canvas.blit(newim.frame.bbox) # We need to keep track of the last image viewed in a cube because # if we jump (in the calling environment) to another cube and back, # there should be a way to restore the last displayed image for this # cube. #C = self.cubelist[newim.cubenr] #C.lastimagenr = self.indx - C.movieframeoffset newX = newY = None newim.interact_imagecolors() if self.toolbarinfo: newim.interact_toolbarinfo() newim.interact_writepos(pixfmt=newim.coord_pixfmt, dmsprec=newim.coord_dmsprec, wcsfmt=newim.coord_wcsfmt, zfmt=newim.coord_zfmt, gipsy=True, # No effect if gipsymod == False grids=newim.coord_grids, world=newim.coord_world, worlduf=newim.coord_worlduf, imval=newim.coord_imval, g_typecli=newim.coord_tocli, g_tolog=newim.coord_tolog, g_appendcr=newim.coord_appendcr) #flushprint("Toolbarinfo voor newim %s CONnected"%(str(id(newim)))) cb = newim.toolbarkey if not (None in [xd, yd]): cb.xdata, cb.ydata = newim.frame.transData.inverted().transform((xd,yd)) # Trigger the toolbarinfo method to get the right image data value (z) # for this new image. Note that events from the panels come always from # outside the image frame. In the eventpanel methods, the values # for the last position of the current image are reset to None. Then # skip the update of the toolbar message. newim.mouse_toolbarinfo(cb) newX , newY = cb.xdata, cb.ydata self.oldim = newim self.callbackinfo.mes = newim.slicemessage self.callbackinfo.indx = newmovieframeindx self.callbackinfo.cubenr = newim.cubenr self.callbackinfo.slicemessage = newim.composed_slicemessage self.callback('movchanged', self.callbackinfo) # Attention must be paid to the right order of drawing things. # After a change of cubes, we need to force a redraw by flushing # the event buffer. Otherwise it often skips the update. # Only after this update we should update the crosshair # cursor if doflush: self.canvas.flush_events() #flushprint("------ Einde setimage. id new image=%d----------"%(id(newim))) self.callback('crosshair', self.indx, cubechanged, oldcubenr) if cubechanged: self.callback('zprofile', None, newX, newY, newim.cubenr) if self.recording: filenam = "%s%07d.%s"%(self.record_basename, self.record_counter, self.record_frameformat) #flushprint(filenam) self.record_counter += 1 self.fig.savefig(filenam)
#------------------------------------------------------------------------ """Background We developed some methods that enable a user to inspect slices of a data cube. One of these methods displays an image which is part of a movie. A position in such an image sets the data in two slices which are shown as two panels. For each panel, one axis is shared with the movie frame image. The other is the third axis in the cube. What we try to do is to copy the functionality of the display program GIDS (Groningen Image Display Server). In that application one enters a number of images. For example in a simple case: one has a RA-DEC-VELO cube. The interesting parts of the data are in slices RA-VELO and DEC-VELO. These slices need an extra position which is derived from the mouse position in a movieframe. In GIDS a series of movie frames create a cube. It doesn't matter if these images have their origin in different files. It also doesn't matter if there is an n-th axis (n>3). The second axis of a slice is always the third axis of the movie cube. For the slices one enters movie frame numbers which sets the values along the new slice axis. Example: With GIDS and a big data cube with many spectral observations one can enter images at each second velocity to speed up a movie. If the number of spectral images are too many to display in an XV slice, one can enter a selection of movie images which contribute to the spectral axis in your XV slices. Example: In GIDS one can upload 2-dim subsets of a 4 dimensional data set. The order of the slices are set by grids on the repeat axes (e.g. FREQ, STOKES) and this will also be the order in the movie cube. This will also be the default order along the V axis in you slice, which is strictly speaking not a V axis anymore. In the XV slices there are also mouse interactions. The position in V sets a movie frame. A position in X in one panel updates the other XV panel. We try to achieve the same functionality as in GIDS but in the context of module maputils. That is, we don't use X-memory to store the movie data (the movie cube). With maputils we build a sequence of movieframes and from this list with 2-dim data arrays we retrieve the slice data. So one of the slice axes is the index axis of the movie frames and the other is one of the movie frame axes. For a sequence of data sources (data sets), we extend the movie loop with new frames and add slice panels (instead of extending the previous panels). TODO: Server functionality in the new viewer by adding communication via wkey()'s -Verbeteren positie informatie """ #------------------------------------------------------------------------
[docs]class Cubeatts(object): #---------------------------------------------------------------------------- """ Class that sets cube attributes. The constructor is called by the :meth:`Cubes.append` method of class :class:`maputils.Cubes` and should not be used otherwise. We document the class because it has some useful attributes. The attributes should only be used as a read-only attribute. :param xxx: All parameters are set by the :class:`maputils.Cubes` class. :Attributes: .. attribute:: frame Matplotlib Axes object used to display the current image. The attribute should only be used as a read-only attribute. .. attribute:: pxlim, pylim The axes limits in pixels. The attribute should only be used as a read-only attribute. .. attribute:: slicepos Attribute ``slicepos`` is documented in :meth:`Cubes.append`. It is a sequence with numbers or tuples with numbers. The attribute should only be used as a read-only attribute. .. attribute:: fitsobj Object from class :class:`maputils.FITSimage` .. attribute:: pixelaspectratio This attribute stores the aspect ratio of the pixels in the images of this cube. .. attribute:: nummovieframes This attribute stores the number of images in this cube. .. attribute:: hasgraticule Boolean which flags whether one requested to plot a graticule .. attribute:: gridmode Boolean which flags wheter one requested positions in grids or in pixels. Pixels follow the FITS syntax. That is, the first pixel along an axis is pixel 1. The last pixel is pixel NAXISn. NAXIS is a header item and n is the axis number (starting with 1). FITS headers also contain an key CRPIXn. This is the number (may be non-integer) of the pixel where the projection center is located. If ``gridmode==True``, the position of CRPIX is labeled 0. The mode applies to the output of positions. .. attribute:: cmap The current color lookup table (color mapping). .. attribute:: hascolbar Boolean which flag whether one requested to draw a color bar. .. attribute:: vmin, vmax Attributes which store the minimum and maximum clip values to display the data. .. attribute:: datmin, datmax Attributes which stores the minimum and maximum data values. They are used to set the default clip levels. .. attribute:: mean Mean of all the data values in this cube. It gets its value for certain values of attribute ``clipmode``. .. attribute:: rms Root mean square of all the data values in this cube. It gets its value for certain values of attribute ``clipmode``. .. attribute:: clipmode Stores the requested mode which sets the clip values. The modes are documented in method :meth:`Cubes.append`. .. attribute:: clipmn Stores the requested number of times (m, n) the value of the rms is used to set the clip values around the mean. The modes are documented in method :meth:`Cubes.append`. """ #---------------------------------------------------------------------------- def __init__(self, frame, fitsobj, axnums, slicepos=[], pxlim=None, pylim=None, vmin=None, vmax=None, hasgraticule=False, gridmode=False, hascolbar=True, pixelaspectratio=None, clipmode=0, clipmn=(4,5), callbackslist={}): self.frame = frame self.pxlim = pxlim self.pylim = pylim # If there are no limits given, then calculate some defaults if pxlim is None: self.pxlim = (1, fitsobj.axisinfo[axnums[0]].axlen) if pylim is None: self.pylim = (1, fitsobj.axisinfo[axnums[1]].axlen) # 'slicepos' is a list with tuples. Each tuple sets positions # on repeat axes. For 2-dim images, the list is empty self.slicepos = slicepos self.fitsobj = fitsobj # The source from which the data is extracted self.pixelaspectratio = pixelaspectratio if slicepos: self.nummovieframes = len(slicepos) else: self.nummovieframes = 1 self.hasgraticule = hasgraticule self.gridmode = gridmode # If True then positions are displayed in grids self.xold = None # Remember last visited position self.yold = None self.framep1 = None # Initialize the panel frames self.framep2 = None self.annimp1 = None # Annotated image object for panels 1 and 2 self.annimp2 = None self.linepieces = set() self.textlabels = set() self.grat = None # Axis numbers for images and extra axis which # is one of the repeat axes (usually velocity is axnum 3) # A FITSimage object has two attributes that could have been useful # to find names and positions of the slice (repeat) axes. # However these are just default values if method set_imageaxes() has not # been called. This call is postponed until we create Annotatedimage # objects. So for now we need to build our own axis numbers array. # The first two numbers are the axis numbers of the image (one or a # sequence that belongs to this cube). anums = [axnums[0], axnums[1]] n = fitsobj.naxis if (n > 2): # Note that we have our slice axes always in the same order as in the header for i in range(n): axnr = i + 1 if axnr not in anums: anums.append(axnr) self.axnums = anums # To extract slices we need to know the axis order self.movieframeoffset = None # Gets a value after loading a cube # For GUI's where one stores multiple cubes etc. we want to know what the # last image is if we change cubes. So we need an attribute that stores an image number. # Initially it is set to the offset (number of images in previous cubes) # It is not changed in maputils if images are changed in a cube, but the # calling environment can keep track of it and update this attribute self.lastimagenr = 0 # Gets the value of movieframeoffset after loading self.panelXframes = [] # Initialize the slice panels self.panelYframes = [] self.cmap = None """ if vmin is None or vmax is None: vmi, vma = fitsobj.get_dataminmax(box=False) if vmin is None: self.vmin = vmi if vmax is None: self.vmax = vma """ self.panelscb = None self.panelscbkey = None self.splitcb = None self.origdata = None # Set the properties for the slice panel images self.set_xvprojections() self.preparefirst = True self.cnr = None # Cube number in container self.hascolbar = hascolbar #if hascolbar and self.cbframe is None: self.cbframe = None self.imagesinthiscube = [] self.shortfilename = os_basename(self.fitsobj.filename) self.callbackslist = callbackslist # Crosshair lines lineprops = {'animated': False,'linewidth':1} self.lineh = self.frame.axhline(xmin=MINBOUND, xmax=MAXBOUND, visible=False, **lineprops) #self.lineh = self.frame.axhline(self.frame.get_ybound()[0], visible=False, **lineprops) self.linev = self.frame.axvline(ymin=MINBOUND, ymax=MAXBOUND, visible=False, **lineprops) # Every cube has an xpanel and a y panel which also can have a crosshair # These line pieces are created when we create the frames self.panelxline = None self.panelxline2 = None self.panelyline = None self.panelyline2 = None self.needclear = False self.background = None self.panelXback = None # For crosshair in x/y panels self.panelYback = None # Clip level related attributes self.vmin = vmin self.vmax = vmax self.zmin = vmin self.zmax = vmax self.vmin_def = None self.vmax_def = None self.datmin = None self.datmax = None self.mean = None self.rms = None self.clipmode = clipmode self.clipmn = clipmn self.scalefac = None self.limitsinfo = None # String that show the max. limits of the axes self.setlimits = [] # xlo, xhi, ylo, yhi def callback(self, cbid, *arg): #----------------------------------------------------------------- """ Helper function for registered callbacks """ #----------------------------------------------------------------- if cbid in self.callbackslist: self.callbackslist[cbid](*arg) def cubestats(self): #------------------------------------------------------------------------- """ Given the datmin, datmax, mean and rms attributes of all the images in this cube, calculate the same attributes for the entire cube Formulas are explained in article on: http://en.wikipedia.org/wiki/Standard_deviation Section: Population-based statistics """ #------------------------------------------------------------------------- if not self.imagesinthiscube: # To be save. Here is nothing to do return alldatmin = [] alldatmax = [] allmean = [] allrms = [] for aim in self.imagesinthiscube: if not (aim.datmin is None) and numpy.isfinite(aim.datmin): # Not None, Nan or +-inf alldatmin.append(aim.datmin) if not (aim.datmax is None) and numpy.isfinite(aim.datmax): alldatmax.append(aim.datmax) if not (aim.mean is None) and numpy.isfinite(aim.mean): allmean.append(aim.mean) if not (aim.rms is None) and numpy.isfinite(aim.rms): allrms.append(aim.rms) if len(alldatmin): self.datmin = min(list(alldatmin)) # Could be one value if len(alldatmax): self.datmax = max(list(alldatmax)) # To calculate the mean and rms of all the data we use a formula # to get mean and rms as a result of a list with means and rms's # for data sets with an equal number of data points n = 0; mean_t = 0.0; rms_t = 0.0 for mean, rms in zip(allmean, allrms): #flushprint("mean, rms=%f %f"%(mean, rms)) if rms and mean: #flushprint("n=%d"% n) n += 1 mean_t += mean rms_t += rms*rms + mean*mean if n: self.mean = mean_t/n self.rms = numpy.sqrt(rms_t/n - (self.mean*self.mean)) else: self.mean = self.rms = None def imagestats(self): #------------------------------------------------------------------------- """ For each image in this cube, calculate the values for the data min., data max., mean and rms. """ #------------------------------------------------------------------------- if not self.imagesinthiscube: # To be save. Here is nothing to do return for aim in self.imagesinthiscube: aim.datmin, aim.datmax, aim.mean, aim.rms = aim.get_stats() # Get same attributes applied to all cube images self.cubestats() def setcubeclips(self, clipmin=None, clipmax=None, clipmode=0, clipmn=(4,5)): #---------------------------------------------------------------------------- # Purpose: Set the norm limits for image scaling to all images in this cube # # Use this method if you want to use the same clip levels for all the # images in this cube. Each image has attributes clipmin and clipmax that # are finite numbers (forced by the constructor of Annotatedimage. # Use the min and max of all these clip values to set the norm for all # images in the list. #---------------------------------------------------------------------------- if not self.imagesinthiscube: # To be save. Here is nothing to do return #flushprint("setcubeclips: clipmin, max=%s %s"%(str(clipmin), str(clipmax))) if None in (clipmin, clipmax) or clipmode in [1,2]: # Set values for cube.datmin, cube.datmax etc. if None in [self.datmin, self.datmax, self.mean, self.rms]: self.cubestats() #flushprint("setcubeclips: clipmode=%d"%clipmode) if clipmode == 0: if clipmin is None: clipmin = self.datmin if clipmax is None: clipmax = self.datmax if clipmode == 1: clipmin = self.datmin clipmax = self.datmax #flushprint("setcubeclips: MODE=1 clipmin, max=%s %s"%(str(clipmin), str(clipmax))) if clipmode == 2: if self.mean and self.rms: clipmin = self.mean - clipmn[0]*self.rms clipmax = self.mean + clipmn[1]*self.rms #flushprint("setcubeclips: MODE=2 mean, rms=%s %s"%(str(self.mean), str(self.rms))) #flushprint("setcubeclips: MODE=2 clipmin, max=%s %s"%(str(clipmin), str(clipmax))) if clipmin is None: if clipmax is not None: clipmin = clipmax - 1.0 else: clipmin = 0.0 if clipmax is None: if clipmin is not None: clipmax = clipmin + 1.0 else: clipmax = 1.0 self.vmin = clipmin self.vmax = clipmax for aim in self.imagesinthiscube: # Prevent exception if min > max aim.image.im.set_clim(min(clipmin,clipmax), max(clipmin,clipmax)) #TODO hier nog aim.clipmin en aim.clipmax updaten (heeft dit consequenties?) canvas = aim.frame.figure.canvas canvas.draw() def set_slicemessages(self, spectrans): #------------------------------------------------------------------------- """ Change slice messages to comply with this spectral translation. Apply message change to all Annotatedimage objects in this cube. :param spectrans: A coded spectral translation e.g. FREQ, WAVE or VRAD with translation code (e.g. V2F) or questionmarks as wildcard (e.g. VRAD-???) :type spectrans: String :Notes: After the slice messages are set, we have to refresh the image if a message was required on the image. After loading this is done by setting the image to the first loaded image (for this cube). If this method is called externally, the caller is responsible for refreshing the current image. """ #------------------------------------------------------------------------- #flushprint("set_sliceimages = %s"%(str(self.slicepos))) if not self.imagesinthiscube: # To be save. Here is nothing to do return axnames = [] crpix = [] # Note: these are the axis numbers with sorted repeat axes! for an in self.axnums[2:]: axi = self.fitsobj.axisinfo[an] if axi.wcstype == 'spectral' and spectrans: # Remove the F2W etc. extensions axnames.append(spectrans.split('-')[0]) else: axnames.append(axi.axname) # If we work in grid mode, we need to translate the slice positions # to grids. For this we need the CRPIX header values. crpix.append(axi.crpix) for i, aim in enumerate(self.imagesinthiscube): j = i + self.movieframeoffset # This message should contain only the information about # the slice. It will be reused for the mouse position # in physical coordinates. The non image axes of the slice # panels, could be composed of multiple axes (e.g. FREQ, STOKES) # and cannot be represented by module wcsgrat. # We compose a more informative string (sc) to be used in the calling # environment using the 'slicemes' callback. s = "" sc = "im%3d: %s "%(j, os_basename(self.fitsobj.filename)) if self.slicepos: # Note that each fitsobj has one 'slicepos'. This value # can be a tuple if there is more than one repeat axis # If not, we have to make it a sequence for slice2world() # slicepos values follow the FITS syntax, i.e. start at 1 if not issequence(self.slicepos[i]): splist = [self.slicepos[i]] else: splist = self.slicepos[i] self.fitsobj.slicepos = splist vlist, ulist = self.fitsobj.slice2world(skyout=None, spectra=spectrans, userunits=None) #print("SLICEPOS=", self.slicepos[i], vlist, ulist,self.axnums) # If the slice position is required in grids, convert using CRPIX values if self.gridmode: #flushprint("len(splist=%d, %s %s"%(len(splist),str(splist), str(aim.projection.crpix))) splist = [splist[k]-crpix[k] for k in range(len(splist))] for v, u, aname, sp in zip(vlist, ulist, axnames, splist): #s += "%s(%*d)=%+-*g"%(aname, 4, sp, 10, v) s += "%s(%d)=%+-g"%(aname, sp, v) if u: s += " (%s)"%u s += " " s = s.strip() aim.slicemessage = s aim.composed_slicemessage = sc + ' ' + s aim.imnr = j aim.spectrans = spectrans def set_panelXframes(self, framelist): #------------------------------------------------------------------------- """ For the current cube, build a list with indices which all represent a slice in the panel that is parallel to the X axis. Prepare a data array for this slice panel. Remember that you have set up a 'movie cube' (which can be composed out of more than one data source). In this movie cube one can imagine a third axis perpendicular to the image axes. Along this axis, one can take a slice through the data. The sliced data can be represented in an image (e.g. a position-velocity diagram). The positions on the third axis are indices of the movie frame array and need not to be contiguous or ordered. """ #------------------------------------------------------------------------- self.panelXframes = framelist # Should be filtered and corrected for cube offset in movie frames if self.panelXframes: lx = int(self.pxlim[1] - self.pxlim[0] + 1) # Length of x axis of movie image ly = int(len(self.panelXframes)) # Length of new axis is equal to the number # of selected movie frames self.Mx = numpy.zeros((ly,lx)) # Image for horizontal panel def set_panelYframes(self, framelist): #------------------------------------------------------------------------- """ For the current cube, build a list with indices which all represent a slice in the panel that is parallel to the Y axis. Prepare a data array for this slice panel. See also text at set_panelXframes(). """ #------------------------------------------------------------------------- self.panelYframes = framelist if self.panelYframes: ly = int(self.pylim[1] - self.pylim[0] + 1) lx = int(len(self.panelYframes)) self.My = numpy.zeros((ly,lx)) # Image for vertical panel def set_xvprojections(self): #---------------------------------------------------------------------------- """ Is there an axis in the set that is not part of the image? We may need this axis later if we need a 'missing' spatial axis """ #---------------------------------------------------------------------------- ap = set(range(1,self.fitsobj.naxis+1)) apsub = set(self.axnums[:2]) apdiff = list(ap.difference(apsub)) if apdiff: dummyax = apdiff[0] else: dummyax = None # Find a suitable axis to represent the movieframe index axis # For the panel that shares the x axis, this will be the y axis. fo = self.fitsobj if dummyax: spatials = [fo.proj.lataxnum, fo.proj.lonaxnum] if self.axnums[0] in spatials and dummyax in spatials: convproj = fo.proj.sub((self.axnums[0], dummyax)) else: if not (self.axnums[0] in spatials or dummyax in spatials): axperm = (self.axnums[0], dummyax) convproj = fo.proj.sub(axperm) else: if fo.proj.lonaxnum in [dummyax,self.axnums[0]]: axperm = (self.axnums[0], dummyax, fo.proj.lataxnum) convproj = fo.proj.sub(axperm) else: axperm = (self.axnums[0], dummyax, fo.proj.lonaxnum) convproj = fo.proj.sub(axperm) self.panelx_axperm = (self.axnums[0], dummyax) else: convproj = fo.convproj self.panelx_axperm = (self.axnums[0], self.axnums[1]) self.panelx_proj = convproj # Now for the slice panel that shares the y axis if dummyax: spatials = [fo.proj.lataxnum, fo.proj.lonaxnum] if self.axnums[1] in spatials and dummyax in spatials: convproj = fo.proj.sub((dummyax, self.axnums[1])) else: if not (self.axnums[1] in spatials or dummyax in spatials): axperm = (self.axnums[1], dummyax) convproj = fo.proj.sub(axperm) else: if fo.proj.lonaxnum in [dummyax,self.axnums[1]] : axperm = (dummyax, self.axnums[1], fo.proj.lataxnum) convproj = fo.proj.sub(axperm) else: axperm = (dummyax, self.axnums[1], fo.proj.lonaxnum) convproj = fo.proj.sub(axperm) self.panely_axperm = (dummyax, self.axnums[1]) else: convproj = fo.convproj self.panely_axperm = (self.axnums[0], self.axnums[1]) self.panely_proj = convproj
[docs]class Cubes(object): #---------------------------------------------------------------------------- """ A container with Cubeatts objects. With this class we build the movie container which can store images from different data cubes. :param toolbarinfo: This flag sets an informative message in the toolbar. For the QT backend the message is printed on a separate line at the bottom of the plot. The message contains information about the position of the cursor and when possible, it will show the corresponding world coordinates. Also the pixel value is printed. If you display a slice panel, then the information about the mouse position is different. For example the y direction of the horizontal slice panel is not a function of a world coordinate, but a function of movie frame number. This number can be used to find a world coordinate. :type toolbarinfo: Boolean :param imageinfo: If set to True, a label will be plotted in the upper left corner of your plot. The label contains information about the current image, such as the name of the repeat axis and the value on the repeat axis in physical coordinates. :type imageinfo: Boolean :param printload: If True, print a message on the terminal about the image that is loading. :type printload: Boolean :param helptext: Print a message at the bottom of the figure with information about the navigation. :type helptext: Boolean :param callbackslist: To be able to interfere or interact with the cube viewer we need to know the status of processes in maputils, like when is the loading of images finished or when does the image viewer switches between two images from a different cube. These events are sometimes related to the container with cubes, sometimes they are specific for a cube, see also :meth:`Cubes.append`. To handle these events, one must connect it to a function or an object with methods. For a :class:`maputils.Cubes` container we have defined two events: * 'progressbar' -- The progress bar is an object from a class that (somehow) displays the progress. It should have at least four methods: 1) ``setMinimum()`` 2) ``setMaximum()`` 3) ``setValue()`` 4) ``reset()`` For example in a PyQt4 gui we can define `progressbar` with an object from class ``QProgressBar()``. This class provides already the necessary methods. The PyQt4 progress bar displays percentages. Maputils sets its range between zero and the number of movie frames (images) contained in the new cube. Usually, loading 1 image is too fast for a useful progress bar, but with numerous images in a cube, it is convenient to know the progress of the loading process. * 'memory' -- Every time Maputils calls a function associated with trigger 'memory', a string is generated with a 'used memory' report. The function should have only one argument and this argument is set by Maputils to the requested string with memory information. * 'cubechanged' -- If images are loaded from different data cubes and a user changes the viewer to display an image from a different cube, then the handler for this trigger is executed. The trigger must be handled by a function with at least one parameter and this parameter (the first) is returned as object from class :class:`maputils.Annotatedimage` * 'imagereset' -- Actions like transparency setting, splitting, blurring and histogram equalization are restricted to the current image and are undone after changing to another image in the movie container. This trigger can update a GUI with this information (e.g. reset a transparency slider). The trigger must be handled by a function that does not need to have any parameter because nothing will be returned. * 'movchanged' -- The function that handles the event has a parameter which is an object with four attributes: * ``mes`` -- A string which contains information about the slicemessage * ``indx`` -- The index of the image in the movie container with all images * ``cubenr`` -- The index of the cube to which the current image belongs. The index is for the list :attr:`maputils.Cubes.cubelist` * ``slicemessage`` -- A string with information about the image (name and axis names) * 'speedchanged' -- User changed frame rate. The function has at least one parameter (the first) which stores the new frame rate in frames per second. * 'moviepaused' -- User pressed keyboard key 'P' to start or stop a movie. The function has at least two parameters (the first and second) which stores the the pause status as a Boolean and the forward/backward status as a Boolean. * 'finished' -- All images are loaded and mouse- and keyboard interaction with the canvas is enabled. A similar callback is found in method :meth:`Cubes.append` but the difference is that 'finished' is triggered only after all stacked append actions are finished, while 'cubeloaded` is triggered as soon images from a cube are loaded. :type callbackslist: Python dictionary with function names and function pointers :Example: Example 1 (Use callback functions in the constructor of a Cubes object) :: from matplotlib.pyplot import figure, show from kapteyn import maputils def writeMemory(mem): print(mem) class ProgressBar(object): def __init__(self): self.value = 0 def setMinimum(self, x): self.minimum = x def setMaximum(self, x): self.maximum = x def setValue(self, x): self.value = x print("Progress: %d %%"%(100.0*self.value/(self.maximum-self.minimum))) def reset(self): self.value = self.minimum def cubeChanged(newim, x=10): print("Viewer changed cube with image", newim, x) def imageChange(imageinfo): print("New image with movie container index:", imageinfo.indx) print("Image belongs to cube number:", imageinfo.cubenr) print("Short info:", imageinfo.mes) print("Long info:", imageinfo.slicemessage) def imageReset(): print("User changed images, but the previous was changed by temporary settings (e.g. splitting)") def printFinish(): print("Loading finished and user interaction has been started") def speedChanged(framerate): print("User changes speed of movie loop to %d fr/s"%framerate) fig = figure() frame = fig.add_subplot(1,1,1) progressBar = ProgressBar() myCubes = maputils.Cubes(fig, toolbarinfo=True, printload=False, callbackslist={'memory' : writeMemory, 'progressbar' : progressBar, 'cubechanged' : cubeChanged, 'imagereset' : imageReset, 'speedchanged' : speedChanged, 'movchanged' : imageChange, 'finished' : printFinish}) fitsobject = maputils.FITSimage('ngc6946.fits') myCubes.append(frame, fitsobject, axnums=(1,2), slicepos=list(range(1,101))) frame = fig.add_subplot(1,1,1, label='m101') fitsobject2 = maputils.FITSimage('m101.fits') myCubes.append(frame, fitsobject2, axnums=(1,2)) :Example: Use two different cube containers connected to two different figures :: from matplotlib.pyplot import figure, show from kapteyn import maputils def externalMes(pos): print("Position cube 1:", pos) def externalMes2(pos): print("Position cube 2:", pos) def postLoading(cubecontainer, splitfr, slicepos): print("Set frame to compare with to:", splitfr) cubecontainer.set_splitimagenr(splitfr) cubecontainer.set_panelframes(list(range(len(slicepos))), panel='XY') fig = figure() myCubes = maputils.Cubes(fig, toolbarinfo=True, printload=False) frame = fig.add_subplot(1,1,1, label='ngc6946') fitsobject = maputils.FITSimage('ngc6946.fits') splitfr = 30 slicepos = list(range(1,102)) # Create a lambda function, but make sure it uses the current values # for the cube container (Cubes() object) cubeLoaded = lambda A=myCubes, B=splitfr, C=slicepos: postLoading(A,B,C) myCubes.append(frame, fitsobject, axnums=(1,2), slicepos=slicepos, callbackslist={'exmes' : externalMes, 'cubeloaded' : cubeLoaded}) fig2 = figure(2) myCubes = maputils.Cubes(fig2, toolbarinfo=True, printload=False) frame2 = fig2.add_subplot(1,1,1, label='m101') fitsobject2 = maputils.FITSimage('ngc6946.fits') myCubes.append(frame2, fitsobject2, axnums=(1,3), slicepos=list(range(1,101)), callbackslist={'exmes' : externalMes2}) show() :Attributes: .. attribute:: calbackslist List with strings which correspond to the triggers in parameter `maputils.Cubes.callbackslist` The list is filtered and contains only known triggers. Triggers for the MovieContainer object are filtered and passed to the constructor of this object. .. attribute:: colbwpixels Class attribute which sets a fixed width for the color bar in pixels. e.g.:: myCubes = maputils.Cubes(fig, toolbarinfo=True, printload=False) myCubes.colbwpixels = 100 .. attribute:: crosshair Boolean which flags whether a cross hair cursor has been set or not. .. attribute:: cubelist Python list with cubes appended to this list by method :meth:`Cubes.append`. .. attribute:: currentcube This cube containers can store multiple cubes. Each cube (object of class :class:`maputils.Cubeatts`) is stored in the list :attr:`maputils.Cubes.cubelist`. The current cube is the cube to which the image on display belongs. This current cube is also an object of :class:`maputils.Cubeatts`. .. attribute:: fig The Matplotlib Figure used to display the images in the cubes container .. attribute:: imageinfo The status of Boolean parameter ``imageinfo`` .. attribute:: numcubes The number of cubes with images stored in this container .. attribute:: movieimages The container of class :class:`maputils.MovieContainer` .. attribute:: printload The status of Boolean parameter ``printload`` .. attribute:: splitmovieframe Number of movie frame which contains the image which we want to use together with the current image to split. The attribute is set with :meth:`Cubes.set_splitimagenr`. .. attribute:: toolbarinfo The status of Boolean parameter ``toolbarinfo`` .. attribute:: zprofileheight Class attribute to set the height of the plot with the z-profile. The height is given in normalized device coordinates [0,1]. The height is set to 0.1 by default. The other plots on your canvas will adjust to fit the new height. You can set the new height immediately after you created a cubes container, e.g.:: myCubes = maputils.Cubes(fig, toolbarinfo=True, printload=False) myCubes.zprofileheight = 0.3 .. attribute:: zprofileOn Boolean which flags whether a z-profile plot has been requested or not. :Methods: .. automethod:: append .. automethod:: set_aspectratio .. automethod:: set_colbar_on .. automethod:: set_coordinate_mode .. automethod:: set_crosshair .. automethod:: set_graticule_on .. automethod:: set_panelframes .. automethod:: set_skyout .. automethod:: set_spectrans .. automethod:: set_splitimagenr .. automethod:: set_zprofile .. automethod:: zprofileLimits """ # Changelog: #-18 aug 2011: Changed toolbar message for QT backends, i.e. do not cut off #---------------------------------------------------------------------------- # Class variables: # Define the required colorbar width. The width remains the same after # a resize of the canvas. colbwidth = 0.06 # A dummy value that stores width of colorbar in device coordinates colbwpixels = 40 # Fixed width of colorbar in pixels zprofileheight = 0.1 def __init__(self, fig, toolbarinfo=False, imageinfo=True, printload=False, helptext=True, callbackslist={}): # Clean and separate the list with callbacks triggers # One list is for the movie container which always contains the # internal triggers for cross hair cursor and z-profile. # The other triggers are for the Cubes container. keysM = ['cubechanged', 'movchanged', 'imagereset', 'speedchanged', 'moviepause'] dictM = {'crosshair':self.drawCrosshair, 'zprofile':self.plotZprofile} keysC = ['progressbar', 'memory', 'finished'] dictC = {} for k in keysM: if k in callbackslist: dictM[k] = callbackslist[k] for k in keysC: if k in callbackslist: dictC[k] = callbackslist[k] self.numcubes = 0 self.movieimages = MovieContainer(fig, helptext=helptext, toolbarinfo=toolbarinfo, imageinfo=imageinfo, callbackslist = dictM) # Initialize two callback id's for reacting to keys and scroll wheel # for movie actions in the figure. These are not of type AxexCallbacks # because the actions are not restricted to a frame. self.fig = fig # The timerlist must be accessible by all objects of this instance. self.timerlist = [] # One list with timer callbacks to load all cubes self.movieimages.cidkey = None self.movieimages.cidscroll = None self.toolbarinfo = toolbarinfo self.imageinfo = imageinfo self.imageloadnr = 0 # A counter for images in all cubes self.movieframecounter = 0 # Only used in the loading process self.currentcube = 0 self.cubelist = [] self.maxframe = [None]*4 # Corner positions of biggest frame # Every cube represents an number of movie frames # If there is more than one cube, you need an offset to find the # correspondence between a position in a slice panel, the cube and the # index of the movie frame. self.movieframeoffset = 0 self.cubeindx = 0 self.loadcb = None # The callback for the image load method #self.cmap = None self.printload = printload self.splitmovieframe = 0 self.panelmode = 0 self.infoobj = None # Usually one loads images in Matplotlib, selects one image to # be visible and the call show() to display this image. By setting images # to visible/invisible, we create a movie effect. # In our load process, we want to show each loaded image immediately. # so not only the movie loop software, but also the load procedure # should know how to make an image visible or invisible. For the load # procedure we use attribute 'lastimage' to keep track of which image # was the last one that was on display. self.lastimage = None # We want to start to display the images as big as possible, given the # size of the mpl canvas. We do this only one time, because appended # cubes will follow this adjustment. So we need a flag to inspect # if this figure adjustment has ben done or not. self.figadjust = False #self.resizecb = CanvasCallback(self.reposition, fig.canvas, 'draw_event') self.resizecb = CanvasCallback(self.reposition, fig.canvas, 'resize_event') NavigationToolbar2.ext_callback = self.reposition NavigationToolbar2.ext_callback2 = self.set_graticules Figure.ext_callback = self.reposition # From the subplot configurator # If we want to redraw after a draw_event, we must prevent that this draw # action also executes method 'reposition'. So we need a flag to prevent this. #self.resizecb.update = True #flushprint("I CONNECT DRAW_EVENT to REPOSITION()") self.callbackslist = dictC if 'progressbar' in self.callbackslist: self.progressbar = self.callbackslist['progressbar'] else: self.progressbar = None if 'memory' in self.callbackslist: self.memorystatus = self.callbackslist['memory'] else: self.memorystatus = None # We need administration to keep track of the number of cubes that # define side panels. Then it is possible to use maximum space for these # panels to plot. self.numXpanels = 0 self.numYpanels = 0 # Needed to take re-position action of side panels when h/wspace changes self.previous_subpars = (fig.subplotpars.hspace, fig.subplotpars.wspace) self.crosshair = False self.zprofileOn = False self.zprofileframe = None self.zprofileXdata = None self.zprofileYdata = None def callback(self, cbid, *arg): #----------------------------------------------------------------- """ Helper function for registered callbacks """ #----------------------------------------------------------------- if cbid in self.callbackslist: self.callbackslist[cbid](*arg) def plotZprofile(self, cube, x, y, cubenr=None, pos=None): #----------------------------------------------------------------- """ Purpose: This method plots a Z-profile for the current cube at the current position. The method can be called either with the current cube or with the (index) number of the current cube. For the cursor position in x and y, the image value is extracted from all the images in the cube. This way we build a so called Z-profile. The profile is updated if you move the mouse (event causes execution of method self.updatepanels). But it is also triggered with a callback in toggle_images if the cube changed. We use blitting to speed up the plotting of the profile. """ #----------------------------------------------------------------- if not self.zprofileOn: return if cube is None: # Select cube either by id or index number if cubenr is None: return else: cube = self.cubelist[cubenr] # Position could be a string if pos is not None: im = cube.imagesinthiscube[0] # There is always one image, so take first world, pixels, units, errmes = str2pos(pos, im.projection, mixpix=im.mixpix, gridmode=im.gridmode) if errmes != '': return errmes else: x = float(pixels[:,0]) # Otherwise the value is an array y = float(pixels[:,1]) #s = "Plot profile at %f %f"%(x,y) #flushprint(s) if x is None: x = cube.xold if y is None: y = cube.yold if None in [x, y]: # Not a valid position in this cube return if self.zprofileframe.changed: framechanged = True self.zprofileframe.changed = False else: framechanged = False if cube.zmin is None: cube.zmin = cube.vmin if cube.zmax is None: cube.zmax = cube.vmax clipchanged = False if cube.zmin != self.zprofileframe.clipmin: clipchanged = True if cube.zmax != self.zprofileframe.clipmax: clipchanged = True # Build the Z-profile yyy = [] xi = int(numpy.round(x - (cube.pxlim[0]-1))) yi = int(numpy.round(y - (cube.pylim[0]-1))) if xi < 1 or xi > (cube.pxlim[1] - cube.pxlim[0] + 1): return if yi < 1 or yi > (cube.pylim[1] - cube.pylim[0] + 1): return #flushprint("zprofile x,y=%f %f"%(x,y)) yyy = [aim.data[yi-1, xi-1] for aim in cube.imagesinthiscube] #for aim in cube.imagesinthiscube: # z = aim.data[yi-1, xi-1] # yyy.append(z) start = cube.movieframeoffset xxx = numpy.arange(start, start+len(yyy)) xxx1 = numpy.zeros(2*len(xxx)) yyy1 = numpy.zeros(2*len(xxx)) j = 0 for i in range(len(xxx)): xxx1[j] = xxx[i] - 0.5 xxx1[j+1] = xxx[i] + 0.5 yyy1[j] = yyy[i] yyy1[j+1] = yyy[i] j += 2 xxx = xxx1 yyy = yyy1 offset = cube.imagesinthiscube[0].pixoffset v = "%+3d %+3d"%(round(x-int(offset[0])),round(y-int(offset[1]))) s = "Z-profile at: x,y=".ljust(22) + v.rjust(10) self.zprofileframe.XYinfo.set_text(s) # Restore the frame and re-draw the profile lines canvas = self.zprofileframe.figure.canvas if not self.zprofileYdata or self.zprofileframe.cubeindx != cube.cnr or framechanged or clipchanged: # A Line2D object does not yet exists or the cube changed self.zprofileframe.XYinfo.set_text("") self.zprofileframe.set_xlim(xxx.min(),xxx.max()) self.zprofileframe.set_ylim(cube.zmin,cube.zmax) if self.zprofileYdata: self.zprofileYdata.set_xdata([]) self.zprofileYdata.set_ydata([]) canvas.draw() self.zprofileframe.background = canvas.copy_from_bbox(self.zprofileframe.bbox) self.zprofileYdata, = self.zprofileframe.plot(xxx, yyy, 'g', animated=False) self.zprofileframe.cubeindx = cube.cnr # Add new attribute else: canvas.restore_region(self.zprofileframe.background) self.zprofileYdata.set_ydata(yyy) self.zprofileframe.draw_artist(self.zprofileYdata) self.zprofileframe.draw_artist(self.zprofileframe.XYinfo) canvas.blit(self.zprofileframe.bbox) self.zprofileframe.clipmin = cube.zmin self.zprofileframe.clipmax = cube.zmax cube.ZprofileXold = x cube.ZprofileYold = y
[docs] def zprofileLimits(self, cubenr, zmin=None, zmax=None): #----------------------------------------------------------------- """ Set limits of plot with Z_profile for the current cube. :param cubenr: Index of the cube in Cubes.cubelist for which we want to adjust the range in image values in the z-profile :type cubenr: Integer :param zmin: Clip Y range (image values) in z-profile. ``zmin`` is the minimum value. If set to None, the minimum data value in the corresponding cube is used. :type zmin: Float :param zmax: Clip Y range (image values) in z-profile. ``zmax`` is the maximum value. If set to None, the maximum data value in the corresponding cube is used. :type zmax: Float :Notes: See example at :meth:`Cubes.set_zprofile` """ #----------------------------------------------------------------- cube = self.cubelist[cubenr] if not self.zprofileOn: return if zmin is None: zmin = cube.vmin if zmax is None: zmax = cube.vmax self.zprofileframe.set_ylim(zmin, zmax) #flushprint("I redraw with zmin,zmax=%f %f"%(zmin, zmax)) self.zprofileframe.figure.canvas.draw() cube.zmin = zmin cube.zmax = zmax
def drawCrosshair(self, framenr, cubechanged=False, oldcubenr=-1): #----------------------------------------------------------------- """ Another image appeared on screen. If there are panels and you have a crosshair cursor, then they should be re-drawn. In the panels, the positions of the cursor line should change because another image in the cube is displayed. """ #----------------------------------------------------------------- if not self.crosshair: return # What is the corresponding cube of the new image? currentimage = self.movieimages.annimagelist[framenr] cnr = currentimage.cubenr cube = self.cubelist[cnr] # A cube changed. We need to remove the crosshair in the old cube if cubechanged: #flushprint("Cube changed remove old lines") oldcube = self.cubelist[oldcubenr] oldcube.linev.set_visible(False) oldcube.lineh.set_visible(False) oldcube.frame.draw_artist(oldcube.linev) oldcube.frame.draw_artist(oldcube.linev) if oldcube.panelXframes: oldcube.panelxline.set_visible(False) oldcube.framep1.draw_artist(oldcube.panelxline) oldcube.panelxline2.set_visible(False) oldcube.framep1.draw_artist(oldcube.panelxline2) if oldcube.panelYframes: oldcube.panelyline.set_visible(False) oldcube.framep2.draw_artist(oldcube.panelyline) oldcube.panelyline2.set_visible(False) oldcube.framep2.draw_artist(oldcube.panelyline2) #flushprint("Ik ga lijnen verwijderen uit oude kubus") self.fig.canvas.draw() self.fig.canvas.flush_events() # We are sure that the background changed, so we need to make a new copy. #cube.linev.set_visible(False) #cube.lineh.set_visible(False) #flushprint("Ik ga hier de achtergrond kopieren") cube.background = self.fig.canvas.copy_from_bbox(cube.frame.bbox) if not cube.panelXback and cube.panelXframes: cube.panelXback = self.fig.canvas.copy_from_bbox(cube.framep1.bbox) if not cube.panelYback and cube.panelYframes: cube.panelYback = self.fig.canvas.copy_from_bbox(cube.framep2.bbox) # If there is no previous position, then take the middle of the box # This way you can plot a crosshair at the start if cube.xold is None: cube.xold = (currentimage.box[1]+currentimage.box[0])/2.0 if cube.yold is None: cube.yold = (currentimage.box[3]+currentimage.box[2])/2.0 #flushprint("xold, yold=%f %f %s"%(cube.xold, cube.yold, str(currentimage.box))) x = cube.xold y = cube.yold # flushprint("Crosshair in drawCrosshair after setimage set at %f %f"%(x, y)) cube.linev.set_xdata((x,x)) cube.lineh.set_ydata((y,y)) cube.linev.set_visible(True) # Then also a cursor is drawn when crosshair is set on cube.lineh.set_visible(True) #flushprint("Ik ga lijnen tekenen in nieuwe kubus") cube.frame.draw_artist(cube.linev) cube.frame.draw_artist(cube.lineh) self.fig.canvas.blit(cube.frame.bbox) # But there is also something to do in its panels: currentindx = framenr offset = cube.movieframeoffset if cube.panelXframes: yp = currentindx - offset #flushprint("crosshair in xpanel at yp=%d"%yp) try: i = cube.panelXframes.index(yp) #self.fig.canvas.restore_region(cube.panelXback) cube.framep1.draw_artist(cube.annimp1.image.im) cube.panelxline.set_ydata((i,i)) #flushprint("crosshair in xpanel at index i=%d"%i) cube.panelxline.set_visible(True) cube.framep1.draw_artist(cube.panelxline) cube.panelxline2.set_xdata((x,x)) #flushprint("crosshair in xpanel at index i=%d"%i) cube.panelxline2.set_visible(True) cube.framep1.draw_artist(cube.panelxline2) self.fig.canvas.blit(cube.framep1.bbox) #xlim = framep1.get_xlim() #ylim = framep1.get_ylim() #flushprint("Set horizontal line for cube at y=%d, bbox=%s %s"%(i, str(xlim), str(ylim))) #1 = framep1.plot(xlim, (i,i)) #framep1.draw_artist(L1[0]) #framep1.figure.canvas.blit(framep1.bbox) except: pass if cube.panelYframes: xp = currentindx - offset try: i = cube.panelYframes.index(xp) #self.fig.canvas.restore_region(cube.panelYback) cube.framep2.draw_artist(cube.annimp2.image.im) cube.panelyline.set_xdata((i,i)) #flushprint("crosshair in ypanel at index i=%d"%i) cube.panelyline.set_visible(True) cube.framep2.draw_artist(cube.panelyline) cube.panelyline2.set_ydata((y,y)) #flushprint("crosshair in ypanel at index i=%d"%i) cube.panelyline2.set_visible(True) cube.framep2.draw_artist(cube.panelyline2) self.fig.canvas.blit(cube.framep2.bbox) #ylim = framep2.get_ylim() #flushprint("Set vertical line for cube at x=%d %s"%(i, str(ylim))) #framep2.plot((i,i), ylim) except: pass def cleanupall(self): #------------------------------------------------------------------------- """ Cleanup the contents of this object (destructor). We need a separate function to cleanup because our class contains circular references so it will not be touched by Python's garbage collector. Implementing a __del__ method also does not work because that decreases the reference counter with one, but it was bigger than 1 to begin with. See also: http://www.electricmonk.nl/log/2008/07/07/python-destructor-and-garbage-collection-notes/ """ #------------------------------------------------------------------------- cidkey= self.movieimages.cidkey if cidkey: self.canvas.mpl_disconnect(cidkey) # Prevent problems while loading cidscroll = self.movieimages.cidscroll if cidscroll: self.canvas.mpl_disconnect(cidscroll) # Prevent problems while loading
[docs] def append(self, frame, fitsobj, axnums, slicepos=[], pxlim=None, pylim=None, vmin=None, vmax=None, hasgraticule=False, gridmode=False, hascolbar=True, pixelaspectratio=None, clipmode=0, clipmn=(4,5), callbackslist={}): #------------------------------------------------------------------------- """ Add a new cube to the container with cubes. Let's introduce some definitions first: 1) A **data cube** is a FITS file with more than two data axes. Examples are a sequence of channel maps in a radio cube where the spatial maps are a function of the observing freequency. 2) **Image axes** are the two axes of a map we want to display Usually these maps have two spatial axes (e.g. R.A. and Dec.), but this class can also handle images which are slices with axes of mixed types (e.g. R.A. and velocity, a so called position velocity diagram). 3) **Repeat axes** are all the axes that do not belong to the set of image axes. Along these repeat axes, one can define a sequence of images, for example a set of R.A., Dec. images as function of velocity or Stokes parameter. :param frame: Each series of movie images that is appended needs its own frame. Frames can be defined with either Matplotlib's figure.add_subplot() or figure.add_axes(). Note that any frame is always resized to fit the canvas (with some room for a color bar and using the aspect ratio of the pixels in the current image). .. only:: latex To prevent that Matplotlib thinks that two frames are the same, you need to give each frame a unique label. .. only:: html To prevent that Matplotlib thinks that two frames are the same, you need to give each frame a unique label:: frame1 = fig.add_subplot(1,1,1, label='label1', frameon=False) cube = myCubes.append(frame1, ....) frame2 = fig.add_subplot(1,1,1, label='label2', frameon=False) cube = myCubes.append(frame2, ....) :type frame: Matplotlib Axes object. :param fitsobj: A FITSimage object as in: ``fitsobject = maputils.FITSimage('ngc6946.fits')``. :type fitsobj: Instance of :class:`maputils.FITSimage` :param axnums: A sequence (tuple or list) with axis numbers. The numbers start with 1. For an image you need to specify two axes. The numbers can be in any order. So, for instance, you can swap R.A. and Dec. axes, or you can make slices with one spatial and a spectral axis. For a cube with CTYPE's (RA,DEC,FREQ) you should enter then ``axnums=(1,3)`` or ``axnums=(3,2)`` etc. .. only:: html If we want RA, FREQ images for all DEC slices, we should write:: fitsobject = maputils.FITSimage('ngc6946.fits') n1, n2, n3 = [fitsobject.axisinfo[i].axlen for i in (1,2,3)] print("Range axis 1:", 1, n1) print("Range axis 2:", 1, n2) print("Range axis 3:", 1, n3) # Note that slice positions follow FITS standard, i.e. first pixel is 1 slicepos = list(range(1, n2+1)) # all DEC slices cube = myCubes.append(frame1, fitsobject, axnums=(1,3), slicepos=slicepos) :type axnums: Tuple or list with integers :param slicepos: A sequence with integers which set the slices that are stored as images. If we call axes outside an image *repeat axes*, then the slice positions are pixel positions (following the FITS syntax, so they start with 1 and end with NAXISn (e.g. NAXIS3 from the header). By default, the value of ``slicepos`` is None. This value is interpreted as follows: 1) If a data structure is 2-dimensional, ``slicepos`` is not used 2) If a data structure has n repeat axes, then for each repeat axis, the value of CRPIX of the corresponding axis is used, unless the this value is negative or greater than the axis length. In that case it is set to 1. Note that ``slicepos=None`` always defines only one image!. .. only:: latex Slice positions can be given in any order. (See example in HTML-version of this documentation) .. only:: html Slice positions can be given in any order:: fitsobject = maputils.FITSimage('ngc6946.fits') naxis3 = fitsobject.hdr['NAXIS3'] slicepos = list(range(naxis3, 0, -1)) cube = myCubes.append(frame1, fitsobject, axnums=(1,2), slicepos=slicepos) Assume you have two repeat axes. One has CTYPE *VELO* and the other has CTYPE *STOKES*. Then we need to define a list ``slicepos`` with elements that are tuples with two integers. The first for the VELO axis and the second for the STOKES axis, e.g.: ``slicepos=[(1,1), (1,2), (1,3), ... ]`` .. only:: latex (See example in HTML-version of this documentation) .. only:: html In the lines below, we load images from a set with axes (RA, DEC, VELO, STOKES). We want RA, DEC as image axes (``axnums=(1,2)``) and VELO and STOKES as the repeat axes. Assume we want a movie loop where we want to loop over all STOKES slices for each slice on the VELO axis. One can define the slice positions as follows:: fitsobject3 = maputils.FITSimage('aurora.fits') n3 = fitsobject3.axisinfo[3].axlen n4 = fitsobject3.axisinfo[4].axlen a = range(1,n3+1) b = range(1,n4+1) A = [x for x in a for i in range(n4)] B = b*n3 slicepos = list(zip(A, B)) cube = myCubes.append(frame3, fitsobject3, axnums=(1,2), slicepos=slicepos) If we want to see all the VELO slices per STOKES slice, then use:: A = a*n4 B = [x for x in b for i in range(n3)] slicepos = list(zip(A, B)) :type slicepos: None or a list, tuple or NumPy array with integers :param pxlim: Two values which set the range on the X axis. The numbers follow the FITS standard, i.e. the first pixel is 1 and the last pixel is given by header item NAXISn. :type pxlim: Tuple with two integers :param pylim: Two values which set the range on the Y axis. The numbers follow the FITS standard, i.e. the first pixel is 1 and the last pixel is given by header item NAXISn. .. only:: latex (See example in HTML-version of this documentation) .. only:: html Example:: fitsobject = maputils.FITSimage('ngc6946.fits') n1, n2, n3 = [fitsobject.axisinfo[i].axlen for i in (1,2,3)] print("Range axis 1:", 1, n1) print("Range axis 2:", 1, n2) print("Range axis 3:", 1, n3) pxlim = (5, n1-5) pylim = (5, n2-5) cube = myCubes.append(frame1, fitsobject, axnum(1,2), slicepos=list(range(1, n3+1)), pxlim=pxlim, pylim=pylim) :type pylim: Tuple with two integers :param vmin: Colors are assigned to pixels in a range between vmin and vmax. The values are pixel values in units given by BUNIT in the header of the data set. By default, the value of *vmin* is set to None. After loading all images, the value of *vmin* will be set to the minumum value of all the loaded data. One can use *vmin* to enhance features in the data. Note that the distribution of colors can be so unbalanced that nothing can be seen in the data (e.g. with one pixel which has a much higher value than all the others). In this case, one can press key ``h`` to get histogram equalization. :type vmin: Floating point number :param vmax: Colors are assigned to pixels in a range between *vmin* and *vmax*. The values are pixel values in units given by BUNIT in the header of the data set. By default, the value of *vmax* is set to None. After loading all images, the value of *vmax* will be set to the minimum value of all the loaded data. One can use *vmax* to visually extract features in the data. Note that the distribution of colors can be so unbalanced that almost no details can be seen (e.g. for data with one pixel which has a much higher value than all the others). In this case, one can press key ``h`` to get histogram equalization. .. only:: latex (See example in HTML-version of this documentation) .. only:: html Example:: fitsobject = maputils.FITSimage('ngc6946.fits') vmin, vmax = fitsobject.get_dataminmax() n3 = fitsobject.axisinfo[3].axlen slicepos = list(range(1, n3+1)) cube = myCubes.append(frame1, fitsobject, axnums=(1,2), slicepos=slicepos, vmin=1.1*vmin, vmax=0.9*vmax) :type vmax: Floating point number :param hasgraticule: If set to True, a system of grid lines will be plotted. The grid lines represent the world coordinate system. The system of grid lines is also called a *graticule*. :type hasgraticule: Boolean :param gridmode: If set to True, positions are no longer given in the pixel system of the FITS standard, but are given in *grids*. In a grid system we call the pixel that corresponds to the projection center, pixel 0. In this system you don't need to know the header values of CRPIXn to find the projection center in a map. :type gridmode: Boolean :param hascolbar: If set to True (is set by default), a color bar is plotted at the left of the images. The color bar has a fixed with and its height is always the same as the height of the canvas. The color bar can also be plotted with a separate method. :type hascolbar: Boolean :param pixelaspectratio: Set the ratio between the height of a pixel and its width. For images with spatial axes, the default should be fine. For other images, the aspect ratio is adjust so that a square image will be plotted. In this situation it is often convenient to be able to change the size of a plot with the pixel aspect ratio. :type pixelaspectratio: Floating point :param clipmode: * ``clipmode=0`` (default): distribute all available image colors equally between *vmin* and *vmax*. If these values are not specified, then calculate *vmin* and *vmax* from the minimum and maximum data value (of all the loaded images in the current cube). * ``clipmode=1``: Calculate clip values from the minimum and maximum data value (of all the loaded images in the current cube) unless values are entered in *vmin* or *vmax* which overrule the calculated values. * ``clipmode=2``: First calculate the mean and standard deviation (std) of all the data (of all the loaded images in the current cube). Then calculate ``vmin=mean-clipmn[0]*std`` and ``vmax=mean+clipmn[1]*std`` and use these values unless a preset value for either *vmin* or *vmax* was found. For *clipmn*, see next parameter. .. only:: latex (See example in HTML-version of this documentation) .. only:: html The next example illustrates this clip mode:: fig = figure(figsize=(10,10)) myCubes = maputils.Cubes(fig, toolbarinfo=True, printload=False, helptext=False, imageinfo=True) # Create a maputils FITS object from a FITS file on disk fitsobject = maputils.FITSimage('m101.fits') frame1 = fig.add_subplot(1,1,1, label='label1', frameon=False) cube = myCubes.append(frame1, fitsobject, axnums=(1,2), slicepos=None, clipmode=2, clipmn=(4,4), hascolbar=False) show() :type clipmode: Integer :param clipmn: Set the scale factors for the standard deviation as described at the previous parameter *clipmode*. By default, the value is set to (4,5) :type clipmn: Tuple with two floats :param callbackslist: To be able to interfere or interact with the cube viewer we need to know the status of processes in Maputils, like when is the color mapping in an image has been changed. The events handlers that we define with this method are specific to one cube. To handle these events, one must connect it to a function or an object with methods. With method :meth:`Cubes.append` we can handle the following events: * 'waitcursor' -- The process of loading images starts and one could change the cursor to another shape to indicate the loading process * 'resetcursor' -- The process of loading images has been finished and one could change the cursor to its original shape * 'exmes' -- This event is triggered when the string with information about the cursor position is send to the canvas or to an external function. * 'cubeloaded' -- This event is triggerd after all images of the current cube (for which you apply this append method) are loaded. It differs from the 'finished' event because that event is triggered after loading **all** the cubes in a queue of cubes. Some triggers are included to report changes in the color mapping of an image: * 'blankcol' -- Must be handled by a function with one parameter. It returns the index value of the new color that represents blank pixels in the image. .. only:: latex The index can be used to retrieve the Matplotlib short and long name of the color. (Example in the HTML-version of this documentation) .. only:: html The index can be used to retrieve the Matplotlib short and long name of the color as in:: print(maputils.Annotatedimage.blankcols[bcol]) print(maputils.Annotatedimage.blanknames[bcol]) * 'blurfac' -- User pressed *x* on the keyboard to change the smoothing factor The callback function must have at least one parameter. The first parameter stores the smoothing (blur) factor in pixels (floating point number). * 'blurred' -- User pressed *z* on the keyboard to toggle between smoothed and original image The callback function must have at least one parameter. The first parameter stores the current status (Boolean) * 'savelut' -- User pressed *m* on the keyboard to write the current color lookup table to file on disk. The callback function must have at least one parameter. The first parameter stores the file name (string). * 'histeq' -- A user pressed key *h* on the keyboard and requested a histogram equalization (or a reset of a previous equalization). The trigger must be handled by a function with one parameter and this parameter is returned as True or False. * 'inverse' -- A user changed the current color map to its inverse. The trigger must be handled by a function with one parameter and this parameter is returned as True or False. * 'lut' -- A user changed the color lookup table. These so called luts are stored in :data:`maputils.cmlist`. The trigger must be handled by a function with one parameter and this parameter is returned as a index for the list with color maps. * 'offset' -- A user changed the relation between pixel values and color mapping. The trigger must be handled by a function with one parameter and this parameter is returned as the current offset which is a floating point value between -0.5 and 0.5 * 'scale' -- A user changed the color map scaling 1='linear', 2='log', 3='exp', 4='sqrt', 5='square' Key '9' triggers the 'inverse' callback. Key '0' triggers all other callbacks in this list. The trigger must be handled by a function with (at least) one parameter (i.e. the first) and this parameter is returned as a index for the list with scales :attr:`maputils.Annotatedimage.lutscales`. * 'slope' -- A user changed the relation between pixel values and color mapping. The trigger must be handled by a function with one parameter and this parameter is returned as the current slope which is a floating point value between 0 and 89 degrees :type callbackslist: A dictionary with functions that will be executed after the loading of the images has been completed (for the current cube) :Examples: 1) Simple example with a 3D data FITS file. The cube has axes RA, DEC and VELO. We show how to initialize a Cubes container and how to add objects of type :class:`maputils.FITSimage` to this container:: from matplotlib.pyplot import figure, show from kapteyn import maputils fig = figure() frame = fig.add_subplot(1,1,1) myCubes = maputils.Cubes(fig, toolbarinfo=True, printload=True) fitsobject = maputils.FITSimage('ngc6946.fits') slicepos = list(range(1,101)) axnums = (1,2) myCubes.append(frame, fitsobject, axnums, slicepos=slicepos) show() 2) List with callbacks. In this example we demonstrate how triggers for callbacks can be used. The use of Lambda expressions is demonstrated for functions that need extra parameters. If you run this example you will soon discover how these callbacks work: .. code-block:: python from matplotlib.pyplot import figure, show from kapteyn import maputils def printSlope(slope, cubenr=0): # Right mouse button in horizontal direction print("Color mapping slope for cube %d = %f"%(cubenr, slope)) def printOffset(offs): # Right mouse button in vertical direction print("Shift in color mapping:", offs) def printLut(indx): # User pressed PgUp or PgDn key print("User changed lut to: ", maputils.Colmaplist.colormaps[indx]) def printBlankcol(bcol): # User pressed key 'b' print("Blank color set to: ", bcol, maputils.Annotatedimage.blankcols[bcol], maputils.Annotatedimage.blanknames[bcol]) def printHistog(hist): # User pressed key 'h' to toggle between a histogram equalized version # and the original print("Histogram equalized?", hist) def printScale(scale): # User pressed a number from the list [1,2,3,4,5] on the keyboard to set a scale print("Scale:", scale, maputils.Annotatedimage.lutscales[scale]) def printInverse(inv): # User pressed key '9' on the keyboard to get an inverse color mapping print("Inverse color mapping?", inv) def printBlurfac(blurfac): # User pressed 'x' on the keyboard to change the smoothing factor print("Smoothing factor:", blurfac) def writeLut(filename): # User pressed 'm' on the keyboard to write the current color lookup table to file print("Write lut to file:", filename) def printSmooth(smooth): # User pressed 'z' on the keyboard to toggle between smoothed and original image print("Image smoothed?", smooth) def externalMes(pos): print("Position:", pos) def externalMes2(pos): print("Position 2:", pos) def waitCursor(): print("Wait cursor on") def resetCursor(): print("Wait cursor off") def postLoading(splitfr, slicepos): print("Set frame to compare with to:", splitfr) myCubes.set_splitimagenr(splitfr) myCubes.set_panelframes(list(range(len(slicepos))), panel='XY') def printFinish(): print("Loading finished and user interaction has been started") fig = figure() myCubes = maputils.Cubes(fig, toolbarinfo=True, printload=False, callbackslist={'finished':printFinish} ) frame = fig.add_subplot(1,1,1, label='ngc6946') fitsobject = maputils.FITSimage('ngc6946.fits') splitfr = 30 slicepos = list(range(1,101)) myCubes.append(frame, fitsobject, axnums=(1,2), slicepos=slicepos, callbackslist={'slope' : lambda slope, cubenr=0 : printSlope(slope, cubenr), 'offset' : printOffset, 'lut' : printLut, 'scale' : printScale, 'inverse' : printInverse, 'blankcol' : printBlankcol, 'blurfac' : printBlurfac, 'blurred' : printSmooth, 'savelut' : writeLut, 'exmes' : externalMes, 'waitcursor' : waitCursor, 'resetcursor' : resetCursor, 'cubeloaded' : lambda: postLoading(splitfr, slicepos)}) frame = fig.add_subplot(1,1,1, label='m101') fitsobject2 = maputils.FITSimage('m101.fits') myCubes.append(frame, fitsobject2, axnums=(1,2), callbackslist={'slope' : lambda slope, cubenr=1 : printSlope(slope, cubenr), 'histeq' : printHistog, 'waitcursor' : waitCursor, 'exmes' : externalMes2 'resetcursor' : resetCursor}) show() """ #------------------------------------------------------------------------- C = Cubeatts(frame, fitsobj, axnums, slicepos, pxlim, pylim, vmin, vmax, hasgraticule, gridmode, hascolbar, pixelaspectratio, clipmode, clipmn, callbackslist) C.movieframeoffset = self.movieframeoffset # First cube has offset 0 C.cnr = self.cubeindx self.cubelist.append(C) self.numcubes += 1 # Prepare movie frame offset for the next cube. self.movieframeoffset += C.nummovieframes # We do not want the initial frame and default labeling on screen: frame.axis('off') # Before starting loading images in a timer callback, we disable # all registred callbacks in 'end_interactionMovieToPanels'. # In the loadimages() method, the callbacks are activated again if self.cubelist[0].panelscb is not None: self.end_interactionMovieToPanels() #if self.resizecb: # self.resizecb.deschedule() self.cubeindx += 1 # Prepare index for the next cube # Start loading the images. We do this with a timer callback so that # we don't have to wait for the last image before a window appears. #timer = TimeCallback(self.loadimages, 0.00001, False, count=0, cube=C, zprofileOn=self.zprofileOn) timer = TimeCallback(self.loadimages, 0.00001, False, count=0, cube=C, zprofileOn=self.zprofileOn) # The timer list keeps track of the timers. The first that is registered # is the first that will be processed entirely before the next callback is # scheduled. self.timerlist.append(timer) if len(self.timerlist) == 1: # There are no previous load sessions in progress. Start this one self.timerlist[0].schedule() C.previous_bbox = None return C
def cmapcallback(self): #------------------------------------------------------------------------- """ Module mplutil has an update() method which is triggered after a color map change. A callback can be added. This callback is executed at the end of the update() method. The callback is added as an attribute as in: cube.cmap.callback = self.cmapcallback. We need the callback to register when a background has been changed and a crosshair cursor needs to be redrawn (background is blitted). """ #------------------------------------------------------------------------- global backgroundchanged backgroundchanged = True #self.fig.canvas.flush_events() #flushprint("Color map has been changed (callback) !!!!!!!!! self=%d"%(id(self)))
[docs] def set_aspectratio(self, cube, aspectratio): #------------------------------------------------------------------------- """ Change aspect ratio of frame of 'cube'. :param cube: An entry from the list ``Cubes.cubelist``. For example: ``cube=myCubes.cubelist[0]`` :type cube: An object from class :class:`maputils.Cubeatts`. :param aspectratio: Set aspect ratio of pixels for the images in the cube ``cube``. :type aspectratio: Floating point number :Examples: Load a cube with images and set the pixel aspect ratio:: from matplotlib.pyplot import figure, show from kapteyn import maputils def postloading(): cube = myCubes.cubelist[0] print("Original aspect ratio current cube: %f"%(cube.pixelaspectratio)) myCubes.set_aspectratio(cube, 2.1) fig = figure() frame = fig.add_subplot(1,1,1) myCubes = maputils.Cubes(fig, toolbarinfo=True, helptext=False, printload=False) fitsobject = maputils.FITSimage('ngc6946.fits') slicepos = list(range(1,101)) myCubes.append(frame, fitsobject, axnums=(1,2), slicepos=slicepos, callbackslist={'cubeloaded':postloading}) show() :notes: Note for programmers: We need a call to method :meth:`Cubes.reposition` to adjust the graticule frames """ #------------------------------------------------------------------------- if aspectratio is None or aspectratio == 0.0: aspectratio = cube.fitsobj.get_pixelaspectratio() # Set default from header else: aspectratio = abs(aspectratio) cube.pixelaspectratio = abs(aspectratio) cube.frame.set_aspect(aspectratio) currentindx = cube.lastimagenr + cube.movieframeoffset #self.movieimages.indx if cube.hasgraticule: self.new_graticule(cube, visible=True) self.reposition(force=True) self.movieimages.setimage(currentindx, force_redraw=True)
def compose_movieframelist(self): #------------------------------------------------------------------------- """ Each cube can have a series of images set as images for the movie loop. This method composes a list for all cubes. """ #------------------------------------------------------------------------- newlist = [] for C in self.cubelist: newlist += [j + C.movieframeoffset for j in C.movieframelist] self.movieimages.set_movieframes(newlist) def set_movieframes(self, framelist): #------------------------------------------------------------------------- """ For INTERACTIVE use. Set the images that you want to include in a movieloop. The numbers are indices of the array with all the collected images in the moviecontainer. The index numbers are not related to a cube. """ #------------------------------------------------------------------------- self.movieimages.set_movieframes(framelist)
[docs] def set_panelframes(self, framelist, panel='X'): #------------------------------------------------------------------------- """ Set a list with movie frame index numbers to show up in one of the side panels. The panels appear when the list is not empty. Imagine a 3D data structure. The axes of such a structure could be RA, DEC and FREQ. When we appended images, we specified the axis numbers in *axnums*. Assume these numbers are (1,2) which corresponds to images with axes RA and DEC. We can show a movie loop of the images as function of positions in axis FREQ. A side panel is a new image that shares either the RA or the DEC axis with the cube, but the other axis is a FREQ axis. This way we extracted a slice from the cube. ``panel='X'`` gives a horizontal RA-FREQ slice and ``panel='Y'`` gives a vertical DEC-FREQ slice and the data is extracted at the position of the cursor in the current image and when you move the mouse while pressing the left mouse button, you will see that the side panels are updated with new data extracted from the new mouse cursor position. It is also possible use the mouse in the side panels. If you move the mouse in the FREQ direction, you will see that the current image is replaced by the one that matches the new slice position in FREQ. If you move in a side panel in the RA or DEC direction, then you will see that the opposite side panel is updated because the position where the slice was extracted (DEC or RA) has been changed. This functionality has been used often for the inspection of radio data because it allows you to discover features in the data in a simple and straightforward way. :param framelist: List with numbers which represent the movie frames from which data is extracted to compose a side panel with slice data. Movie frame numbers start with index 0 and range to the number of stored images. These images need not to be extracted from the same cube. Therefore there is no clear correspondence between slice positions and movie frame numbers. :type framelist: Iterable sequence with integers :param panel: Default is to draw a panel along the horizontal image axis (X axis). The panel along the Y axis needs to be specified with ``panel='Y'``. Both panels are plotted with: ``panel='XY'`` :type panel: One or two characters from the list 'X', 'Y', 'x', 'y' :Examples: Set both side panels to inspect orthogonal slices:: from matplotlib.pyplot import figure, show from kapteyn import maputils def postloading(): myCubes.set_panelframes(list(range(len(slicepos))), panel='XY') fig = figure() frame = fig.add_subplot(1,1,1) myCubes = maputils.Cubes(fig, toolbarinfo=True, printload=False) fitsobject = maputils.FITSimage('ngc6946.fits') slicepos = list(range(1,101)) myCubes.append(frame, fitsobject, axnums=(1,2), slicepos=slicepos, callbackslist={'cubeloaded':postloading}) show() """ #------------------------------------------------------------------------- self.numXpanels = self.numYpanels = 0 for C in self.cubelist: frlo = C.movieframeoffset; frhi = C.movieframeoffset + C.nummovieframes panelframes = [d-frlo for d in framelist if frlo <= d < frhi] if 'X' in panel.upper(): #flushprint("SET_XPANELS.................") C.set_panelXframes(panelframes) #self.create_panel('x', C) if 'Y' in panel.upper(): #flushprint("SET_YPANELS.................") C.set_panelYframes(panelframes) #self.create_panel('y', C) if C.panelXframes: # Just set by set_panel(X)Yframes self.numXpanels += 1 if C.panelYframes: # Just set by set_panel(X)Yframes self.numYpanels += 1 for C in self.cubelist: if 'X' in panel.upper(): # Call method even when there are no panelXframes. Then we can clean up. self.create_panel('x', C) if 'Y' in panel.upper(): self.create_panel('y', C)
def create_panel(self, panid, cube): #---------------------------------------------------------------------------- """ Plot (or clean up) one of the panels or both. This method is invoked either at the start of the program after loading all images, or when there are triggers for the side panels. """ #---------------------------------------------------------------------------- panx = pany = False if panid == 'x': panx = True elif panid == 'y': pany = True # The request to update the elif panid == 'r': # A redo request # TODO: nog nodig?? pany = True panx = True else: return #flushprint("panx=%s pany=%s"%(str(panx), str(pany))) cubeindx = cube.cnr xold = cube.xold yold = cube.yold frame = cube.frame fig = frame.figure framep1 = cube.framep1 framep2 = cube.framep2 #flushprint("Frames in create_panel: fr p1 p2=%d %d %d"%(id(frame), id(framep1), id(framep2))) cmap = cube.cmap mplimp1 = cube.annimp1 mplimp2 = cube.annimp2 slicepos = cube.slicepos fitsobject = cube.fitsobj pxlim = cube.pxlim pylim = cube.pylim vmin = cube.vmin vmax = cube.vmax offset = cube.movieframeoffset # The horizontal panel has an vertical range that should directly # convert mouse positions into array indices. I.e. if pylim is # 1,3, the range in the plot is -0.5, 2.5. Only the exact position # 2.5 will be rounded to 3, which is not a valid array index for this # range. Therefore we lower the upper value with a small number to # avoid this problem. delta = 0.000001 # !!!!!!!!!!!!!!!!!!! # Dit gaat alleen werken als van te voren bekend is wat de inhoud is van # panelXframes en panelYframes in de eventfrompanel routines. j = 0 panelindx = j for C in self.cubelist: if C.cnr == cubeindx: panelindx = j #flushprint("panelindx=%d, C.cnr, cubeindx=%d %d"%(panelindx,C.cnr, cubeindx )) if (C.panelXframes or C.panelYframes): j += 1 cube.frame = frame # Store it as attribute after it got new values # Important note: In this method we add slice panels. These get an initial # box with dummy sizes. That is because the actual drawing is postponed # and the reposition method (usually triggered after resize events) # calculated the correct values for all the panels. # Equal boxes in Matplotlib are considered to belong to the same # Axes object (frame). We force a unique frame by giving # the Axes contructor a unique label. panbox = [0,0,0.01,0.01] # Dummy # If there is already a panel, then we need to remove its contents etc. if panx or pany: if panx and framep1: ##flushprint("Removing frame1") #if framep1: if mplimp1: mplimp1.disconnectCallbacks() mplimp1.regcb.deschedule() #flushprint("Remove panel1 callback met id=%d"%(id(mplimp1.regcb))) framep1.clear() fig.delaxes(framep1) if mplimp1.grat: fig.delaxes(mplimp1.grat.frame) fig.delaxes(mplimp1.grat.frame2) framep1 = cube.framep1 = None mplimp1 = None #if framep2: # pany = True if pany and framep2: #if framep2: #flushprint("Removing frame2") if mplimp2: mplimp2.disconnectCallbacks() mplimp2.regcb.deschedule() #flushprint("Remove panel2 callback met id=%d"%(id(mplimp2.regcb))) framep2.clear() fig.delaxes(framep2) if mplimp2.grat: fig.delaxes(mplimp2.grat.frame) fig.delaxes(mplimp2.grat.frame2) framep2 = cube.framep2 = None mplimp2 = None #if framep1: # panx = True #if not (cube.panelXframes or cube.panelYframes): # self.reposition(force=True) # return # Nothing to do xi = xold; yi = yold if None in [xi,yi]: # If the first time is a resize event, then xold xi = pxlim[0] # and yold are None. yi = pylim[0] cube.xold = xi cube.yold = yi # Create new frame. If a user did reset the panels with an empty list, # one must skip this part makeXline = False if panx and cube.panelXframes: label = 'x_'+str(id(cube)) # For a unique Axes object we need a unique label!! framep1 = fig.add_axes(panbox, sharex=frame, label=label) framep1.set_aspect('auto') framep1.set_adjustable('box') framep1.axis('off') M = cube.Mx ly = M.shape[0] j = 0 for indx in cube.panelXframes: # yi should be in range of pylim. To make it an array index, # subtract the limit pylim[0] I = self.movieimages.annimagelist[offset+indx] if I.origdata is not None: N = I.origdata else: N = I.data M[ly-j-1] = N[int(yi-pylim[0])] j += 1 mixpix = pylim[0] fo = cube.fitsobj wcstypes = [fo.wcstypes[cube.panelx_axperm[0]-1], fo.wcstypes[cube.panelx_axperm[1]-1]] #wcstypes = [fo.wcstypes[fo.axperm[0]-1], fo.wcstypes[fo.axperm[1]-1]] mplimp1 = Annotatedimage(framep1, header=fo.hdr, # Invert the limits in y # because the image is also build # with highest y below pxlim=pxlim, pylim=[ly-1-delta,0], #[ly-1-delta,0], imdata=M, projection=cube.panelx_proj, #fo.convproj, axperm=cube.panelx_axperm, #fo.axperm wcstypes=wcstypes, skyout=fo.skyout, spectrans=fo.spectrans, alter=fo.alter, mixpix=fo.mixpix, aspect='auto', slicepos=mixpix, #fo.slicepos, sliceaxnames=fo.sliceaxnames, sourcename=fo.filename, cmap=cmap, adjustable='box', clipmin=vmin, clipmax=vmax) #framep1.set_xlim(pxlim) #framep1.set_ylim([ly-1-delta,0]) posobj1 = Positionmessage(cube.panelx_proj.skysys, fo.skyout, cube.panelx_proj.types) # Add a callback for this panel. A move with button 1 pressed, will change # the cube image to the one where the index corresponds to the (mouse) position mplimp1.regcb = AxesCallback(self.eventfrompanel1, framep1, 'motion_notify_event', cubenr=cubeindx, posobj=posobj1) #flushprint("Add motion notify for panel 1 callback %d for object %d"%(id(mplimp1.regcb), id(self))) mplimp1.AxesCallback_ids.append(mplimp1.regcb) mplimp1.regcbkey = AxesCallback(self.eventfrompanel1, framep1, 'key_press_event', cubenr=cubeindx, posobj=posobj1) mplimp1.AxesCallback_ids.append(mplimp1.regcbkey) #mplimp1.data = bo dat mplimp1.Image(animated=False) #mplimp1.Graticule()#pxlim=pxlim, pylim=[ly-1-delta,0]) #mplimp1.Image() mplimp1.plot() #mplimp1.grat = None im = cube.imagesinthiscube[0] if im.contours_on: # Some trickery. First we do not store the contours for panels as we do with images. # This is because we do have only one panel image for which we change the # data when we change images in the main panel. So we calculate contours on the fly. # Note that we hace to remove contours from CS.collections because otherwise # the contours from a previous setting will re-appear when we replot a panel. # The image data is retrieved with method get_array(). We need the extent keyword # for a correct overlay on the panel image. The collection is stored in # attribute contourSet cmap, colors, norm, levels = (im.contours_cmap, im.contours_colour, im.norm, im.clevels) # There must be either a cmap or a color specification in contour() if cmap is None and colors is None: colors = 'r' if not (cmap is None): cmap = cm.get_cmap(cmap) colors = None mplimp1.contourSet = framep1.contour(mplimp1.image.im.get_array(), levels=levels, extent=mplimp1.box, cmap=cmap, norm=norm, colors=colors) cube.annimp1 = mplimp1 cube.framep1 = framep1 if cube.panelxline: del cube.panelxline del cube.panelxline2 lineprops = {'animated': False,'linewidth':1} #cube.panelxline = framep1.axhline(framep1.get_ybound()[0], visible=False, **lineprops) cube.panelxline = framep1.axhline(xmin=MINBOUND, xmax=MAXBOUND, visible=False, **lineprops) cube.panelxline2 = framep1.axvline(ymin=ly-1-delta, ymax=0, visible=False, **lineprops) makeYline = False if pany and cube.panelYframes: label = 'y_'+str(id(cube)) framep2 = fig.add_axes(panbox, sharey=frame, label=label) framep2.set_aspect('auto') framep2.set_adjustable('box') framep2.axis('off') M = cube.My lx = M.shape[1] j = 0 for indx in cube.panelYframes: I = self.movieimages.annimagelist[offset+indx] if I.origdata is not None: N = I.origdata else: N = I.data M[:,j] = N[:,int(xi-pxlim[0])] # self.movieimages.annimagelist[offset+indx].data[:,xi-pxlim[0]] j += 1 fo = cube.fitsobj mixpix = pxlim[0] wcstypes = [fo.wcstypes[cube.panely_axperm[0]-1], fo.wcstypes[cube.panely_axperm[1]-1]] mplimp2 = Annotatedimage(framep2, header=fo.hdr, pxlim=[0,lx-1-delta], pylim=pylim, imdata=M, projection=cube.panely_proj, axperm=cube.panely_axperm, #fo.axperm, wcstypes=wcstypes, skyout=fo.skyout, spectrans=fo.spectrans, alter=fo.alter, mixpix=mixpix, aspect='auto', slicepos=fo.slicepos, sliceaxnames=fo.sliceaxnames, sourcename=fo.filename, cmap=cmap, adjustable='box', clipmin=vmin, clipmax=vmax) #framep2.set_xlim([0,lx-1-delta]) #framep2.set_ylim(pylim) posobj2 = Positionmessage(cube.panely_proj.skysys, fo.skyout, cube.panely_proj.types) mplimp2.regcb = AxesCallback(self.eventfrompanel2, framep2, 'motion_notify_event', cubenr=cubeindx, posobj=posobj2) mplimp2.AxesCallback_ids.append(mplimp2.regcb) #flushprint("Add motion notify for panel 1 callback %d for object %d"%(id(mplimp2.regcb), id(self))) #mplimp2.data = boxdat mplimp2.regcbkey = AxesCallback(self.eventfrompanel2, framep2, 'key_press_event', cubenr=cubeindx, posobj=posobj2) mplimp2.AxesCallback_ids.append(mplimp2.regcbkey) mplimp2.Image(animated=False) mplimp2.plot() im = cube.imagesinthiscube[0] if im.contours_on: # Some trickery. First we do not store the contours for panels as we do with images. # This is because we do have only one panel image for which we change the # data when we change images in the main panel. So we calculate contours on the fly. # Note that we hace to remove contours from CS.collections because otherwise # the contours from a previous setting will re-appear when we replot a panel. # The image data is retrieved with method get_array(). We need the extent keyword # for a correct overlay on the panel image. The collection is stored in # attribute contourSet cmap, colors, norm, levels = (im.contours_cmap, im.contours_colour, im.norm, im.clevels) # There must be either a cmap or a color specification in contour() if cmap is None and colors is None: colors = 'r' if not (cmap is None): cmap = cm.get_cmap(cmap) colors = None mplimp2.contourSet = framep2.contour(mplimp2.image.im.get_array(), levels=levels, extent=mplimp2.box, cmap=cmap, norm=norm, colors=colors) #mplimp2.contourSet = framep2.contour(mplimp2.image.im.get_array(), levels=None, colors='c', extent=mplimp2.box) # Properties #flushprint("Panel contours after creating image") #for col in CS.collections: # col.set_visible(True) # mplimp2.frame.draw_artist(col) """ if not mplimp2.contourSet or mplimp2.contours_changed: if mplimp2.contours_cmap: cs = mplimp2.Contours(mplimp2.clevels, cmap=cm.get_cmap(mplimp2.contours_cmap)) else: # Not a color map. Is it a plain colour then? if not mplimp2.contours_colour: mplimp2.contours_colour = 'r' # Then set a default (red) #cs = mplimp2.Contours(mplimp2.clevels, colors=mplimp2.contours_colour) cs = mplimp2.Contours(None, colors=mplimp2.contours_colour) cs.plot(mplimp2.frame) mplimp2.contourSet = cs.CS mplimp2.contours_changed = False """ #mplimp2.Contours() """ grat = mplimp2.Graticule(offsety=True, skipx=True) grat.set_tickmode(mode="na") grat.setp_gratline(wcsaxis=1, visible=False) grat.setp_axislabel(plotaxis=("left"), visible=False) grat.setp_axislabel(plotaxis=("bottom"), visible=False) grat.setp_axislabel(plotaxis=("right"), visible=True) grat.setp_ticklabel(plotaxis=("right"), visible=True) grat.setp_ticklabel(plotaxis=("bottom"), visible=False) grat.setp_ticklabel(plotaxis=("left"), visible=False) grat.setp_tickmark(plotaxis=("bottom"), visible=False) grat.setp_tickmark(plotaxis=("left"), visible=False) grat.setp_tickmark(plotaxis=("right"), visible=True) mplimp2.plot() b = mplimp2.box xb = (b[0], b[1], b[1], b[0], b[0]) yb = (b[2], b[2], b[3], b[3], b[2]) grat.frame2.plot(xb, yb, lw=1, c='k', alpha=0.5) mplimp2.linepieces = set() mplimp2.linepieces.update(grat.frame2.findobj(Line2D)) mplimp2.grat = grat # mplimp2.interact_toolbarinfo() else: """ cube.annimp2 = mplimp2 mplimp2.grat = None cube.framep2 = framep2 if cube.panelyline: del cube.panelyline del cube.panelyline2 lineprops = {'animated': False,'linewidth':1} #cube.panelyline = framep2.axvline(framep1.get_xbound()[0], visible=False, **lineprops) cube.panelyline = framep2.axvline(ymin=MINBOUND, ymax=MAXBOUND, visible=False, **lineprops) cube.panelyline2 = framep2.axhline(xmin=0, xmax=lx-1-delta, visible=False, **lineprops) #flushprint("Frames net Na create_panel: fr p1 p2=%d %d %d"%(id(frame), id(cube.framep1), id(cube.framep2))) # Instead of a draw, we trigger a re-position so that all frames # will be adjusted to the new image frame. This saves an extra # call to canvas.draw(). self.reposition(force=True) # If there are requests to draw graticule lines in side panels # then this must processed in the reposition() mode because otherwise # its frame will have the wrong size. """ grat = mplimp1.Graticule(offsetx=True, skipx=True) #, pxlim=pxlim, pylim=[ly-1-delta,0]) mplimp1.grat = grat grat.plot(cube.framep1) """ def updatemovieframe(self, cb, frompanel1=False, frompanel2=False): #------------------------------------------------------------------------- """ This method changes an image in the movie container. It is called after a mouse move in one of the slice panels. That's why either 'frompanel1' or 'frompanel2' must be true. But this mouse move can also imply a move in a (e.g. spatial) direction in a panel comparable to a move in the main window. This updates the other panel(s). """ #------------------------------------------------------------------------- cbx = cb.xdata; cby = cb.ydata # Temp. store current mouse position cubenr = cb.cubenr currentcube = self.cubelist[cubenr] currentindx = self.movieimages.indx subsetnr = currentindx #s = "Currentindx=%d"%currentindx #flushprint(s) yi = int(numpy.round(cby)) if cb.event.name == 'key_press_event': k = cb.event.key.upper() #flushprint(k) #s = "xold=%f"%(currentcube.yold) #flushprint(s) skip = len("SHIFT+") arrow = k[skip:] if frompanel1: if 'UP' in arrow: cbx = currentcube.xold yi = subsetnr - 1 cby = yi elif 'DOWN' in arrow: cbx = currentcube.xold yi = subsetnr + 1 cby = yi elif 'RIGHT' in arrow: cbx = currentcube.xold + 1 cby = subsetnr yi = cby elif 'LEFT' in arrow: cbx = currentcube.xold - 1 cby = subsetnr yi = cby elif frompanel2: if 'UP' in arrow: xi = subsetnr cbx = xi cby = currentcube.yold + 1 elif 'DOWN' in arrow: xi = subsetnr cbx = xi cby = currentcube.yold - 1 elif 'RIGHT' in arrow: xi = subsetnr + 1 cbx = xi cby = currentcube.yold elif 'LEFT' in arrow: xi = subsetnr - 1 cbx = xi cby = currentcube.yold #s = "yi=%f"%(yi) #flushprint(s) if frompanel1: # yi = int(numpy.round(cby)) #flushprint(str(cb.event.name)) framenr = currentcube.panelXframes[yi] + currentcube.movieframeoffset if framenr >= 0 and framenr < self.movieimages.numimages and\ framenr != self.movieimages.indx: #flushprint("MAPUTILS setimage in updatemovieframe") self.movieimages.setimage(framenr) # After we set the right movie image, we also have to update the # opposite panel, because, for example, a move to the right # in the x panel (along x axis) will change the slice along the y axis. self.updatepanels(None, frompanel1, frompanel2, cubenr=cubenr, xpos=cbx, ypos=cby) if frompanel2: xi = int(numpy.round(cbx)) framenr = currentcube.panelYframes[xi] + currentcube.movieframeoffset if framenr >= 0 and framenr < self.movieimages.numimages and\ framenr != self.movieimages.indx: #flushprint("MAPUTILS setimage in updatemovieframe") self.movieimages.setimage(framenr) self.updatepanels(None, frompanel1, frompanel2, cubenr=cubenr, xpos=cbx, ypos=cby) # Are there any other panels (from other cubes) to update? if frompanel1 or frompanel2: xd, yd = currentcube.frame.transData.transform((cbx,cby)) for i in range(0, self.numcubes): if i != cubenr: # We need only an updated x or an updated y, not both # Method updatepanels() deals with this fact. xpos, ypos = self.cubelist[i].frame.transData.inverted().transform((xd,yd)) cb.cubenr = i self.updatepanels(None, frompanel1, frompanel2, cubenr=i, xpos=xpos, ypos=ypos) cb.cubenr = cubenr def eventfrompanel1(self, cb): #------------------------------------------------------------------ # Change the movie frame. The new frame number is # derived from the x position in the vertical panel (usually along # the latitude of the image). The function is registered as a # callback in the updatepanels() method. # The callback object 'cb' should have an attribute 'cubenr' # to identify the current cube to find the offset in the list # with movie frames. #------------------------------------------------------------------ # Try to compose a message with position information. # We know the value of y. This is an index. yi = int(numpy.round(cb.ydata)) # Each cube has a so called panelXframes and panelXframes list. # This is a list with all the # user supplied frame index numbers that are part of the movie. The default # is all movie frames in a cube, but a user could have changed this. # To find the index of the corresponding image we need to add the # offset of movieframes for the current cube. cubenr = cb.cubenr currentcube = self.cubelist[cubenr] if yi < 0 or yi >= len(currentcube.panelXframes): return framenr = currentcube.panelXframes[yi] + currentcube.movieframeoffset #print("yi, panelXframes[yi[, offset,framenr", yi, currentcube.panelXframes[yi], currentcube.movieframeoffset,framenr) currentim = self.movieimages.annimagelist[framenr] # The mouse is outside the current image. This is a trigger to # reset the last mouse position oim = self.movieimages.annimagelist[self.movieimages.indx] oim.X_lastvisited = oim.Y_lastvisited = None if self.toolbarinfo: yinfo = currentim.slicemessage ypos = currentcube.pylim[0] # As a dummy xw, yw, missingspatial = currentim.toworld(cb.xdata, ypos, matchspatial=True) sl = cb.posobj.wcs2str(xw, yw, missingspatial, returnlist=True) s = sl[0] + ' ' + yinfo currentim.messenger(s) if cb.event.name == 'motion_notify_event' and cb.event.button == 1: self.updatemovieframe(cb, frompanel1=True) if cb.event.name == 'key_press_event' and cb.event.key.upper().startswith('SHIFT+'): self.updatemovieframe(cb, frompanel1=True) def eventfrompanel2(self, cb): #------------------------------------------------------------------ # Update the movie with a new frame. The new frame number is # derived from the y position in the horizontal panel (usually along # the longitude). The function is registered as a callback. # The callback object 'cb' should have an attribure 'cubenr' # to identify the current cube to find the offset in the list # with movie frames. #------------------------------------------------------------------ xi = int(numpy.round(cb.xdata)) # Each cube has a so called 'framesindxlist'. This is a list with all the # user supplied frame index numbers that are part of the movie. The default # is all movie frames in a cube, but a user could have changed this. # To find the index of the corresponding image we need to add the # offset of movieframes for the current cube. cubenr = cb.cubenr currentcube = self.cubelist[cubenr] # TODO: documenteer dit. Voorheen kon je in het witte gebied na pannen niet # navigeren want xi was buiten range. if xi < 0 or xi >= len(currentcube.panelYframes): return #framenr = currentcube.framesindxlist[xi] + currentcube.movieframeoffset framenr = currentcube.panelYframes[xi] + currentcube.movieframeoffset currentim = self.movieimages.annimagelist[framenr] # The mouse is outside the current image. This is a trigger to # reset the last mouse position oim = self.movieimages.annimagelist[self.movieimages.indx] oim.X_lastvisited = oim.Y_lastvisited = None if self.toolbarinfo: xinfo = currentim.slicemessage xpos = currentcube.pxlim[0] # As a dummy xw, yw, missingspatial = currentim.toworld(xpos, cb.ydata, matchspatial=True) sl = cb.posobj.wcs2str(xw, yw, missingspatial, returnlist=True) s = sl[1] + ' ' + xinfo currentim.messenger(s) if cb.event.name == 'motion_notify_event' and cb.event.button == 1: self.updatemovieframe(cb, frompanel2=True) if cb.event.name == 'key_press_event' and cb.event.key.upper().startswith('SHIFT+'): self.updatemovieframe(cb, frompanel2=True) def getpanelboxes(self, frame, numXpanels, numYpanels, numXframesList, numYframesList, Xpanelindx=None, Ypanelindx=None): #----------------------------------------------------------------------------- """ This method calculates the positions of the side panels. It locates these panels to the bottom and right of an existing frame. The images are anchored to the top left corner and are made smaller if there are side panels. The space that is available to the panels is divided in a way that pixels from different panels are equal in size. So the ratios of frames between panels sets the width or height.The width of the gaps between panels is fixed in pixels. Note that numXframesList/numYframesList set the number of pixels in a panel for each panel. We need this list because for each panel we need the positions of the previous panels. """ #----------------------------------------------------------------------------- # We want gaps between panels of a fixed number of pixels. So transform this # number to figure coordinates fig = frame.figure xf0, yf0 = fig.transFigure.inverted().transform((0, 0)) xf1, yf1 = fig.transFigure.inverted().transform((4, 4)) dx = xf1 - xf0 dy = yf1 - yf0 # Get the box in figure coordinates. It seems that we need to apply # the aspect ratio first. frame.apply_aspect() # Get the current box in figure coordinates bbox = frame.get_position() xlo, ylo, xhi, yhi = bbox.xmin, bbox.ymin, bbox.xmax, bbox.ymax # Get the boundaries of all frames and calculate the min, max # of those in figure coordinates xmin, ymin, xmax, ymax = self.getbiggestbox() boxX = boxY = None # Is there something to do for this side? if Xpanelindx is not None and numXframesList is not None: # First box for x panel, i.e. connected to lower x axis totalXframes = 0 for nf in numXframesList: # Contains the number of frames per panel totalXframes += nf if not totalXframes: return boxX, boxY dys = (len(numXframesList)+1) * dy H = ymin - dys # Total height that is left bymin = []; bymax = [] for i, nf in enumerate(numXframesList): if i == 0: y = ymin - dy else: y = bymin[i-1] - dy bymax.append(y) h = H*float(nf)/totalXframes # If there is no space, then create some. This gives overlap, but is better than # plotting panel outside figure. if h <= 0.0: h = 0.1 bymin.append(y-h) bxmin = xlo; bxmax = xhi # Positions in x are fixed here w = bxmax - bxmin h = bymax[Xpanelindx] - bymin[Xpanelindx] boxX = (bxmin, bymin[Xpanelindx], w, h) if Ypanelindx is not None and numYframesList is not None: # Second box for y panel, i.e. connected to right y axis totalYframes = 0 for nf in numYframesList: totalYframes += nf if not totalYframes: return boxX, boxY dxs = (len(numYframesList)+1) * dx W = 1.0 - xmax - dxs # Total width that is left bxmin = []; bxmax = [] for i, nf in enumerate(numYframesList): if i == 0: x = xmax + dx else: x = bxmax[i-1] + dx bxmin.append(x) w = W*float(nf)/totalYframes if w <= 0.0: # If there is no space, then create some. This gives overlap w = 0.1 bxmax.append(x+w) bymin = ylo; bymax = yhi # Positions in y are fixed here h = bymax - bymin w = bxmax[Ypanelindx] - bxmin[Ypanelindx] boxY = (bxmin[Ypanelindx], bymin, w, h) #flushprint("In getpanelboxes boxX, boxY=%s %s"%(str(boxX), str(boxY))) return boxX, boxY
[docs] def set_crosshair(self, status): #---------------------------------------------------------------------------- """ This method prepares a crosshair cursor, which can be moved with the mouse while pressing the left mouse button. Usually we want a crosshair cursor to find the location of features. The crosshair cursor extends also to the side panels where the indicate at which slice we are looking. Note that the situation with blitted images and multiple frames (Axes objects) prevents the use of class:`matplolib.widgets.Cursor` so an alternative is implemented. :param status: If set to `True`, a crosshair cursor will be plotted. If `False`, an existing crosshair will be removed. :type status: Boolean :Examples: Draw a crosshair on the canvas which can be moved with the mouse while pressing the left mouse button. Extend the cursor in two side panels:: from matplotlib.pyplot import figure, show from kapteyn import maputils def postloading(): myCubes.set_panelframes(list(range(len(slicepos))), panel='XY') myCubes.set_crosshair(True) fig = figure() frame = fig.add_subplot(1,1,1) myCubes = maputils.Cubes(fig, toolbarinfo=True, printload=False) fitsobject = maputils.FITSimage('ngc6946.fits') slicepos = list(range(1,101)) myCubes.append(frame, fitsobject, axnums=(1,2), slicepos=slicepos, callbackslist={'cubeloaded':postloading}) show() """ #---------------------------------------------------------------------------- self.crosshair = status currentindx = self.movieimages.indx if self.crosshair: #flushprint("MAPUTILS setimage in set_crosshair") self.movieimages.setimage(currentindx, force_redraw=True) # In setimage() there is a callback to draw the crosshair else: currentimage = self.movieimages.annimagelist[currentindx] cnr = currentimage.cubenr cube = self.cubelist[cnr] #flushprint("Not crosshair-> remove it cube.backfround=%s"%(str(cube.background))) cube.linev.set_visible(False) cube.lineh.set_visible(False) if cube.panelXframes: cube.panelxline.set_visible(False) cube.framep1.draw_artist(cube.panelxline) cube.panelxline2.set_visible(False) cube.framep1.draw_artist(cube.panelxline2) if cube.panelYframes: cube.panelyline.set_visible(False) cube.framep2.draw_artist(cube.panelyline) cube.panelyline2.set_visible(False) cube.framep2.draw_artist(cube.panelyline2) self.fig.canvas.draw() # Redraw all to be sure to get rid of the cursor lines
[docs] def set_zprofile(self, status): #---------------------------------------------------------------------------- """ This method plots (or removes) a so called z-profile as a (non-filled) histogram as function of cursor position in an image. The data is extracted from a data set, with at least three axes, at the cursor position in the current image and this is repeated for all the images in all the cubes that were entered. This is also repeated for all the repeat axex that you specified for a given cube. So the X axis of the z-profile is the movie frame number and not a slice number. The z-profile is shown per cube. If you change to an image that belongs to another cube, the z-profile will change accordingly, but the X axis still represents the movie frame number. You can update the z-profile by moving the mouse while pressing the left mouse button. Change the limits in the Y direction (image values) with method :meth:`Cubes.zprofileLimits()`. :param status: Plot z-profile if ``status=True``. If there is already a z-profile plotted, then remove it with ``status=False`` :type status: Boolean :Examples: Load a cube and plot its z-profile. Set the range of image values for the z-profile:: from matplotlib.pyplot import figure, show from kapteyn import maputils def postloading(): myCubes.set_zprofile(True) myCubes.zprofileLimits(0, 0.0, 0.1) fig = figure() frame = fig.add_subplot(1,1,1) myCubes = maputils.Cubes(fig, toolbarinfo=True, printload=False) myCubes.zprofileheight = 0.3 # Change the height of the z-profile in normalized device coordinates fitsobject = maputils.FITSimage('ngc6946.fits') slicepos = list(range(1,101)) myCubes.append(frame, fitsobject, axnums=(1,2), slicepos=slicepos, callbackslist={'cubeloaded':postloading}) show() """ #---------------------------------------------------------------------------- self.zprofileOn = status # In resposition() the frames are recalculated with a new value for # the top. A top smaller than 1.0 creates space for a plot at the top. if self.zprofileOn: left = 0 #self.colbwidth width = 1.0 # 1.0 - left bottom = 1.0 - self.zprofileheight height = self.zprofileheight self.zprofileframe = self.fig.add_axes([left, bottom, width, height], label="zprofile", #frameon=False, # transparency needed autoscale_on=False, facecolor='#D2D2D2') #self.zprofileframe.set_axis_bgcolor("#D9D9D9") bbox=dict(edgecolor='w', facecolor='w', alpha=0.4, boxstyle="square,pad=0.1") ml = MultipleLocator(5) self.zprofileframe.xaxis.set_minor_locator(ml) for tick in self.zprofileframe.xaxis.get_major_ticks(): tick.set_pad(-10.0) tick.tick1line.set_color('r') #tick.tick2line.set_markeredgewidth(2) #tick.label1.set_bbox(bbox) for tick in self.zprofileframe.yaxis.get_major_ticks(): tick.set_pad(-10.0) tick.tick1line.set_color('r') tick.label1.set_horizontalalignment('left') #tick.label1.set_bbox(bbox) #self.zprofileframe.spines['bottom'].set_color('green') for t in self.zprofileframe.get_xticklabels(): # Must be 'plotted' before we can change properties t.set_fontsize(8) for t in self.zprofileframe.get_yticklabels(): # Must be 'plotted' before we can change properties t.set_fontsize(8) self.zprofileframe.grid(True) #for loc, spine in self.zprofileframe.spines.items(): #if loc in ['left','bottom']: #spine.set_position(('outward',-10)) # outward by 10 points_inside_poly self.zprofileframe.clipmin = None self.zprofileframe.clipmax = None self.zprofileframe.cubeindx = -1 self.zprofileframe.changed = False info = self.zprofileframe.text(0.99, 0.8, "Z-profile at: x,y=", horizontalalignment='right', verticalalignment='top', transform=self.zprofileframe.transAxes, fontsize=8, color='w', animated=False, family='monospace', bbox=dict(facecolor='red', alpha=0.5)) self.zprofileframe.XYinfo = info else: fr = self.zprofileframe if fr: fr.clear() self.fig.delaxes(fr) del fr self.zprofileframe = None self.zprofileXdata = None self.zprofileYdata = None self.reposition(force=True)
[docs] def set_colbar_on(self, cube, mode): #---------------------------------------------------------------------------- """ This method draws or removes a colorbar. Each cube is associated with a color bar. A color bar will always be plotted to the left side of your plot and it have the same height as the canvas. The color bar will change its colours if you move the mouse while pressing the right mouse button. It will also change if you use color navigation with keyboard keys: * pgUp and pgDown: browse through colour maps * Right mouse button: Change slope and offset * Colour scales: * 0=reset * 1=linear * 2=logarithmic * 3=exponential * 4=square-root * 5=square * 9=inverse * h: Toggle histogram equalization & raw image :param cube: An entry from the list ``Cubes.cubelist``. For example: ``cube=myCubes.cubelist[0]`` :type cube: An object from class :class:`maputils.Cubeatts`. :param mode: If set to ``True``, a color bar will be plotted. If set to ``False``, an existing color bar will be removed. :type mode: Boolean :Examples: Plot a color bar:: from matplotlib.pyplot import figure, show from kapteyn import maputils def postloading(): myCubes.colbwpixels = 100 # Change width of color bar in pixels cube = myCubes.cubelist[0] myCubes.set_colbar_on(cube, mode=True) fig = figure() frame = fig.add_subplot(1,1,1) myCubes = maputils.Cubes(fig, toolbarinfo=True, printload=False) fitsobject = maputils.FITSimage('ngc6946.fits') slicepos = list(range(1,101)) myCubes.append(frame, fitsobject, axnums=(1,2), slicepos=slicepos, hascolbar=False, callbackslist={'cubeloaded':postloading}) show() """ #---------------------------------------------------------------------------- if not cube or not len(cube.imagesinthiscube): return # Nothing to do cube.hascolbar = mode currentindx = cube.lastimagenr + cube.movieframeoffset # self.movieimages.indx for im in cube.imagesinthiscube: im.hascolbar = cube.hascolbar #flushprint("MAPUTILS setimage in set_colbar_on") self.movieimages.setimage(currentindx, force_redraw=True) # Redraw with/without color bar self.reposition() # Rescale
def get_colbar_position(self): #---------------------------------------------------------------------------- """ Purpose: Get position of the colorbar. This position can change if a line graph is added. It will be adjusted then in method reposition() """ #---------------------------------------------------------------------------- left = bottom = 0.0 width = self.colbwidth if self.zprofileOn: height = 1.0 - self.zprofileheight else: height = 1.0 return (left, bottom, width, height) def new_colorbar(self, cube): #---------------------------------------------------------------------------- """ Purpose: This method creates a frame for a new colorbar. """ #---------------------------------------------------------------------------- label = 'CB_' + str(id(cube)) # We need a unique label left = bottom = 0.0 width = self.colbwidth if self.zprofileOn: height = 1.0 - self.zprofileheight else: height = 1.0 cube.cbframe = self.fig.add_axes( self.get_colbar_position(), label=label, frameon=False, # transparency needed autoscale_on=False) # We need one image from this cube as a dummy dummyim = cube.imagesinthiscube[0] cube.colorbar = dummyim.Colorbar(frame=cube.cbframe, orientation='vertical') if 'BUNIT' in cube.fitsobj.hdr: dataunits = cube.fitsobj.hdr['BUNIT'] else: dataunits = 'units unknown' cube.colorbar.plot(cube.cbframe, dummyim.image.im) # Postponed draw #dummyim.cmap.add_frame(cube.cbframe) # We want the labels inside the colorbar. We need also a transparent background # for each label so that the labels are visible under all circumstances. bbox=dict(edgecolor='w', facecolor='w', alpha=0.4, boxstyle="square,pad=0.1") #cube.cbframe.yaxis.set_major_formatter(y_formatter) for tick in cube.cbframe.yaxis.get_major_ticks(): tick.set_pad(-5.0) # Matplotlib 1.1 does it correctly. Matplotlib 1.01 not #tick.tick2line.set_markersize(6) tick.tick2line.set_color('w') tick.tick2line.set_markeredgewidth(2) tick.label2.set_horizontalalignment('right') tick.label2.set_bbox(bbox) #flushprint("label=%s"%(tick.label2)) for t in cube.cbframe.get_yticklabels(): # Must be 'plotted' before we can change properties t.set_fontsize(8) t.set_rotation(90) # These dummy print functions prove that both are the same Axes object # print("Frame 1=", id(cube.colorbar.cb.ax)) # print("Frame 2=", id(cube.cbframe)) cube.cbframe.text(0.2, 0.98, dataunits, fontsize=7, fontweight='bold', color='w', rotation=90, horizontalalignment='left', verticalalignment='top', transform=cube.cbframe.transAxes, bbox=dict(edgecolor='w', facecolor='red', alpha=0.5)) # Copy pointer to all images in this cube for im in cube.imagesinthiscube: im.cbframe = cube.cbframe im.hascolbar = cube.hascolbar
[docs] def set_skyout(self, cube, skyout): #---------------------------------------------------------------------------- """ This method changes the celestial system. It redraws the graticule so that its labels represent the new celestial system. :param cube: An entry from the list ``Cubes.cubelist``. For example: ``cube=myCubes.cubelist[0]`` :type cube: An object from class :class:`maputils.Cubeatts`. :param skyout: See :ref:`celestial-skysystems` :type skyout: String or tuple :Examples: Change the sky system from the native system to Galactic and draw a graticule showing the world coordinate system:: from matplotlib.pyplot import figure, show from kapteyn import maputils def postloading(): cube = myCubes.cubelist[0] myCubes.set_skyout(cube, "galactic") myCubes.set_graticule_on(cube, True) fig = figure() frame = fig.add_subplot(1,1,1) myCubes = maputils.Cubes(fig, toolbarinfo=True, helptext=False, printload=False) fitsobject = maputils.FITSimage('ngc6946.fits') slicepos = list(range(1,101)) myCubes.append(frame, fitsobject, axnums=(1,2), slicepos=slicepos, callbackslist={'cubeloaded':postloading}) show() """ #---------------------------------------------------------------------------- if not cube or not len(cube.imagesinthiscube): return for im in cube.imagesinthiscube: im.skyout = im.projection.skyout = skyout #flushprint("Set skyout to %s"%(str(skyout))) if cube.hasgraticule: self.new_graticule(cube, visible=True) currentindx = cube.lastimagenr + cube.movieframeoffset #self.movieimages.indx # Reset current image to change toolbarinfo and possibly graticule #flushprint("MAPUTILS setimage in set_skyout") self.movieimages.setimage(currentindx, force_redraw=True)
[docs] def set_spectrans(self, cube, spectrans): #---------------------------------------------------------------------------- """ This method changes the spectral system. If a valid system is defined, you will notice that spectral coordinates are printed in the new system. If a graticule has a spectral axis, you will see that the labels will change so that they represent the new spectral system. :param cube: An entry from the list ``Cubes.cubelist``. For example: ``cube=myCubes.cubelist[0]`` :type cube: An object from class :class:`maputils.Cubeatts`. :param spectrans: See :ref:`Alternate headers for a spectral line example <spectralbackground-alt>` :type spectrans: String :Examples: Change the sky system from the native spectral system to wavelengths (in m) and draw a graticule showing the world coordinate system. Note that we loaded images with one spatial and one spectral axis (the cube has a RA, DEC and VELO axis) with axnums=(1,3). To get a list with valid spectral translations, we printed ``fitsobject.allowedtrans``:: from matplotlib.pyplot import figure, show from kapteyn import maputils def postloading(): cube = myCubes.cubelist[0] myCubes.set_spectrans(cube, "WAVE") myCubes.set_graticule_on(cube, True) fig = figure() frame = fig.add_subplot(1,1,1) myCubes = maputils.Cubes(fig, toolbarinfo=True, helptext=False, printload=False) fitsobject = maputils.FITSimage('ngc6946.fits') print("Possible spectral translations:", fitsobject.allowedtrans) slicepos = list(range(1,101)) myCubes.append(frame, fitsobject, axnums=(1,3), slicepos=slicepos, callbackslist={'cubeloaded':postloading}) show() """ #---------------------------------------------------------------------------- if not cube or not len(cube.imagesinthiscube): return im0 = cube.imagesinthiscube[0] if im0.projection.specaxnum: # There is a spectral axis in this image (otherwise value is None) if not hasattr(cube, 'nativeproj'): cube.nativeproj = im0.projection for im in cube.imagesinthiscube: #flushprint("Spectral axis aanwezig?=%s"%(str(im.projection.specaxnum))) #flushprint("CTYPES in projection object=%s"%(str(im.projection.ctype))) #flushprint("Spectrans=%s"%spectrans) if spectrans: im.projection = im.projection.spectra(spectrans) im.spectrans = spectrans # Otherwise the spectral translation does not change else: im.projection = cube.nativeproj #flushprint("Set spectrans to %s"%(str(spectrans))) if cube.hasgraticule: self.new_graticule(cube, visible=True) else: # Update the image frame information labels only cube.set_slicemessages(spectrans) currentindx = cube.lastimagenr + cube.movieframeoffset #self.movieimages.indx # Reset current image to change toolbarinfo and possibly graticule self.movieimages.setimage(currentindx, force_redraw=True)
[docs] def set_graticule_on(self, cube, mode): #---------------------------------------------------------------------------- """ This method creates the WCS overlay and refreshes the current image or the relevant image if the current image does not belong to the cube for which a graticule is requested. The method can be used if the *hasgraticule* parameter in :meth:`Cubes.append` has not been set. :param cube: An object from the list Cubes.cubelist. For example: ``cube = myCubes.cubelist[0]`` :type cube: An object from class :class:`Cubeatts`. :param mode: If set to True, a graticule will be plotted. If set to False, an existing graticule will be removed. :type mode: Boolean :Examples: Define a callback which is triggered after loading all the images in a data cube. The callback function extracts the first (and only) cube of a list of cubes and plots the graticules:: from matplotlib.pyplot import figure, show from kapteyn import maputils def postloading(): cube = myCubes.cubelist[0] myCubes.set_graticule_on(cube, True) fig = figure() frame = fig.add_subplot(1,1,1) myCubes = maputils.Cubes(fig, toolbarinfo=True, printload=False) fitsobject = maputils.FITSimage('ngc6946.fits') slicepos = list(range(1,101)) axnums = (1,2) myCubes.append(frame, fitsobject, axnums=(1,2), slicepos=list(range(1,101)), callbackslist={'cubeloaded':postloading}) show() """ #---------------------------------------------------------------------------- if not cube or not len(cube.imagesinthiscube): return cube.hasgraticule = mode currentindx = cube.lastimagenr + cube.movieframeoffset # was: self.movieimages.indx if not mode: # No graticule should be visible if cube.grat: # There exists a graticule. Empty contents, leave frames unaltered for li in cube.linepieces: li.set_visible(False) for tl in cube.textlabels: tl.set_visible(False) """VOG. After MPL 3.5, one cannot set these attributes directly. Replaced by function below. cube.grat.frame.lines = [] cube.grat.frame2.lines = [] cube.grat.frame.texts = [] cube.grat.frame2.texts = [] """ removeLinesandTextfromframe(cube.grat.frame) removeLinesandTextfromframe(cube.grat.frame2) cube.linepieces = [] cube.textlabels = [] for im in cube.imagesinthiscube: im.grat = cube.grat im.linepieces = cube.linepieces im.textlabels = cube.textlabels else: self.new_graticule(cube, visible=True) #flushprint("MAPUTILS setimage in set_graticuleon") self.movieimages.setimage(currentindx, force_redraw=True)
def set_graticules(self): #---------------------------------------------------------------------------- """ This method should be called after zoom/pan actions where we want to update the graticule on an image. Then one should update this for all cubes because a zoom is a zoom in all images in the movie container because each frame is zoomed to the same area on display. This is a feature of Matplotlib. """ #---------------------------------------------------------------------------- if not len(self.movieimages.annimagelist): return # No images yet, nothing to do currentindx = self.movieimages.indx currentimage = self.movieimages.annimagelist[currentindx] cnr = currentimage.cubenr cube = self.cubelist[cnr] for cnr, cube in enumerate(self.cubelist): if cnr == currentimage.cubenr: visible = True else: visible = False self.new_graticule(cube, visible) #self.movieimages.setimage(currentindx) # This method is always triggered in combination with a reposition event # The reposition() method does the re-draw of the canvas. def new_graticule(self, cube, visible): #---------------------------------------------------------------------------- """ Note: For images with one spatial axis and one axis that is not spatial (for instance spectral or Stokes axis), the spatial axis is labeled with offsets. These offsets are a function of the missing spatial axis. This implies that in fact, each image in a movie of the same cube, could have a different labeling. We chose the alternative, which is that for each image the offsets are calculated at the same position on the missing axis. This position is the associated value of CRPIX. """ #---------------------------------------------------------------------------- """ if not len(self.movieimages.annimagelist): return currentindx = self.movieimages.indx currentimage = self.movieimages.annimagelist[currentindx] cnr = currentimage.cubenr cube = self.cubelist[cnr] """ if not cube.hasgraticule: return if not len(cube.imagesinthiscube): return else: # We need just one image to attach the graticule to #currentindx = self.movieimages.indx #currentimage = self.movieimages.annimagelist[currentindx] currentimage = cube.imagesinthiscube[0] #flushprint("ID cube, visible, id currentim= %d %s %d"%(id(cube), visible, id(currentimage))) # This seems the only way to clean up the line Line2D and Text objects # that belong to these frames (Axes objects). # However, the lines and text lists do not store all the relevant lines # and text objects (should it?) if cube.grat: # Then reset what is necessary removeLinesandTextfromframe(cube.grat.frame) removeLinesandTextfromframe(cube.grat.frame2) # Store the Axes objects associated with the graticule before # creating a new one. We need these frames to tell the Graticule's plot # method that it can reuse them. frame1 = cube.grat.frame frame2 = cube.grat.frame2 else: frame1 = frame2 = None # If we arrive in this method, then our imagenew or is just interactively # zoomed in our out. This implies that its graticule system is changed. # We want the graticule also to be changed using the new limits of # the image in pixels. If we use the get_x/ylim methods then the # pixel boundaries are usually not integer. In module wcsgrat, we # Add 0.5 to the pixel positions to extend the boundary pixels so # that an image shows the entire boundary pixel. # But in this situation we don't want this, because the zoom action # determined exactly what the boundary is. pxlim = cube.frame.get_xlim(); pxlim = (pxlim[0]+0.5, pxlim[1]-0.5) pylim = cube.frame.get_ylim(); pylim = (pylim[0]+0.5, pylim[1]-0.5) #flushprint("px/ylim na zoom: %s %s"%(str(pxlim), str(pylim))) # The setup for the graticule cube.grat = currentimage.Graticule(pxlim=pxlim, pylim=pylim) #cube.grat.setp_ticklabel(wcsaxis=0, rotation=0) cube.grat.setp_ticklabel(wcsaxis=1, rotation=90) # Y axis labels along the axis not perpendicular #cube.grat.setp_ticklabel(wcsaxis=(0,1), color='r') #cube.grat.setp_ticklabel(wcsaxis=(0,1), visible=False) cube.grat.setp_tick(wcsaxis=(0,1), visible=False) cube.grat.set_tickmode(plotaxis=("left","bottom"), mode="NO_TICKS") #cube.grat.setp_axislabel(plotaxis=("left","bottom"), color='g', fontsize=10, fontweight='normal') cube.grat.setp_axislabel(plotaxis="left", visible=False) cube.grat.setp_axislabel(plotaxis="bottom", visible=False) cube.grat.setp_gratline(wcsaxis=(0,1), lw=0.5, color='k') bbox=dict(edgecolor='w', facecolor='w', alpha=0.4, boxstyle="square,pad=0.") cube.insideX = cube.grat.Insidelabels(wcsaxis=0, aspect=cube.frame.get_aspect(), fontsize=10, ha='right', va='bottom', rotation_mode='anchor', bbox=bbox) bbox=dict(edgecolor='w', facecolor='w', alpha=0.4, boxstyle="square,pad=0.") cube.insideY = cube.grat.Insidelabels(wcsaxis=1, aspect=cube.frame.get_aspect(), fontsize=10, ha='right', va='bottom', rotation_mode='anchor', bbox=bbox) #flushprint("NEW ASPECTRATIO: %f"%(cube.frame.get_aspect())) cube.grat.plot(cube.frame, frame1, frame2) # In the documentation of wcsgrat we read that insidelabels are plotted on frame2 #cube.insideX.plot(cube.frame) #cube.insideY.plot(cube.frame) #flushprint("cube.frame position=%s"%(str(cube.frame.get_position()))) #flushprint("cube.grat.frame position=%s"%(str(cube.grat.frame.get_position()))) #cube.grat.frame.set_position(cube.frame.get_position()) #cube.grat.frame2.set_position(cube.frame.get_position()) # Default, ticks are copied to the top and right axis. However we did # set those tick lines (Line2D objects) to invisible. We don't want # to include thse in the list of objects that need to be restored # after we change an image. cube.linepieces = set(l for l in cube.grat.frame.findobj(Line2D) if l.get_visible()) cube.textlabels = set(t for t in cube.grat.frame2.findobj(Text) if (t.get_text() != '' and t.get_visible())) #for tl in cube.textlabels: # flushprint(" set_graticule: textlabel %d %s op True"%(id(tl), str(tl))) # Only the current image should show its graticule if not visible: for l in cube.linepieces: l.set_visible(False) for t in cube.textlabels: t.set_visible(False) # A new graticule has been made, so the images should get some # knowledge of that. They are all updated with new line pieces and text # labels (i.e. they point to the same object). We need them if we # change images and want to keep the graticule overlay visible. for im in cube.imagesinthiscube: im.grat = cube.grat im.linepieces = cube.linepieces im.textlabels = cube.textlabels ##flushprint("cube.textlabels = %d"%(len(im.textlabels)))
[docs] def set_coordinate_mode(self, grids=True, world=False, worlduf=False, imval=False, pixfmt="%.1f", dmsprec=1, wcsfmt="%.7f", zfmt='%.3e', appendcr=False, tocli=False, tolog=True, resetim=True): #---------------------------------------------------------------------------- """ Set modes for the formatting of coordinate information. The output is generated by :meth:`Annotatedimage.interact_writepos`. Coordinates are written to the terminal (by default) if the left mouse button is pressed simultaneously with the Shift button. The position corresponds to the position of the mouse cursor. For maps with one spatial axis, the coordinate of the matching spatial axis is also written. There are several options to format the output. World coordinates follow the syntax of the system set by :meth:`Cubes.set_skyout` and :meth:`Cubes.set_spectrans`. Note that the settings in this method, only applies to the coordinates that are written to terminal (or log file). The labels on the graticule are not changed. :param grids: Write cursor position in pixels or grids if ``gridmode=True`` in :meth:`Cubes.append`. (the name ``grids`` is a bit misleading). :type grids: Boolean :param world: Write cursor position in world coordinates for the current celestial and spectral system :type grids: Boolean :param worlduf: Write cursor position in unformatted world coordinates for the current celestial and spectral system. This implies that equatorial coordinates are written in degrees, not in hms/dms format. :type grids: Boolean :param imval: Write the image value at the cursor position in units of the (FITS) header. :type imval: Boolean :param pixfmt: Set the precision of the pixels/grids with a Python number format string. (default: pixfmt="%.1f") :type pixfmt: Number format string :param dmsprec: Number of digits after comma for seconds in latitude. If the longitude is equatorial, one extra digit is used. :type dmsprec: Integer :param wcsfmt: Set the precision of the un-formatted world coordinates with a Python number format string. (Default: wcsfmt="%.7f") :type wcsfmt: Number format string :param zfmt: Set the precision of the image values with a Python number format string (default: zfmt='%.3e') :type zfmt: Number format string :param appendcr: After composing the string with coordinate transformation, add a carriage return character. This can be useful when the output is written to a prompt which expects a carriage return to process the input. :type appendcr: Boolean :param tocli: Only available in the context of GIPSY. This selects the input area of Hermes. Often used together with ``appendcr``. :type tocli: Boolean :param tolog: Only available in the context of GIPSY. This selects the log file of Hermes. :type tolog: Boolean :param resetim: Redraw the current image. The value is set to True by default. The value is set to False in the loading process. After loading, one can set parameters but you need a trigger to make them current. Note for programmers: the parameters in this method are set in :meth:`MovieContainer.toggle_images`. :type resetim: Boolean :Examples: Modify the output of coordinates. We want a position written to a terminal in pixels, formatted and unformatted world coordinates and we also want the image value at the current cursor location:: from matplotlib.pyplot import figure, show from kapteyn import maputils def postloading(): myCubes.set_coordinate_mode(grids=True, world=True, worlduf=True, wcsfmt='%.2f', imval=True, pixfmt='%.2f', dmsprec=2) cube = myCubes.cubelist[0] fig = figure() frame = fig.add_subplot(1,1,1) myCubes = maputils.Cubes(fig, toolbarinfo=True, helptext=False, printload=False) fitsobject = maputils.FITSimage('ngc6946.fits') print("Possible spectral translations:", fitsobject.allowedtrans) slicepos = list(range(1,101)) myCubes.append(frame, fitsobject, axnums=(1,3), slicepos=slicepos, callbackslist={'cubeloaded':postloading}) show() """ #---------------------------------------------------------------------------- #flushprint("len container=%d"%(len(self.movieimages.annimagelist))) for im in self.movieimages.annimagelist: im.coord_grids = grids im.coord_world = world im.coord_worlduf = worlduf im.coord_imval = imval im.coord_appendcr = appendcr im.coord_tocli = tocli im.coord_tolog = tolog im.coord_pixfmt = pixfmt im.coord_wcsfmt = wcsfmt im.coord_zfmt = zfmt im.coord_dmsprec = dmsprec currentindx = self.movieimages.indx #currentimage = self.movieimages.annimagelist[currentindx] if resetim: #flushprint("MAPUTILS setimage in set_coordinate_mode") self.movieimages.setimage(currentindx, force_redraw=True)
def reposition(self, cb=None, force=False): #------------------------------------------------------------------ # After zoom/pan/resize events a draw_event is generated. # We use this event to re-position the panels. #------------------------------------------------------------------ global backgroundchanged backgroundchanged = True if self.zprofileOn: self.zprofileframe.changed = True action = True #(self.numXpanels or self.numYpanels) or force #flushprint("\nThis is a RESIZE event. My action is %s !!!!!!!!!!!!!\n"%(str(action))) has_panels = (self.numXpanels or self.numYpanels) for cube in self.cubelist: if cube.scalefac: # If a scale factor is set, then re calculate the frame borders first self.scaleframe(cube, cube.scalefac, reposition=False) else: self.scaleframe(cube, cube.scalefac, tofit=True, reposition=False) if (has_panels): numXframesList = [] numYframesList = [] for cube in self.cubelist: numXframesList.append(len(cube.panelXframes)) numYframesList.append(len(cube.panelYframes)) pnrX = pnrY = 0 for cube in self.cubelist: frame = cube.frame framep1 = cube.framep1 framep2 = cube.framep2 # TODO: Als ik na het tekenen van slices, de slices 'remove' dan # crasht visions omdat blijkbaar framep1 nog bestaat. De vraag is # dus of framep1/p2 moet worden weggegooid bij deze remove actie. if framep1 and self.numXpanels: #flushprint("Voor: cube=%s framep1=%s %s"%(str(cube), str(id(framep1)), str(framep1))) # The importance of this method is that it re-calculates all # slice panel frames after a resize (usually triggered by a # canvas.draw() call.) boxX, boxY = self.getpanelboxes(frame, self.numXpanels, self.numYpanels, numXframesList, numYframesList, pnrX, None) framep1.set_position(boxX) framep1.set_aspect('auto') pnrX += 1 cube.framep1 = framep1 #flushprint("Na: framep1=%s"%(str(framep1))) if framep2 and self.numYpanels: #flushprint("VOOR: cube=%s framep2=%s %s"%(str(cube), str(id(framep2)), str(framep2))) boxX, boxY = self.getpanelboxes(frame, self.numXpanels, self.numYpanels, numXframesList, numYframesList, None, pnrY) framep2.set_position(boxY) framep2.set_aspect('auto') pnrY += 1 cube.framep2 = framep2 #flushprint("NA: framep2=%s"%(str(framep2))) for cube in self.cubelist: if cube.hasgraticule: if hasattr(cube, 'grat'): if cube.grat: action = True cube.frame.apply_aspect() gratframe = cube.grat.frame #gratframe.apply_aspect() gratframe.set_position(cube.frame.get_position()) #gratframe.set_aspect(cube.frame.get_aspect()) #flushprint("cube.frame position=%s"%(str(cube.frame.get_position()))) #flushprint("cube.grat.frame position=%s"%(str(cube.grat.frame.get_position()))) #gratframe.apply_aspect() gratframe = cube.grat.frame2 #gratframe.set_aspect(cube.frame.get_aspect()) #gratframe.apply_aspect() gratframe.set_position(cube.frame.get_position()) #gratframe.set_aspect(cube.frame.get_aspect()) # Adjust colorbar (Zprofile could have been added/removed for cube in self.cubelist: if cube.cbframe: cube.cbframe.set_position(self.get_colbar_position()) """ for cube in self.cubelist: if cube.cbframe: #flushprint("Ireset the colorbar position") cube.cbframe.set_position([0.,0.,self.colbwidth,1.0]) cube.cbframe.set_autoscale_on(False) #cube.colorbar.cb.set_mappable(cube.imagesinthiscube[0]) # TODO: Er gaat wat mis als je voor het eerst de blankkleur wijzigt. # Dan wordt de image overschreven mat data van het nulde image. """ #self.set_graticule() if action: #currentindx = self.movieimages.indx #self.movieimages.setimage(currentindx, force_redraw=True) # TODO: Als je deze redraw weglaat, is interactie sneller maar je # komt (net) niet in de goede eindtoestand terecht met je graticulen # Ik denk dat het voor de panels nog erger is. self.fig.canvas.draw() """ x0, x1 = cube.frame.get_xlim() y0, y1 = cube.frame.get_ylim() xd0, yd0 = cube.frame.transData.transform((x0,y0)) xd1, yd1 = cube.frame.transData.transform((x1,y1)) #X = cube.frame.get_position().get_points() #flushprint("Coords na resize: %s"%(str(X))) flushprint("Display Coords na resize: %f %f %f %f"%(xd0, yd0, xd1, yd1)) sx = xd1 - xd0; sy = yd1 - yd0 flushprint("new Frame = %f x %f"%(sx, sy)) #xd, yd = self.frame.transData.transform((x,y)) #x2, y2 = self.frame.transData.inverted().transform((xd,yd)) #flushprint("Display coordinates data=%f %f display=%f %f inverse=%f %f"%(x, y, xd, yd, x2, y2)) """ def redopanels(self): #------------------------------------------------------------------ """ Draw the slice panels for the first time or update them (the create_panel() method checks whether something needs to be plotted). The check is done for every cube in the cube list. """ #------------------------------------------------------------------ return for cnr in range(0, self.numcubes): self.create_panel('r', self.cubelist[cnr])
[docs] def set_splitimagenr(self, nr): #------------------------------------------------------------------------- """ Set image (by its number) which is used to split the current screen. :param nr: Number of the movie frame that corresponds to the image you want to use together with the current image to split and compare. Note that these movieframe numbers start with 0 and run to the number of loaded frames. The numbering continues after loading more than 1 data cube. :type nr: Integer """ #------------------------------------------------------------------------- num = len(self.movieimages.annimagelist) if nr < 0 or nr >= num: raise ValueError("Image nr %d does not exist (%d<nr<%d)!"%(nr, 0, num)) else: # First reset a possible earlier image that was used to compare # to the current. if self.movieimages.compareim: currentimage = self.movieimages.annimagelist[self.movieimages.indx] currentimage.image.im.set_alpha(1.0) self.movieimages.compareim.image.im.set_alpha(1.0) self.movieimages.compareim.image.im.set_visible(False) self.movieimages.compareim = None self.splitmovieframe = nr
def show_transparent(self, cube, alpha): """ #----------------------------------------------------------------------- Make current image transparent with alpha factor #----------------------------------------------------------------------- """ splitmovieframe = self.splitmovieframe # A Cubes attribute # If the current image is the same as the second image then do nothing. currentindx = self.movieimages.indx if splitmovieframe == currentindx: return currentimage = self.movieimages.annimagelist[currentindx] # The current image is already visible. But we need to make # the second image visible too. secondimage = self.movieimages.annimagelist[splitmovieframe] secondimage.image.im.set_visible(True) if splitmovieframe > currentindx: # The second image is on top (because it was loaded later and the # zorder is derived from the loading order). So make this image # transparent. But if the alpha is close to 1, we want the current # image to be visible, so the second image should be very transparent # then. This behaviour can be obtained by setting the transparency # to 1-alpha secondimage.image.im.set_alpha(1.0-alpha) currentimage.image.im.set_alpha(1.0) else: currentimage.image.im.set_alpha(alpha) secondimage.image.im.set_alpha(1.0) self.fig.canvas.draw() # Note that we need some reset actions. For example, we don't want # to keep the second image visible if we change images or select # another image as compare image. # But if we do this at this point, then it will not be possible to # save a plot to disk because it does a re-draw and sees different # settings then. So resetting must be done elsewhere e.g. in # toggle_images(). Then store the images used to compare. self.movieimages.compareim = secondimage def splitscreen(self, cb): """ #----------------------------------------------------------------------- With Ctrl on the keyboard and the mouse buttons 1, 2 or 3 while moving the mouse, one can split the screen to make another image visible. It makes the upper (=current image) transparent and shows the image which has index 'splitmovieframe'. #----------------------------------------------------------------------- """ if cb is None or cb.event is None: return if cb.event.button is None or cb.event.key is None: return if cb.event.key != 'control': return # Which mousebutton is pressed (together with key 'shift')? mb = cb.event.button if not mb in [1,2,3]: # Sometimes mb = 'up' which we don't want here return # Which movie are we in? try: currentindx = self.movieimages.indx except: return # What is the movie frame number with which we want to split the screen? splitmovieframe = self.splitmovieframe # A Cubes attribute # If the current image is the same as the splitimage (i.e. the # image below the current image) then do nothing. if splitmovieframe == currentindx: return cube = self.cubelist[cb.cubenr] # We need a copy of the original data and make part of it transparent currentimage = self.movieimages.annimagelist[currentindx] if currentimage.cubenr != cb.cubenr: # This came from an event in another frame than the frame # of the current image return splitimage = self.movieimages.annimagelist[splitmovieframe] if currentimage.origdata is None: # Make copy first and reset variables cube.origdata = currentimage.data.copy() currentimage.origdata = cube.origdata currentimage.splitmovieframe = splitmovieframe currentimage.split_xold = currentimage.split_yold = 0 currentimage.split_mb = mb # Get the mouse position in integer pixel coordinates and # convert to array indices #flushprint("px/pylim=%s %s"%(str(cube.pxlim), str(cube.pylim))) # If an image has a box that is different compared to the default, # we have to correct for the limits to have the array indices started at # 0. For a default box, the indices start at 1 (first pixel). # For another box, this number is the pixel number of the first # pixel in the box. and is always > 1. # Note that we change index at the center of a pixel (which explains the # addition 0f 0.5) xi = int(nint(cb.xdata+0.5) - cube.pxlim[0]) yi = int(nint(cb.ydata+0.5) - cube.pylim[0]) xo = int(currentimage.split_xold); yo = int(currentimage.split_yold) # If the mouse button has switched, then restore entire image first if mb != currentimage.split_mb: currentimage.data[:] = cube.origdata[:] currentimage.split_xold = currentimage.split_yold = 0 if mb == 1: # Left mouse button & 's' is split in y if yi > currentimage.split_yold: currentimage.data[0:yi] = numpy.nan elif yi < currentimage.split_yold: currentimage.data[yi:yo] = cube.origdata[yi:yo] elif mb == 2: # Middle mouse button & 's' is split in x if xi > currentimage.split_xold: currentimage.data[:,0:xi] = numpy.nan elif xi < currentimage.split_xold: currentimage.data[:, xi:xo] = cube.origdata[:, xi:xo] elif mb == 3: # Right mouse button & 's' is split in x & y if xi > currentimage.split_xold: currentimage.data[0:yo:,0:xi] = numpy.nan elif xi < xo: currentimage.data[0:yo:, xi:xo] = cube.origdata[0:yo, xi:xo] if yi > yo: currentimage.data[yo:yi, 0:xi] = numpy.nan elif yi < yo: currentimage.data[yi:yo, 0:xi] = cube.origdata[yi:yo, 0:xi] currentimage.split_xold = xi currentimage.split_yold = yi currentimage.split_mb = mb splitimage.image.im.set_visible(True) #for v in splitimage.data: # if numpy.isnan(v).any(): # print(v, " is NaN") currentimage.image.im.set_visible(True) # This is the trick. We set all the new NaN numbers to transparent. # But first we need to store the current colormap settings currentimage.old_blankcolor = currentimage.cmap.bad_val currentimage.set_blankcolor('w', alpha=0.0) # Transparent # The blank color is reset to its old value if we change images # in toggle_images() #currentimage.image.im.changed() # 29-06-2023, VOG: the currentimage.image.im is a MPL AxeImage object. # It was created with the plot method of the Image class. Its data # was linked to currentimage.data. But after MPL 3.5 one cannot change # the data in currentimage.data and assume the image data has been changed # then also. This implies that the image data needs to be changed with a setter. # see alos: https://matplotlib.org/stable/api/image_api.html currentimage.image.im.set(data = currentimage.data) currentimage.frame.draw_artist(currentimage.image.im) if self.imageinfo: currentimage.frame.draw_artist(currentimage.info) for li in currentimage.linepieces: #li.set_visible(True) currentimage.frame.draw_artist(li) for tl in currentimage.textlabels: #li.set_visible(True) currentimage.frame.draw_artist(tl) #currentimage.set_blankcolor('w', alpha=0.0) # Transparent #currentimage.frame.figure.canvas.blit(currentimage.frame.bbox) def updatepanels(self, cb, frompanel1=False, frompanel2=False, cubenr=None, xpos=None, ypos=None, force=False): #------------------------------------------------------------------ # One moved the mouse while pressing button 1 in the main window # or one uses the arrow keys together with <Shift> # Then one extracts slices at different positions in the data cube. # This is reflected in an update of the data in the corresponding # panels. # Note that if you stacked more than # one cube, you will have more than one Axescallback. Each one # will update its corresponding slice panels. This behaviour is # copied from GIPSY's GIDS program. # # This method expects a callback object 'cb' which should have an # attribute 'cubenr' so that we can know which cube gets updated # slice panels. # If the callback cb is None, then we expect the cube number and # the mouse position for the cube that corresponds to the cube number # # Crosshair cursor: # # When one changes the cursor to a crosshair, then changing the # position of the cursor (while pressing mb1) should not only # update the panel images, but must also move the crosshair to a # new position. The callback 'cb' can originate from more than one # frame, but we need to move the crosshair only once, so we need # to know whether the cube in this method is also the cube to which the # current image belongs. # # if cube is currentcube: # if crosshair: # restore the original background, i.e. without any cursor lines # move the cursor # # If the window has been resized, the pixel buffers of the backgounds # do not represent the real display area, so we need to copy the background # for all cubes. The content of this background of a frame that is # not at the foreground is not correct (it contains pixels from the # image on top), is not important, because it will be restored # as soon a new image is ordered with the setimage() method. This method # has a callback which calls method drawCrosshair(). There, a new background # will be created for the frame on top, because its image has been changed. # Note that if the window is resized in method reposition(), # the flag 'backgroundchanged' is set. At that moment the images are # plotted without crosshair lines and it is save to store the new # backgrounds in this method. #------------------------------------------------------------------ global backgroundchanged skipupdatep1 = skipupdatep2 = False if frompanel1: # Event came from panel 1 so do not update panel 1 skipupdatep1 = True if frompanel2: skipupdatep2 = True frommain = not (frompanel1 or frompanel2) skipupdatep1 = skipupdatep2 = False # VOG experiment if cb is None: x = xpos; y = ypos #s = "Entered from updatemovieframe. xpos=%f, ypos=%f"%(xpos,ypos) #flushprint(s) cubeindx = cubenr if x is None or y is None: # !!!!!!!! Testen return else: # Note that cubenr, xdata and ydata are attributes of AxesCallback # while other properties are in cb.event (e.g. cb.event.name) # Note also that we process both motion_notify_event as key_press_event if cb and cb.event.name == 'motion_notify_event': if cb.event.button != 1: return # Because to get panel interaction, we need to press the left button if cb and cb.event.name == 'key_press_event': if not cb.event.key.upper().startswith('SHIFT+'): return # React only to arrow keys when shift is pressed also if not hasattr(cb, 'cubenr'): return x = cb.xdata; y = cb.ydata cubeindx = cb.cubenr cube = self.cubelist[cubeindx] canvas = cube.frame.figure.canvas currentimage = self.movieimages.annimagelist[self.movieimages.indx] cnr = currentimage.cubenr currentcube = self.cubelist[cnr] # For the arrow keys we set positions relative to the previous position # and not relative to the cursor position. if cb and cb.event.name == 'key_press_event': k = cb.event.key.upper() skip = len("SHIFT+") arrow = k[skip:] if 'UP' in arrow: x = cube.xold y = cube.yold + 1 elif 'DOWN' in arrow: x = cube.xold y = cube.yold - 1 elif 'RIGHT' in arrow: x = cube.xold + 1 y = cube.yold elif 'LEFT' in arrow: x = cube.xold - 1 y = cube.yold xold = cube.xold yold = cube.yold #frame = cube.frame #fig = frame.figure framep1 = cube.framep1 framep2 = cube.framep2 mplimp1 = cube.annimp1 mplimp2 = cube.annimp2 pxlim = cube.pxlim pylim = cube.pylim offset = cube.movieframeoffset # The limits of the movie frame axes are pxlim, pylim. # We are interested in the pixel position and this is stored by # the callback object 'cb'. xi = int(numpy.round(x)) yi = int(numpy.round(y)) if skipupdatep1 and x == xold and not force: # Skip this position return if skipupdatep2 and yi == yold and not force: return # Check the limits if skipupdatep1 and (xi > pxlim[1] or xi < pxlim[0]): return elif skipupdatep2 and (yi > pylim[1] or yi < pylim[0]): return if not (skipupdatep1 or skipupdatep2): if xi > pxlim[1] or xi < pxlim[0] or yi > pylim[1] or yi < pylim[0]: return if cube is currentcube and self.zprofileOn: if not (frompanel1 or frompanel2): self.plotZprofile(cube, x, y) else: if frompanel1: self.plotZprofile(cube, x, None) if frompanel2: self.plotZprofile(cube, None, y) #currentimage = self.movieimages.annimagelist[self.movieimages.indx] #cnr = currentimage.cubenr #currentcube = self.cubelist[cnr] # Draw a crosshair if self.crosshair: # If the window is resized, we need to create new pixel buffers for the backgrounds for C in self.cubelist: if not C.background or backgroundchanged: if C is currentcube: if C.background: C.linev.set_visible(False) #self.visible and self.vertOn) C.lineh.set_visible(False) C.frame.draw_artist(C.linev) C.frame.draw_artist(C.lineh) self.fig.canvas.draw() #self.fig.canvas.blit(C.frame.bbox) #flushprint("Zet background terug") #self.fig.canvas.restore_region(C.background) C.background = self.fig.canvas.copy_from_bbox(C.frame.bbox) """ if C.panelXframes: if not C.panelXback or backgroundchanged: C.panelXback = self.fig.canvas.copy_from_bbox(C.framep1.bbox) if C.panelYframes: if not C.panelYback or backgroundchanged: C.panelYback = self.fig.canvas.copy_from_bbox(C.framep2.bbox) """ #flushprint("Background changed ?%s"%(str(backgroundchanged))) # Draw new crosshair only for image/frame on display if cube is currentcube: #flushprint("Cube is current cube") #flushprint("Crosshair in updatepanels() set at %f %f"%(x, y)) if not (frompanel1 or frompanel2): cube.linev.set_xdata((x,x)) cube.lineh.set_ydata((y,y)) else: if frompanel1: cube.linev.set_xdata((x,x)) if frompanel2: cube.lineh.set_ydata((y,y)) cube.linev.set_visible(True) #self.visible and self.vertOn) cube.lineh.set_visible(True) #self.visible and self.horizOn) self.fig.canvas.restore_region(cube.background) cube.frame.draw_artist(cube.linev) cube.frame.draw_artist(cube.lineh) #flushprint("I blit for new crosshair") self.fig.canvas.blit(cube.frame.bbox) else: cube.linev.set_visible(False) cube.lineh.set_visible(False) # PANEL 1 = PANEL X = Horizontal panel # If the crosshair position is changed in the main panel, # we need perhaps a new image in the slice panels. Note that # yi and yold are integer positions and must be different then. # The event source cannot be panel X because cursor changes in # that panel in y only changes the subset in main and # changes in x do not change the slice because the slice is taken along # the x axis. # New image data for panel X if cube.panelXframes and ((yi != yold and not frompanel1) or force): #if backgroundchanged: #self.fig.canvas.restore_region(cube.panelXback) M = cube.Mx ly = M.shape[0] j = 0 for indx in cube.panelXframes: # yi should be in range of pylim. To make it an array index, # subtract the limit pylim[0] I = self.movieimages.annimagelist[offset+indx] if I.origdata is not None: N = I.origdata else: N = I.data M[ly-j-1] = N[int(yi-pylim[0])] j += 1 mplimp1.image.im.set_data(M) if cube.panelXframes and (((y != yold or x != xold)) or force): framep1.draw_artist(mplimp1.image.im) if self.crosshair and cube is currentcube: if frompanel1: cube.panelxline.set_ydata((y,y)) framep1.draw_artist(cube.panelxline) if not frompanel2: cube.panelxline2.set_xdata((x,x)) framep1.draw_artist(cube.panelxline2) #mplimp1.image.im.changed() if mplimp1.contourSet: for col in mplimp1.contourSet.collections: framep1.collections.remove(col) mplimp1.contourSet = None # And create a new set of contours im = cube.imagesinthiscube[0] if im.contours_on: # contours in panels set to on cmap, colors, norm, levels = (im.contours_cmap, im.contours_colour, im.norm, im.clevels) # There must be either a cmap or a color specification in contour() if cmap is None and colors is None: colors = 'r' if not (cmap is None): cmap = cm.get_cmap(cmap) colors = None mplimp1.contourSet = framep1.contour(mplimp1.image.im.get_array(), levels=levels, extent=mplimp1.box, cmap=cmap, norm=norm, colors=colors) for col in mplimp1.contourSet.collections: # Make elements visible before blit action col.set_visible(True) mplimp1.frame.draw_artist(col) #for li in mplimp1.linepieces: # framep1.draw_artist(li) framep1.figure.canvas.blit(framep1.bbox) # PANEL 2 = PANEL Y = VERTICAL PANEL # Second frame which shares an y axis with main plot if cube.panelYframes and ((xi != xold and not frompanel2) or force): M = cube.My lx = M.shape[1] j = 0 for indx in cube.panelYframes: I = self.movieimages.annimagelist[offset+indx] if I.origdata is not None: N = I.origdata else: N = I.data M[:,j] = N[:,int(xi-pxlim[0])] # self.movieimages.annimagelist[offset+indx].data[:,xi-pxlim[0]] j += 1 mplimp2.image.im.set_data(M) if cube.panelYframes and (((x != xold or y != yold)) or force): framep2.draw_artist(mplimp2.image.im) if self.crosshair and cube is currentcube: if frompanel2: cube.panelyline.set_xdata((x,x)) framep2.draw_artist(cube.panelyline) if not frompanel1: cube.panelyline2.set_ydata((y,y)) framep2.draw_artist(cube.panelyline2) #mplimp2.image.im.changed() # We need to draw contours every time the data in a panel has changed. # Take the simple route and use the contour method. Use the # collections object to plot and remove individual contours. The # removal is not automatically. The contour objects must be removed explicitely # from the collections attribute of the current frame. if mplimp2.contourSet: for col in mplimp2.contourSet.collections: framep2.collections.remove(col) mplimp2.contourSet = None # And create a new set of contours im = cube.imagesinthiscube[0] if im.contours_on: # contours in panels set to on cmap, colors, norm, levels = (im.contours_cmap, im.contours_colour, im.norm, im.clevels) # There must be either a cmap or a color specification in contour() if cmap is None and colors is None: colors = 'r' if not (cmap is None): cmap = cm.get_cmap(cmap) colors = None mplimp2.contourSet = framep2.contour(mplimp2.image.im.get_array(), levels=levels, extent=mplimp2.box, cmap=cmap, norm=norm, colors=colors) for col in mplimp2.contourSet.collections: # Make elements visible before blit action col.set_visible(True) mplimp2.frame.draw_artist(col) #for li in mplimp2.linepieces: # framep2.draw_artist(li) framep2.figure.canvas.blit(framep2.bbox) #cs = mplimp2.Contours(None, colors='c') #cs.plot(mplimp2.frame) # If the background was changed (in size or color), then we reset # the flag. If this method is called by a second event at the same # cursor position, there is no need to resize the pixel buffers again. backgroundchanged = False if frompanel1: # Event from panel2: Only x has been changed cube.xold = xi elif frompanel2: # Event from panel1: Only y has been changed cube.yold = yi else: # Event in main window. Both x and y are changed cube.xold = xi cube.yold = yi #self.fig.canvas.flush_events() def getbiggestbox(self): #--------------------------------------------------------------- """ This method finds (e.g. after a resize action) the biggest frame in the list of cubes. That frame sets the positions of the slice panels (usually the position velocity slices). """ #--------------------------------------------------------------- self.biggestbox = None for c in self.cubelist: # Get the position of all the frames # With this if statement we assume that the X&Y panelframes are known for each cube if c.panelXframes or c.panelYframes: c.frame.apply_aspect() # TODO: alweer een frame_aspect? Nodig of niet? bbox = c.frame.get_position() # The bounds attribute represents x0,y0,w,h, but we want # x0,y0,x1,y1: box = [bbox.xmin, bbox.ymin, bbox.xmax, bbox.ymax] if self.biggestbox is None: self.biggestbox = box else: if box[0] < self.biggestbox[0]: self.biggestbox[0] = box[0] if box[1] < self.biggestbox[1]: self.biggestbox[1] = box[1] if box[2] > self.biggestbox[2]: self.biggestbox[2] = box[2] if box[3] > self.biggestbox[3]: self.biggestbox[3] = box[3] return self.biggestbox def clearimage(self, im): for li in im.linepieces: li.set_visible(False) for tl in im.textlabels: tl.set_visible(False) if im.infoobj: im.infoobj.set_visible(False) im.image.im.set_visible(False) if im.cbframe is not None: im.cbframe.set_visible(False) def scaleframe(self, cube, scalefac, tofit=False, reposition=True): #---------------------------------------------------------------------------- """ Scale the current frame. We position the frame in the upper left corner. This gives the best results if panels are added. A user can resize the window to get an optimal view. """ #---------------------------------------------------------------------------- fig = cube.frame.figure xf0, yf0 = fig.transFigure.inverted().transform((0, 0)) xf1, yf1 = fig.transFigure.inverted().transform((self.colbwpixels, 0)) dx = xf1 - xf0 #dy = yf1 - yf0 self.colbwidth = dx if tofit: if cube.hascolbar: left = self.colbwidth else: left = 0.0 if self.zprofileOn: top = 1.0 - self.zprofileheight else: top = 1.0 if self.numXpanels or self.numYpanels: figaspect = fig.get_figwidth()/fig.get_figheight() # Make space for panels if there are any frames if self.numXpanels: H_vert = 0.2 else: H_vert = 0.0 H_horz = H_vert/figaspect if self.numYpanels: right = 1.0-0.2/figaspect else: right = 1.0 bottom = 0.0+H_vert #top=1.0 else: right = 1.0 bottom = 0.0 #top = 1.0 cube.scalefac = None else: # This is the option where a user zooms the entire 'current' # cube and corresponding panels. cube.scalefac = scalefac x0, x1 = cube.pxlim y0, y1 = cube.pylim lx = x1 - x0 + 1; ly = y1 - y0 + 1 #flushprint("pxylim x0, y0=%f %f"%(x0, y0)) #flushprint("pxylim x1, y1=%f %f"%(x1, y1)) #flushprint("scalefac = %s"%(str(scalefac))) #flushprint("in data lx, ly=%f %f"%(lx, ly)) # Calculate the width and height in normalized coordinates of # a zoomed image fig = cube.frame.figure xf0, yf0 = fig.transFigure.inverted().transform((0, 0)) xf1, yf1 = fig.transFigure.inverted().transform((lx*scalefac, ly*scalefac)) xf = xf1 - xf0; yf = yf1 - yf0 if cube.hascolbar: left = self.colbwidth else: left = 0.0 right = left + xf top = 1.0 bottom = top - yf # Note that we cannot use subplots_adjust, because the zooming # factor applies to individual cubes and we don't want to adjust # all the cubes at the same time. cube.frame.set_position([left, bottom, right-left, top-bottom]) # The call to this method could come from method reposition() # If not so, you need to request re-positioning explicitely. if reposition: self.reposition(force=True) def loadimages(self, cb): #---------------------------------------------------------------------------- """ Loading image(s) into a movie cube is done with a timer callback. This callback makes it possible to show the images that you load immediately, which is more user friendly then when you have to wait until all images are loaded to see something happening on screen. Also it makes the gui more responsive. """ #---------------------------------------------------------------------------- cube = cb.cube cnr = cube.cnr fig = cube.frame.figure zprofileOn = cb.zprofileOn oldim = self.lastimage # We need to remove the last displayed image #self.movieimages.movie_events(allow=False) i = cb.count if i == 0: if self.progressbar: self.progressbar.setMinimum(0) self.progressbar.setMaximum(cube.nummovieframes) cube.callback('waitcursor') # Remove image and other attributes if oldim: self.clearimage(oldim) # We want image to fill as much space as possible. Set this only # once (i==0, cnr==0) if 1: # TODO: Moet dit? : cnr == 0: # Re-position frame to fill the available space. Do not use method # frame.set_position() because that will not update the subplot # configuration values like 'left', 'right', etc. xf0, yf0 = fig.transFigure.inverted().transform((0, 0)) xf1, yf1 = fig.transFigure.inverted().transform((self.colbwpixels, 0)) dx = xf1 - xf0 #dy = yf1 - yf0 self.colbwidth = dx if zprofileOn: top = 1.0 - self.zprofileheight else: top = 1.0 fig.subplots_adjust(left=self.colbwidth, right=1.0, bottom=0.0, top=top) # A message for the terminal if self.printload and cube.slicepos: s = "\nLoading image at slice position (in pixels)=%s"% str(cube.slicepos[i]) flushprint(s) # We need different axes in this cube. if cube.slicepos: cube.fitsobj.set_imageaxes(cube.axnums[0], cube.axnums[1], slicepos=cube.slicepos[i]) # Set the box cube.fitsobj.set_limits(cube.pxlim, cube.pylim) # If no aspect ratio for the pixels was given, then find a default # Note that only in this method, the image axes are known and a proper # aspect ratio can be determined. if cube.pixelaspectratio is None: cube.pixelaspectratio = cube.fitsobj.get_pixelaspectratio() mplim = cube.fitsobj.Annotatedimage(cube.frame, cmap=cube.cmap, clipmin=cube.vmin, clipmax=cube.vmax, gridmode=cube.gridmode, adjustable='box', anchor='NW', newaspect=cube.pixelaspectratio, clipmode=cube.clipmode, clipmn=cube.clipmn, callbackslist=cube.callbackslist) # Prepare for blitting mplim.Image(animated=False, alpha=1.0) mplim.plot() # We add two attributes that are necessary to restore graticules mplim.linepieces = [] mplim.textlabels = [] mplim.cubenr = cnr # Essential if cube.cmap is None: cube.cmap = mplim.cmap if not (i == 0 and cnr == 0): # Make the previous image (that is on the display now) invisible oldim.image.im.set_visible(False) # The text for the info object is first set to a dummy. This is the # text we see while loading the images. After the loading, the # text changes to a more informative message. s = "image number %d: "%self.movieframecounter mplim.info = None if self.imageinfo: mplim.info = mplim.frame.text(0.01, 0.98, s, horizontalalignment='left', verticalalignment='top', transform=mplim.frame.transAxes, fontsize=8, color='w', animated=True, family='monospace', bbox=dict(facecolor='red', alpha=0.5)) if i == 0: mplim.infoobj = mplim.info mplim.infoobj.set_animated(False) # Otherwise nothing will be displayed else: mplim.infoobj = oldim.infoobj mplim.infoobj.set_text(str(i)) # Dummy else: mplim.infoobj = None # Note that in the next method, each movie gets its own informative # text about its slice. The attribute is called 'slicemessage' self.lastimage = mplim self.movieimages.append(mplim, visible=True, cubenr=cnr, slicemessage=s) self.movieframecounter += 1 cube.imagesinthiscube.append(mplim) if cb.count+1 == cube.nummovieframes: if cube.hasgraticule: self.new_graticule(cube, visible=False) # Alwas create a colorbar. Use set_visible to make it (in)visible. self.new_colorbar(cube) # Set a message for the current spectral translation cube.set_slicemessages(cube.fitsobj.spectrans) # spectrans is the same for all mplim's # Set the color normalization to equal for all in this cube. # We gathered all the images and know for each image the minimum # and maximum value in the Annoteded image object (attributes clipmin, clipmax) # These attributes are always finite numbers. If a vmin and vmax # were given, then the clips for all the images are set to these values. #flushprint("Voor 1e maal naar setcubeclips is clipmode: %d"%(cube.clipmode)) cube.setcubeclips(cube.vmin, cube.vmax, cube.clipmode, cube.clipmn) cube.vmin_def = cube.vmin cube.vmax_def = cube.vmax # Note that if we use the Cubes class from a gui program, then the gui # will be in an endless loop and we don't need to give a show() command. # If however, the class is used from a simple script, then the script # should execute Matplotlib's show() command. self.reposition() # Re position graticule etc. before a final draw (included in reposition()) else: fig.canvas.draw() # Do not blit yet because movie container is not filled cb.count += 1 if self.progressbar: self.progressbar.setValue(cb.count) if cb.count == cube.nummovieframes: # We already added 1 to cb.count # Deschedule this callback. Perhaps more loads are scheduled. cb.deschedule() self.set_coordinate_mode(resetim=False) # Configure output of coordinates for all images # The use of the timerlist is to support loading multiple cubes # The loading process is appended to the previous and will not # interfere with the current loading process. # self.timerlist.pop(0) # Get next element until there is nothing more if self.timerlist: self.timerlist[0].schedule() else: # The last image must be set to invisible # This cannot be done by the setimage() method because this # method does not know yet what the previous image is. self.lastimage.image.im.set_visible(False) # Interesting point to show memory use to calling environment self.callback('memory', getmemory()) # At this point there is nothing to load anymore, so we start # to define some callbacks for mouse interaction if self.cubelist[0].panelscb is None: # First time self.set_interactionMovieToPanels() #self.drawpanels() # Draw the slice panels for the first time else: self.update_interactionMovieToPanels() # Panels can get new sizes e.g after adding a cube while the # input fields already had slices defined for the new cube #self.redopanels() #flushprint("MAPUTILS SETS IMAGE nr %d"%(cube.movieframeoffset)) self.movieimages.setimage(cube.movieframeoffset) self.movieimages.movie_events(allow=True) self.callback('finished') # Callback from calling environment to finish up if self.progressbar: self.progressbar.reset() # Reset bar cube.callback('resetcursor') cube.callback('cubeloaded') # Add a callback to the color map. If this changes then we want # to set a global flag. Use the method below to set this flag. cube.cmap.auto = True # Allow images to be updated immediately after a change to the color map cube.cmap.callback = self.cmapcallback def set_interactionMovieToPanels(self): #------------------------------------------------------------------------- """ The movie frames for the current cube are all stored in the movie. Register a callback for interaction in the movie. Each cube is associated with a matplotlib frame (Axes object). So a change in mouse position will update the slice panels that belong to each cube """ #------------------------------------------------------------------------- for cube in self.cubelist: if cube.nummovieframes >= 1: cube.panelscb = AxesCallback(self.updatepanels, cube.frame, 'motion_notify_event', cubenr=cube.cnr) cube.panelscbkey = AxesCallback(self.updatepanels, cube.frame, 'key_press_event', cubenr=cube.cnr) # We need a separate callback for splitscreen actions. # These actions need the control key to be pressed # and the mouse to be moved. cube.splitcb = AxesCallback(self.splitscreen, cube.frame, 'motion_notify_event', cubenr=cube.cnr) cube.splitcb.deschedule() for aim in cube.imagesinthiscube: aim.splitcb = cube.splitcb def update_interactionMovieToPanels(self): """ #----------------------------------------------------------------------- Update interaction in movie container. This implies that we add a new callback, because this method is called only when we added a new cube. #----------------------------------------------------------------------- """ for cube in self.cubelist: if cube.panelscb is not None: cube.panelscb.schedule() cube.panelscbkey.schedule() else: if cube.nummovieframes >= 1: cube.panelscb = AxesCallback(self.updatepanels, cube.frame, 'motion_notify_event', cubenr=cube.cnr) cube.panelscbkey = AxesCallback(self.updatepanels, cube.frame, 'key_press_event', cubenr=cube.cnr) if cube.splitcb is None: cube.splitcb = AxesCallback(self.splitscreen, cube.frame, 'motion_notify_event', cubenr=cube.cnr) for aim in cube.imagesinthiscube: aim.splitcb = cube.splitcb cube.splitcb.deschedule() def end_interactionMovieToPanels(self): """ #----------------------------------------------------------------------- End interaction in side panels. Note that this method is called in the append() method where the timer is scheduled for the loadimages() method. There we append a cube which does not have any callbacks set, but in a loop over all the cubes we check whether attribute 'panelscb' is set. #----------------------------------------------------------------------- """ for cube in self.cubelist: if cube.panelscb is not None: cube.panelscb.deschedule() if cube.panelscbkey is not None: cube.panelscbkey.deschedule() if cube.splitcb is not None: cube.splitcb.deschedule()
# -- End of this source --