Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
jhennawi opened this issue May 26, 2020 · 15 comments

Comments

@jhennawi
Copy link

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
Copy link
Collaborator

pllim commented May 26, 2020

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
Copy link
Owner

@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
Copy link
Author

jhennawi commented May 27, 2020 via email

@ejeschke
Copy link
Owner

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
Copy link
Author

jhennawi commented May 27, 2020 via email

@ejeschke
Copy link
Owner

@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
Copy link
Author

jhennawi commented May 28, 2020 via email

@ejeschke
Copy link
Owner

ejeschke commented Jun 1, 2020

@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
Copy link
Author

jhennawi commented Jun 1, 2020 via email

@ejeschke
Copy link
Owner

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
Copy link
Owner

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
Copy link
Author

jhennawi commented Jun 22, 2020 via email

@jhennawi
Copy link
Author

jhennawi commented Aug 2, 2020

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
Copy link
Owner

ejeschke commented Aug 2, 2020

@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
Copy link
Author

jhennawi commented Aug 3, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants