ejeschke / ginga

The Ginga astronomical FITS file viewer
BSD 3-Clause "New" or "Revised" License
121 stars 77 forks source link

request to be able to show a second image of values as one explores primary image #842

Open jhennawi opened 4 years ago

jhennawi commented 4 years ago

Dear Ginga Developers,

We have been developing the PypeIt spectroscopic data reduction package (see https://arxiv.org/abs/2005.06505) and we use ginga as our data visualization viewer. We are anticipating widespread adoption by spectroscopists. Four viewing spectra, it would be very valuable if we could display the wavelengths as a second set of values as we move the cursor around the image, analogous to the alpha and delta WCS coordinates. Currently in order to get this to work we have to use an unpleasant hack, i.e in RC mode:

ch.load_np(chname, img, 'fits', header, wcs_image='wavelengths.fits')

Which then allows us to display wavelengths instead of the WCS. Besides being clunky, i.e. we have to write out the wavlengths.fits file, this also had the unwanted side-effect of then breaking the WCSMatch feature, when we want to display multiple registered images (i.e. raw data and sky-subtracted spectra) next to each other.

We would greatly appreciate an option like to simply pass in an image

ch.load_np(chname, img, 'fits', header, waveimg = waveimg)

That would allow us to display the wavelength image directly with "Wavelength" being currently displayed as alpha and delta are.

Would it be possible to add such functionality? We realize this could probably be accommodated with a plugin, but we have thus far not been able to venture that deeply into the ginga source to figure out how to do so.

Many Thanks, Joe Hennawi on behalf of the PypeIt team

pllim commented 4 years ago

I can only advise from the plugin side. You might be able to pull it off by writing something like PixTable and make it display values from another extension. Maybe my implementation over at stginga to read values from another extension can inspire you?

I can display DQ straight on active image using DQInspect, so I don't see why you cannot similarly display values from another extension on yours.

ejeschke commented 4 years ago

@jhennawi, can you clarify just a little? Do you have to construct an auxiliary image or is that simply for the convenience of making the wavelength values conveniently accessible? Is the original data a cube?

jhennawi commented 4 years ago

Hi Eric,

Thanks for the quick reply. We have two images say, a science image and wavelength image. At the moment these are images. In the future for IFUs these may be cubes, and then we would want something like ds9's cube functionality, but were not there yet.

In reality we have like 4 different science diagnostic images (raw data, sky-subtracted, sky-subtracted noise normalized, etc.) and then we have a common wavelength image for them all. What we want to be able to do is use ginga in WCS registered mode and toggle around all these images (that currently works). We then want to be able to see the wavelength value at the bottom of the screen based on the pixel we are hovering over, as derived from the wavelength image.

As I mentioned, we hacked this in with the WCS, but then that breaks WCS registration. I'll add finally that the WCS registration is just based on aligning the pixels, i.e. we are not actually using a full WCS, since these are spectra.

Joe

On Tue, May 26, 2020 at 5:04 PM ejeschke notifications@github.com wrote:

@jhennawi https://github.com/jhennawi, can you clarify just a little? Do you have to construct an auxiliary image or is that simply for the convenience of making the wavelength values conveniently accessible? Is the original data a cube?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/ejeschke/ginga/issues/842#issuecomment-634344177, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACC6HI7APGYXUHPDA2B65E3RTRKKDANCNFSM4NKEOG2A .

--

Joseph F. Hennawi Associate Professor Department of Physics Broida Hall, UC Santa Barbara Santa Barbara, CA 93106-9530 Phone: 805-893-3503 Mobile: 805-450-8697 E-mail: joe@ joe@physics.ucsb.eduphysics.ucsb.edu http://web.physics.ucsb.edu/~joe/ enigma.physics.ucsb.edu

ejeschke commented 4 years ago

I'll add finally that the WCS registration is just based on aligning the pixels, i.e. we are not actually using a full WCS, since these are spectra.

So the wavelength value for a given pixel at (X, Y) (data coords) is just the value of the wavelength array at (X, Y)?

jhennawi commented 4 years ago

that is right.

On Tue, May 26, 2020 at 5:31 PM ejeschke notifications@github.com wrote:

I'll add finally that the WCS registration is just based on aligning the pixels, i.e. we are not actually using a full WCS, since these are spectra.

So the wavelength value for a given pixel at (X, Y) (data coords) is just the value of the wavelength array at (X, Y)?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/ejeschke/ginga/issues/842#issuecomment-634352518, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACC6HI4WAYRHD4FQUWLVGFLRTRNPDANCNFSM4NKEOG2A .

--

Joseph F. Hennawi Associate Professor Department of Physics Broida Hall, UC Santa Barbara Santa Barbara, CA 93106-9530 Phone: 805-893-3503 Mobile: 805-450-8697 E-mail: joe@ joe@physics.ucsb.eduphysics.ucsb.edu http://web.physics.ucsb.edu/~joe/ enigma.physics.ucsb.edu

ejeschke commented 4 years ago

@jhennawi, do you want both the Info (to the left) panel and the Cursor panel (on the bottom) to read the same way? Will you be mixing viewing of spectra and camera images in the same viewer?

jhennawi commented 4 years ago

Hi Eric,

It would be convenient to have a Wavelength or Wave entry on both the left (Info) and the bottom (Cursor) panels. We will not be mixing camera and spectra in the same viewer for the present, so having these ready consistently is better I think. If we have multiple images next to each other in channels now they would all be spectra. I can imagine a future application with IFU pseudo images and Camera images next to each other in different channels where it would be nice to know the wavelength of the IFU pseudo image in the image cube, but we are nowhere near there yet.

Joe

On Wed, May 27, 2020 at 3:27 PM ejeschke notifications@github.com wrote:

@jhennawi https://github.com/jhennawi, do you want both the Info (to the left) panel and the Cursor panel (on the bottom) to read the same way? Will you be mixing viewing of spectra and camera images in the same viewer?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/ejeschke/ginga/issues/842#issuecomment-634976665, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACC6HIYBH2B4TOMESVVGHSDRTWHVBANCNFSM4NKEOG2A .

--

Joseph F. Hennawi Associate Professor Department of Physics Broida Hall, UC Santa Barbara Santa Barbara, CA 93106-9530 Phone: 805-893-3503 Mobile: 805-450-8697 E-mail: joe@ joe@physics.ucsb.eduphysics.ucsb.edu http://web.physics.ucsb.edu/~joe/ enigma.physics.ucsb.edu

ejeschke commented 4 years ago

@jhennawi, this sounds pretty simple, I think. I've long wanted to refactor the way that the cursor readout is handled, because I think this won't be the last time we'll want to customize it. I am now thinking about the best way to go about that. From the PR I'll link back to this issue.

jhennawi commented 4 years ago

All good Eric, many thanks!

On Mon, Jun 1, 2020 at 4:10 PM ejeschke notifications@github.com wrote:

@jhennawi https://github.com/jhennawi, this sounds pretty simple, I think. I've long wanted to refactor the way that the cursor readout is handled, because I think this won't be the last time we'll want to customize it. I am now thinking about the best way to go about that. From the PR I'll link back to this issue.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/ejeschke/ginga/issues/842#issuecomment-637173016, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACC6HI5HD6ADPRGM3I3JGG3RUQYN7ANCNFSM4NKEOG2A .

--

Joseph F. Hennawi Associate Professor Department of Physics Broida Hall, UC Santa Barbara Santa Barbara, CA 93106-9530 Phone: 805-893-3503 Mobile: 805-450-8697 E-mail: joe@ joe@physics.ucsb.eduphysics.ucsb.edu http://web.physics.ucsb.edu/~joe/ enigma.physics.ucsb.edu

ejeschke commented 4 years ago

Hi Joe. So after thinking about this request for a little bit I think it can be pretty easily handled via the existing logic if you are willing to launch Ginga with a custom plugin.

Here is a custom plugin called "Pypelt":

import numpy as np

from ginga import GingaPlugin
from ginga.AstroImage import AstroImage

class PypeltImage(AstroImage):
    """
    Custom image type for Pypelt
    """
    def __init__(self, wav_np=None, **kwargs):

        AstroImage.__init__(self, **kwargs)

        self.wav_np = wav_np

    def info_xy(self, data_x, data_y, settings):
        info = super(PypeltImage, self).info_xy(data_x, data_y, settings)

        try:
            # We report the value across the pixel, even though the coords
            # change halfway across the pixel
            _d_x, _d_y = (int(np.floor(data_x + 0.5)),
                          int(np.floor(data_y + 0.5)))

            _ht, _wd = self.wav_np.shape
            if 0 <= _d_y < _ht and 0 <= _d_x < _wd:
                # spectral wavelength is stored in auxillary array
                wavelength = self.wav_np[_d_y, _d_x]
                # choose your best formatting here...
                wav_s = "{:<14.6g}".format(wavelength)
            else:
                wav_s = ''

            info.update(dict(ra_lbl="\u03bb", ra_txt=wav_s,
                             dec_lbl='', dec_txt=''))

        except Exception as e:
            self.logger.error("Error getting wavelength value: {}".format(e),
                              exc_info=True)

        return info

class Pypelt(GingaPlugin.GlobalPlugin):

    def __init__(self, fv):
        super(Pypelt, self).__init__(fv)

    def load_buffer(self, imname, chname, img_buf, dims, dtype,
                    header, wav_buf, wav_dtype, metadata):
        """Display a FITS image buffer.

        Parameters
        ----------
        imname : string
            a name to use for the image in Ginga
        chname : string
            channel in which to load the image
        img_buf : bytes
            the image data, as a buffer
        dims : tuple
            image dimensions in pixels (usually (height, width))
        dtype : string
            numpy data type of encoding (e.g. 'float32')
        header : dict
            fits file header as a dictionary
        wav_buf : bytes
            the wavelength data, as a buffer
        wav_dtype : string
            numpy data type of wav_buf array encoding (e.g. 'float32')
        metadata : dict
            other metadata about image to attach to image

        Returns
        -------
        0

        Notes
        -----

        * Get array dims: data.shape
        * Get array dtype: str(data.dtype)
        * Make a string from a numpy array: buf = grc.Blob(data.tobytes())

        """
        self.logger.info("received image data len=%d" % (len(img_buf)))

        # Unpack the data
        try:
            # dtype string works for most instances
            if dtype == '':
                dtype = np.float

            byteswap = metadata.get('byteswap', False)

            # unpack the auxillary wavelength file
            data = np.fromstring(wav_buf, dtype=wav_dtype)
            if byteswap:
                data.byteswap(True)
            wav_np = data.reshape(dims)

            # Create image container
            image = PypeltImage(logger=self.logger, wav_np=wav_np)
            image.load_buffer(img_buf, dims, dtype, byteswap=byteswap,
                              metadata=metadata)
            image.update_keywords(header)
            image.set(name=imname, path=None)

        except Exception as e:
            # Some kind of error unpacking the data
            errmsg = "Error creating image data for '%s': %s" % (
                imname, str(e))
            self.logger.error(errmsg)
            raise GingaPlugin.PluginError(errmsg)

        # Display the image
        channel = self.fv.gui_call(self.fv.get_channel_on_demand, chname)

        # Note: this little hack needed to let window resize in time for
        # file to auto-size properly
        self.fv.gui_do(self.fv.change_channel, channel.name)

        self.fv.gui_do(self.fv.add_image, imname, image,
                       chname=channel.name)
        return 0

    def __str__(self):
        return 'pypelt'

If you save this as a file in $HOME/.ginga/plugins/Pypelt.py, and invoke ginga by:

$ ginga --loglevel=20 --stderr --modules=Pypelt,RC

you can then load your file like so from Python:

sh = viewer.shell()
# image name "foo", channel "Image", data is ndarray of float, aux is wavelength data of same
# dimensions (also float), d is a dictionary of FITS header keys and values
args = ["foo", "Image", grc.Blob(data.tobytes()), data.shape, 'float', d, grc.Blob(aux.tobytes()), 'float', {}]
sh.call_global_plugin_method('Pypelt', 'load_buffer', args, {})
ejeschke commented 4 years ago

The good thing about this solution is that you will have established a "beachhead" in Ginga with your own plugin. You can then begin to add more methods or even a GUI, a plugin settings configuration, etc.

jhennawi commented 4 years ago

Great! We will give this a try and get back to you.

Thanks, Joe

On Mon, Jun 22, 2020 at 4:23 PM ejeschke notifications@github.com wrote:

Hi Joe. So after thinking about this request for a little bit I think it can be pretty easily handled via the existing logic if you are willing to launch Ginga with a custom plugin.

Here is a custom plugin called "Pypelt":

import numpy as np from ginga import GingaPluginfrom ginga.AstroImage import AstroImage

class PypeltImage(AstroImage): """ Custom image type for Pypelt """ def init(self, wav_np=None, **kwargs):

    AstroImage.__init__(self, **kwargs)

    self.wav_np = wav_np

def info_xy(self, data_x, data_y, settings):
    info = super(PypeltImage, self).info_xy(data_x, data_y, settings)

    try:
        # We report the value across the pixel, even though the coords
        # change halfway across the pixel
        _d_x, _d_y = (int(np.floor(data_x + 0.5)),
                      int(np.floor(data_y + 0.5)))

        _ht, _wd = self.wav_np.shape
        if 0 <= _d_y < _ht and 0 <= _d_x < _wd:
            # spectral wavelength is stored in auxillary array
            wavelength = self.wav_np[_d_y, _d_x]
            # choose your best formatting here...
            wav_s = "{:<14.6g}".format(wavelength)
        else:
            wav_s = ''

        info.update(dict(ra_lbl="\u03bb", ra_txt=wav_s,
                         dec_lbl='', dec_txt=''))

    except Exception as e:
        self.logger.error("Error getting wavelength value: {}".format(e),
                          exc_info=True)

    return info

class Pypelt(GingaPlugin.GlobalPlugin):

def __init__(self, fv):
    super(Pypelt, self).__init__(fv)

def load_buffer(self, imname, chname, img_buf, dims, dtype,
                header, wav_buf, wav_dtype, metadata):
    """Display a FITS image buffer.        Parameters        ----------        imname : string            a name to use for the image in Ginga        chname : string            channel in which to load the image        img_buf : bytes            the image data, as a buffer        dims : tuple            image dimensions in pixels (usually (height, width))        dtype : string            numpy data type of encoding (e.g. 'float32')        header : dict            fits file header as a dictionary        wav_buf : bytes            the wavelength data, as a buffer        wav_dtype : string            numpy data type of wav_buf array encoding (e.g. 'float32')        metadata : dict            other metadata about image to attach to image        Returns        -------        0        Notes        -----        * Get array dims: data.shape        * Get array dtype: str(data.dtype)        * Make a string from a numpy array: buf = grc.Blob(data.tobytes())        """
    self.logger.info("received image data len=%d" % (len(img_buf)))

    # Unpack the data
    try:
        # dtype string works for most instances
        if dtype == '':
            dtype = np.float

        byteswap = metadata.get('byteswap', False)

        # unpack the auxillary wavelength file
        data = np.fromstring(wav_buf, dtype=wav_dtype)
        if byteswap:
            data.byteswap(True)
        wav_np = data.reshape(dims)

        # Create image container
        image = PypeltImage(logger=self.logger, wav_np=wav_np)
        image.load_buffer(img_buf, dims, dtype, byteswap=byteswap,
                          metadata=metadata)
        image.update_keywords(header)
        image.set(name=imname, path=None)

    except Exception as e:
        # Some kind of error unpacking the data
        errmsg = "Error creating image data for '%s': %s" % (
            imname, str(e))
        self.logger.error(errmsg)
        raise GingaPlugin.PluginError(errmsg)

    # Display the image
    channel = self.fv.gui_call(self.fv.get_channel_on_demand, chname)

    # Note: this little hack needed to let window resize in time for
    # file to auto-size properly
    self.fv.gui_do(self.fv.change_channel, channel.name)

    self.fv.gui_do(self.fv.add_image, imname, image,
                   chname=channel.name)
    return 0

def __str__(self):
    return 'pypelt'

If you save this as a file in $HOME/.ginga/plugins/Pypelt.py, and invoke ginga by:

$ ginga --loglevel=20 --stderr --modules=Pypelt,RC

you can then load your file like so from Python:

sh = viewer.shell()# image name "foo", channel "Image", data is ndarray of float, aux is wavelength data of same# dimensions (also float), d is a dictionary of FITS header keys and valuesargs = ["foo", "Image", grc.Blob(data.tobytes()), data.shape, 'float', d, grc.Blob(aux.tobytes()), 'float', {}]sh.call_global_plugin_method('Pypelt', 'load_buffer', args, {})

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/ejeschke/ginga/issues/842#issuecomment-647817706, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACC6HIZ3RHQAOYEN7OTK65TRX7RXPANCNFSM4NKEOG2A .

--

Joseph F. Hennawi Associate Professor Department of Physics Broida Hall, UC Santa Barbara Santa Barbara, CA 93106-9530 Phone: 805-893-3503 Mobile: 805-450-8697 E-mail: joe@ joe@physics.ucsb.eduphysics.ucsb.edu http://web.physics.ucsb.edu/~joe/ enigma.physics.ucsb.edu

jhennawi commented 4 years ago

Dear @ejeschke,

I finally had a chance to implement this. It works beautifully -- many thanks for your help. The one question I have is whether it is possible for the plugin to live somewhere else besides $HOME/.ginga/plugins/Pypelt.py, and if we can somehow tell ginga to look elsewhere on launch.

The issue is that PypeIt is pip installable etc. and we don't think it is appropriate (may also not be possible) to automatically install a file in someone's home directory. We would prefer the plugin file to reside in the PypeIt code directory, and tell ginga to look in a different place via launch. I understand this may not be possible, but I thought I would just ask.

Thanks! Joe Hennawi

ejeschke commented 4 years ago

@jhennawi, the plugin module simply needs to be in the import path. Are you launching the reference viewer from your application, or is it expected to be launched by the user independently? If you are launching it, simply set the PYTHONPATH to include the directory where the plugin lives--and that can be in your PyPelt install area (or wherever you decide to put the plugin).

If you'd prefer the user to be able to launch the reference viewer and find your custom plugin, I'd recommend using the custom plugin template. The instructions specify how to make a standalone install package, but you could adapt them to doing the install as part of the PyPelt install without having a separate package, etc.

jhennawi commented 4 years ago

Hi @ejeschke,

We usually have the user launch ginga themselves, but if a script that needs ginga is run and ginga is not yet launched, then we launch if for them. I'll take a look at the custom plugin template, and get back to you. Many thanks for your help with this.