Skip to content
Snippets Groups Projects
Select Git revision
  • 2e6d4167ee40c2dd4fcef95ff8135d1b77ce0027
  • master default protected
  • 551-init-broker-service-permissions
  • dev protected
  • release-1.10 protected
  • 549-test-oai-pmh
  • 545-saving-multiple-times-breaks-pid-metadata
  • release-1.9 protected
  • 499-standalone-compute-service-2
  • 539-load-tests
  • hotfix/helm-chart
  • luca_ba_new_interface
  • 534-bug-when-adding-access-to-user-that-is-not-registered-at-dashboard-service
  • release-1.8 protected
  • 533-integrate-semantic-recommendation
  • feature/openshift
  • 518-spark-doesn-t-map-the-headers-correct
  • 485-fixity-checks
  • 530-various-schema-problems-with-subsets
  • release-1.7 protected
  • fix/auth-service
  • v1.10.1 protected
  • v1.10.0-rc13 protected
  • v1.10.0-rc12 protected
  • v1.10.0-rc11 protected
  • v1.10.0-rc10 protected
  • v1.10.0-rc9 protected
  • v1.10.0-rc8 protected
  • v1.10.0-rc7 protected
  • v1.10.0-rc6 protected
  • v1.10.0-rc5 protected
  • v1.10.0-rc4 protected
  • v1.10.0-rc3 protected
  • v1.10.0-rc2 protected
  • v1.10.0rc1 protected
  • v1.10.0rc0 protected
  • v1.10.0 protected
  • v1.9.3 protected
  • v1.9.2 protected
  • v1.9.2-rc0 protected
  • v1.9.1 protected
41 results

query.js

Blame
  • imageviewer_ce.py 34.02 KiB
    #!/usr/bin/env python3
    
    import sys
    import gi
    
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk, Gdk, GdkPixbuf, GLib
    
    import matplotlib
    
    matplotlib.use('Gtk3Cairo')
    from matplotlib.pyplot import colormaps
    from matplotlib.colors import Normalize, LogNorm, PowerNorm
    from matplotlib.figure import Figure
    from matplotlib.widgets import SpanSelector
    # from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
    from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
    from matplotlib.backends.backend_gtk3 import NavigationToolbar2GTK3 as NavigationToolbar
    
    import numpy as np
    import astropy.io.fits as pyfits
    import configparser
    import glob
    import threading
    import multiprocessing
    import time
    from event_storm_squasher import delayed
    
    
    # from event_storm_squasher import delayed
    
    
    class ImageViewer(Gtk.Window):
        default_config = 'egse.cfg'
        pixzoom = 10
        cmap = 'viridis'
        normalisations = {'LINEAR': Normalize, 'LOG': LogNorm, 'POWER': PowerNorm, 'SQRT': PowerNorm}
    
        window_sizes = {'CCD_FULL': (1076, 1033), 'CCD_WIN_EXPOS': (200, 200),
                        'CCD_WIN_LSM': (200, 28), 'CCD_WIN_RSM': (200, 24), 'CCD_WIN_TM': (9, 200)}
    
        refresh_interval = 1
        histrange = (0, 2 ** 16 - 1)
        image_interpolation = 'none'
    
        def __init__(self, cfg=None, directory="./", prefix=""):
            Gtk.Window.__init__(self, title="Image Viewer (watching {}*)".format(directory + prefix), default_height=900,
                                default_width=1200)
            self.set_border_width(5)
            self.set_resizable(True)
            self.set_position(Gtk.WindowPosition.CENTER)
    
            if cfg is not None:
                self.cfg = cfg
            else:
                self.cfg = configparser.ConfigParser()
                self.cfg.read(self.default_config)
                self.cfg.source = self.default_config
    
            self.prefix = prefix
            self.dir_watcher_active = False
            self.autoscroll = 0
            self.slice_count = 2
    
            self.fixed_cuts = False
            self.keep_regions = False
    
            self.region_size = 20
            self.region_shape = 'square'
            self.region_cursor = None
            self.region_stats = {}
            self.region_store = {}
    
            self.watched_directory = directory if directory.endswith('/') else directory + '/'
            self.instore = multiprocessing.Manager().list()
    
            self.fileview = self.create_fileview()
            self.headerview = self.create_headerview()
            self.headerbuffer = self.headerview.get_child().get_buffer()
            self.start_dir_watch(self.watched_directory)
            self.layer_slice = 0
            self.scaled = False
    
            if len(self.instore) == 0:
                self.loaded_fits = None
            else:
                self.load_fits(self.instore[0])
            self.canvas = self.create_canvas()
            # self.statbar = NavigationToolbar(self.canvas, self)
            self.statbar = NavBar(self.canvas, self)
            self.slider = self.create_slider()
    
            clippingbox = Gtk.VBox()
            self.histcanvas = self.create_histogram()
            cutsbox = Gtk.HBox()
            self.minbox, self.maxbox = Gtk.Entry(xalign=1), Gtk.Entry(xalign=1)
            self.vmin, self.vmax = 0, 2 ** 24 - 1
            self.minline, self.maxline = None, None
            self.minbox.connect('activate', self.update_cuts)
            self.maxbox.connect('activate', self.update_cuts)
            self.minbox.set_text('{:.5f}'.format(self.vmin))
            self.maxbox.set_text('{:.5f}'.format(self.vmax))
            cutsbox.pack_start(self.minbox, 1, 0, 0)
            cutsbox.pack_end(self.maxbox, 1, 0, 0)
            cutsbox.set_spacing(5)
            clippingbox.pack_start(self.histcanvas, 1, 1, 0)
            clippingbox.pack_start(cutsbox, 0, 0, 0)
            clippingbox.set_spacing(5)
    
            self.zoom_view = self.create_zoom_view()
            self.stat_settings = self.create_stat_settings()
            self.stat_view = self.create_stat_view()
    
            grid = Gtk.VBox()
            grid.pack_start(self.zoom_view, 1, 1, 0)
            grid.pack_start(self.stat_settings, 1, 1, 0)
            grid.pack_start(self.stat_view, 1, 0, 0)
    
            self.plot_bar = self.create_plot_bar()
            box = Gtk.VBox()
            box.pack_start(self.plot_bar, 0, 1, 0)
            box.pack_start(self.canvas, 1, 1, 0)
            box.pack_start(self.slider, 0, 0, 0)
            box.pack_start(clippingbox, 0, 1, 0)
            box.pack_start(self.statbar, 0, 0, 0)
    
            hbox = Gtk.HBox()
            # hbox.pack_start(self.fileview, 1, 1, 5)
            paned2 = Gtk.HPaned(wide_handle=True)
            paned2.pack1(self.fileview, True, True)
            paned2.pack2(hbox, True, False)
            hbox.pack_start(box, 1, 1, 0)
            hbox.pack_start(grid, 0, 1, 0)
            paned = Gtk.HPaned(wide_handle=True)
            paned.pack1(paned2, True, True)
            # self.headerview.set_size_request(300, -1)
            paned.pack2(self.headerview, True, True)
            # hbox.pack_start(self.headerview, 1, 1, 5)
            # hbox.pack_start(paned, 1, 1, 5)
            self.add(paned)
    
            # self.connect('delete-event',Gtk.main_quit)
            self.canvas.set_size_request(400, -1)
            self.canvas.draw_idle()
    
            self.show_all()
    
        def create_plot_bar(self):
            plot_bar = Gtk.HBox()
    
            cmap_box = Gtk.ComboBoxText()
            cmap_store = Gtk.ListStore(str)
            [cmap_store.append([c]) for c in colormaps()[::2]]
            cidx = {i[0]: j for j, i in enumerate(cmap_store)}['viridis']
            cmap_box.set_model(cmap_store)
            cmap_box.set_wrap_width(5)
            cmap_box.connect('changed', self.set_colormap)
            cmap_box.set_active(cidx)
    
            scaling = Gtk.ComboBoxText()
            scalings = Gtk.ListStore(str)
            for norm in self.normalisations:
                scalings.append([norm])
            scaling.set_model(scalings)
            scaling.connect('changed', self.set_scaling)
            # scaling.set_active(0)
    
            layers = Gtk.ListStore(str)
            if self.loaded_fits:
                for hdu in self.loaded_fits[1:]:
                    layers.append([hdu.name])
            self.layerbox = Gtk.ComboBoxText(model=layers)
            self.layerbox.connect('changed', self.set_layer)
    
            plot_bar.pack_start(cmap_box, 1, 1, 2)
            plot_bar.pack_start(scaling, 1, 1, 2)
            plot_bar.pack_start(self.layerbox, 1, 1, 2)
    
            return plot_bar
    
        def create_canvas(self, img=None):
            dpi = 96
            margin = 0.025
            xpixels, ypixels = 200, 200
            figsize = (1 + margin) * ypixels / dpi, (1 + margin) * xpixels / dpi
    
            fig = Figure(figsize=figsize, dpi=dpi)
    
            # Make the axis the right size...
            self.ax = fig.add_axes([margin, margin, 1 - 2 * margin, 1 - 2 * margin])
            # self.ax = fig.add_subplot(111)
            if self.loaded_fits is not None:
                self.imview = self.ax.imshow(self.loaded_fits[self.layer].data, interpolation='none', cmap=self.cmap)
            else:
                self.imview = self.ax.imshow(np.zeros((200, 200)), interpolation='none', cmap=self.cmap)
            self.ax.xaxis.set_visible(False)
            self.ax.yaxis.set_visible(False)
    
            # self.ax.format_coord = Formatter(self.imview)
    
            canvas = FigureCanvas(fig)
            canvas.mpl_connect('motion_notify_event', self.update_zoom)
            canvas.mpl_connect('motion_notify_event', self.set_pixel_stats)
            canvas.mpl_connect('motion_notify_event', self.update_region_cursor)
            canvas.mpl_connect('button_press_event', self.store_region_stats)
            canvas.mpl_connect('axes_leave_event', self.clear_region_cursor)
            # canvas.mpl_connect('pick_event', self.on_pick)
    
            canvas.figure.subplots_adjust(left=0.02, right=0.98)
            # canvas.set_size_request(300, 600)
            return canvas
    
        def create_fileview(self):
            self.filestore = Gtk.ListStore(str, str)
            view = Gtk.TreeView(model=self.filestore)
            render = Gtk.CellRendererText()
            column = Gtk.TreeViewColumn("FITS files", render, text=0)
            column.set_resizable(True)
            view.append_column(column)
            column = Gtk.TreeViewColumn("FITS files", render, text=1)
            column.set_visible(False)
            view.append_column(column)
    
            self.selection = view.get_selection()
            self.selection.connect("changed", self.set_view)
            # self.selection.connect("changed", self.update_slice_count)
    
            sv = Gtk.ScrolledWindow()
            sv.add(view)
            view.connect('size-allocate', self.treeview_update)
            sv.connect('edge-overshot', self.edge_reached)
            sv.connect('edge-reached', self.edge_reached)
            sv.connect('scroll-event', self.scroll_event)
            view.connect('key-press-event', self.edge_reached)
            sv.get_vscrollbar().connect('value-changed', self.disable_autoscroll)
    
            check = Gtk.CheckButton(label="Keep cut levels fixed")
            check.connect("toggled", self._set_fixed_cuts)
            check.set_active(self.fixed_cuts)
    
            statreg = Gtk.CheckButton(label="Keep statistics regions")
            statreg.connect("toggled", self._set_keep_regions)
            statreg.set_active(self.keep_regions)
    
            button = Gtk.Button(label='Change directory')
            button.connect("clicked", self._on_change_watched_dir)
    
            box = Gtk.VBox(spacing=3)
            box.pack_start(sv, 1, 1, 0)
            box.pack_start(check, 0, 0, 0)
            box.pack_start(statreg, 0, 0, 0)
            box.pack_start(button, 0, 0, 0)
            return box
    
        def _set_fixed_cuts(self, widget):
            self.fixed_cuts = widget.get_active()
    
        def _set_keep_regions(self, widget):
            self.keep_regions = widget.get_active()
    
        def create_headerview(self):
            headerview = Gtk.TextView()
            headerview.set_property("cursor-visible", False)
            headerview.set_property("monospace", True)
            sv = Gtk.ScrolledWindow()
            sv.add(headerview)
            return sv
    
        def disable_autoscroll(self, widget=None, event=None):
            self.autoscroll = 0
    
        def treeview_update(self, widget, event, data=None):
            if self.autoscroll:
                adj = widget.get_vadjustment()
                adj.set_value(adj.get_upper() - adj.get_page_size())
    
                selection = widget.get_selection()
                last = self.get_last_item(widget.get_model())
                if isinstance(last, Gtk.TreeIter):
                    selection.select_iter(last)
    
        def edge_reached(self, widget, event, data=None):
            if hasattr(event, 'value_name'):
                if event.value_name == 'GTK_POS_BOTTOM':
                    self.autoscroll = 1
            if hasattr(event, 'keyval'):
                if event.keyval == Gdk.KEY_End:
                    self.autoscroll = 1
    
        def scroll_event(self, widget, event, data=None):
            # disable autoscroll on scrollwheel up
            if event.get_scroll_deltas()[2] == -1:
                self.autoscroll = 0
    
        def get_last_item(self, model, parent=None):
            n = model.iter_n_children(parent)
            return n and model.iter_nth_child(parent, n - 1)
    
        def load_fits(self, fname):
            self.loaded_fits = pyfits.open(fname)
    
        def calculate_cuts(self, imgdata, ns=2):
            median, sigma = np.median(imgdata), imgdata.std()
            return median - ns * sigma, median + ns * sigma
    
        def set_view(self, widget=None):
            if widget is None:
                layer = self.layerbox.get_active_text()
                if self.loaded_fits[layer].header['NAXIS'] == 3:
                    self.imview.set_data(self.loaded_fits[layer].data[self.layer_slice, :, :])
                else:
                    self.imview.set_data(self.loaded_fits[layer].data)
                self.update_zoom()
                if not self.fixed_cuts:
                    self.set_scaling()
                self.restore_regions()
                self.canvas.draw_idle()
                self.update_histogram()
                self.set_clip_line()
                return
            model, treepath = widget.get_selected_rows()
            if len(model) == 0:
                return
            fname = model[treepath][1]
            self.load_fits(fname)
            layer = self.layerbox.get_active_text()
            layers = self.layerbox.get_model()
            layers.clear()
            for hdu in self.loaded_fits:
                if hdu.is_image and hdu.data is not None:
                    layers.append([hdu.name])
            if layer is None or layer not in [lay[0] for lay in layers]:
                self.layerbox.set_active(0)
            else:
                layer_row = [lay for lay in layers if lay[0] == layer][0]
                self.layerbox.set_active_iter(layer_row.iter)
            self.set_header_view()
            layer = self.layerbox.get_active_text()
            self.update_slice_count()
            self.ax.clear()
            if self.loaded_fits[layer].header['NAXIS'] == 3:
                imgdata = self.loaded_fits[layer].data[self.layer_slice, :, :]
            else:
                imgdata = self.loaded_fits[layer].data
            if not self.fixed_cuts:
                self.vmin, self.vmax = self.calculate_cuts(imgdata)
            self.imview = self.ax.imshow(imgdata, interpolation=self.image_interpolation, cmap=self.cmap,
                                         vmin=self.vmin, vmax=self.vmax, filterrad=1.)
            # self.imview.set_data(self.loaded_fits[layer].data[self.layer_slice, :, :])
            self.ax.set_frame_on(False)
            if not self.fixed_cuts:
                self.set_scaling()
            self.restore_regions()
            self.canvas.draw_idle()
            self.update_histogram()
            self.set_clip_line()
    
        def set_header_view(self):
            head = self.loaded_fits[0].header.tostring()
            header = "\n".join([head[i: i + 80] for i in range(0, len(head), 80)]).strip()
            self.headerbuffer.set_text(header, len(header))
    
        def set_layer(self, widget=None, layer=None):
            layer = widget.get_active_text()
            if layer is None:
                return
            try:
                if self.loaded_fits[layer].header['NAXIS'] == 3:
                    imgdata = self.loaded_fits[layer].data[self.layer_slice, :, :]
                else:
                    imgdata = self.loaded_fits[layer].data
            except IndexError:
                return
            self.update_slice_count()
            # imgheader = self.loaded_fits[layer].header
            # self.imview.remove()
            self.ax.clear()
            if not self.fixed_cuts:
                self.vmin, self.vmax = self.calculate_cuts(imgdata)
            self.imview = self.ax.imshow(imgdata, interpolation=self.image_interpolation, cmap=self.cmap, vmin=self.vmin,
                                         vmax=self.vmax)
            self.restore_regions()
            self.canvas.draw_idle()
            self.update_histogram()
            self.set_clip_line()
    
        def set_colormap(self, widget=None):
            self.cmap = widget.get_active_text()
            self.ax.images[0].set_cmap(self.cmap)
            self.zoomdata.set_cmap(self.cmap)
            self.canvas.draw_idle()
            return
    
        def set_scaling(self, widget=None):
            if widget is None:
                widget = self.layerbox.get_parent().get_children()[1]
            scale = widget.get_active_text()
            if scale is None:
                widget.set_active(0)
                scale = widget.get_active_text()
            if scale in ['LINEAR', 'LOG']:
                self.ax.images[0].set_norm(self.normalisations[scale]())
            elif scale in ['POWER']:
                self.ax.images[0].set_norm(self.normalisations[scale](2))
            elif scale in ['SQRT']:
                self.ax.images[0].set_norm(self.normalisations[scale](-2))
    
            if scale in ['LINEAR', 'LOG']:
                self.zoomdata.set_norm(self.normalisations[scale]())
            elif scale in ['POWER']:
                self.zoomdata.set_norm(self.normalisations[scale](2))
            elif scale in ['SQRT']:
                self.zoomdata.set_norm(self.normalisations[scale](-2))
            self.update_cuts(minmax=(self.vmin, self.vmax))
            self.canvas.draw_idle()
    
        def create_slider(self):
            box = Gtk.HBox()
            label = Gtk.Label()
    
            size = self.slice_count
            slider = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0, size - 1, 1)
            slider.set_value_pos(Gtk.PositionType.LEFT)
            slider.set_digits(0)
            # slider.set_draw_value(False)
    
            # slider.connect('value-changed', self.update_timestamp, label)
            slider.connect('value-changed', self.set_slice)
    
            box.pack_start(slider, 1, 1, 5)
            box.pack_start(label, 0, 0, 0)
            return box
    
        def update_timestamp(self, widget=None, label=None):
            # label.set_text(self.datacube[int(widget.get_value())]['TIMESTAMP'])
            try:
                label.set_text(str(self.loaded_fits[self.layerbox.get_active_text()].header['NAXIS3']))
            except KeyError:
                label.set_text('---')
    
        def set_slice(self, widget=None, index=None):
            if (widget is None) and (index is None):
                return
            if index is None:
                index = int(widget.get_value())
            self.layer_slice = index
            self.set_view()
            if widget is not None:
                widget.show_all()
    
        def update_slice_count(self, widget=None):
            size = self.loaded_fits[self.layerbox.get_active_text()].data.shape[0]
            naxis = self.loaded_fits[self.layerbox.get_active_text()].header['NAXIS']
            slider, label = self.slider.get_children()
            if size == 1 or naxis == 2:
                slider.set_range(0, size - 1)
                slider.set_sensitive(False)
                self.set_slice(index=0)
                self.update_timestamp(label=label)
            else:
                slider.set_sensitive(True)
                slider.set_range(0, size - 1)
                slider.set_value(0)
                slider.set_draw_value(False)
                slider.set_draw_value(True)
                self.set_slice(index=0)
                self.update_timestamp(label=label)
    
        def create_zoom_view(self):
            fig = Figure()
            zoomplot = fig.add_subplot(111, frameon=False)
            zoomplot.xaxis.set_visible(False)
            zoomplot.yaxis.set_visible(False)
            try:
                ref_image = self.ax.get_images()[0].get_array()
            except IndexError:
                ref_image = np.zeros((200, 200))
            xc, yc = [x // 2 for x in ref_image.shape]
            self.zoomdata = zoomplot.imshow(
                ref_image[yc - self.pixzoom:yc + self.pixzoom, xc - self.pixzoom:xc + self.pixzoom],
                interpolation='none', cmap=self.cmap)
            zoom_view = FigureCanvas(fig)
    
            return zoom_view
    
        def update_zoom(self, event=None):
            if event is None:
                image = self.imview.get_array()
                self.zoomdata.set_data(image[0:2 * self.pixzoom, 0:2 * self.pixzoom])
                self.set_pixel_stats(event)
                self.zoom_view.draw_idle()
                return
            if (event.xdata or event.ydata) is None:
                return
            x, y = int(round(event.xdata)), int(round(event.ydata))
            image = event.inaxes.get_images()[0].get_array()
            if y < self.pixzoom:
                y = self.pixzoom
            elif y > (image.shape[0] - self.pixzoom):
                y = image.shape[0] - self.pixzoom
            if x < self.pixzoom:
                x = self.pixzoom
            elif x > (image.shape[1] - self.pixzoom):
                x = image.shape[1] - self.pixzoom
            self.zoomdata.set_data(image[y - self.pixzoom:y + self.pixzoom, x - self.pixzoom:x + self.pixzoom])
            self.zoomdata.set_clim(self.vmin, self.vmax)
            self.zoom_view.draw_idle()
    
        def update_region_cursor(self, event=None):
            if (event.xdata or event.ydata) is None:
                return
            x, y = event.xdata, event.ydata
            image = event.inaxes.get_images()[0].get_array()
            bdist = self.region_size / 2
            # if round(y) < bdist:
            #     y = bdist - 0.5
            # elif round(y) > (image.shape[0] - bdist):
            #     y = image.shape[0] - bdist - 0.5
            # if round(x) < bdist:
            #     x = bdist - 0.5
            # elif round(x) > (image.shape[1] - bdist):
            #     x = image.shape[1] - bdist - 0.5
            self.chcursor(pos=(x, y), size=self.region_size, shape=self.region_shape)
    
        def chcursor(self, pos, size=20, shape='square', store=False):
            if not store:
                if self.region_cursor is not None:
                    try:
                        self.region_cursor.remove()
                    except ValueError:
                        self.region_cursor = None
            if shape == 'square':
                self.region_cursor = matplotlib.patches.Rectangle(
                    (round(pos[0]) - 0.5 - self.region_size // 2, round(pos[1]) - 0.5 - self.region_size // 2), size, size,
                    fc='none', ec='cyan', picker=15)
            elif shape == 'circle':
                self.region_cursor = matplotlib.patches.Ellipse(pos, size, size, fc='none', ec='cyan', picker=15)
            self.ax.add_patch(self.region_cursor)
            self.canvas.draw_idle()
            # pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size('pixmap/circle.svg', 40, 40)
            # win = self.canvas.get_window()
            # win.set_cursor(Gdk.Cursor.new_from_pixbuf(Gdk.Display.get_default(), pixbuf, 20, 20))
            return self.region_cursor
    
        def clear_region_cursor(self, widget=None):
            try:
                self.region_cursor.remove()
            except ValueError:
                self.region_cursor = None
            self.canvas.draw_idle()
    
        def on_pick(self, event):
            print(event.mouseevent)
            if event.artist.contains(event.mouseevent):
                event.artist.set_linewidth(3)
                self.canvas.draw_idle()
    
        def create_stat_view(self):
            buff = Gtk.TextBuffer(text='XPOS:\t{}\nYPOS:\t{}\nADU:\t{}\n'.format(None, None, None) +
                                       'SUM:\t{}\nNPIX:\t{}\nAVG:\t{}\nSTDDEV:\t{}'.format(None, None, None, None))
            stat_view = Gtk.TextView.new_with_buffer(buff)
            stat_view.set_property("cursor-visible", False)
            stat_view.set_property("monospace", True)
            return stat_view
    
        # @delayed(50)
        def set_pixel_stats(self, event, region=None):
            if event is None:
                x, y, z = 0, 0, 0
            elif (event.xdata or event.ydata) is None:
                return
            else:
                image = event.inaxes.get_images()[0].get_array()
                x, y = event.xdata, event.ydata
                region = self.create_mask_from_patch(x, y, image)
                x, y = int(round(x)), int(round(y))
                if not ((0 <= y < image.shape[0]) and (0 <= x < image.shape[1])):
                    return
                try:
                    z = image[y, x]
                except IndexError:
                    return
            buff = self.stat_view.get_buffer()
            buff.set_text('XPOS:\t{:9.0f}\nYPOS:\t{:9.0f}\nADU:\t{:9.0f}\n'.format(x, y, z) +
                          'SUM:\t{:9.0f}\nNPIX:\t{:9.0f}\nAVG:\t{:7.3E}\nSTD:\t{:7.3E}'.format(*self.pixel_stats(region)))
    
        def pixel_stats(self, region=None):
            if region is None or self.region_cursor is None:
                pixels = self.zoomdata.get_array()  # self.image[region]
            else:
                pixels = self.imview.get_array().copy()
                pixels.mask = region
            psum, npix, mean, stddev = pixels.sum(), pixels.count(), pixels.mean(), pixels.std()
            return psum, npix, mean, stddev
    
        def store_region_stats(self, event=None):
            if (event.xdata or event.ydata) is None:
                return
            if event.button == 2:
                image = self.imview.get_array()
                x, y = event.xdata, event.ydata
                cursor = self.chcursor(pos=(x, y), size=self.region_size, shape=self.region_shape, store=True)
                mask = self.create_mask_from_patch(x, y, image)
                stats = self.pixel_stats(mask)
                region_id = '{}_{:d}_x{:.0f}_y{:.0f}'.format(self.region_shape, self.region_size, x, y)
                self.region_store[region_id] = (cursor, mask)
                self.region_stats[region_id] = stats
                statstring = '\n\n'.join(['{}:\n{}'.format(region, ' | '.join(map(str, self.region_stats[region])))
                                          for region in self.region_stats])
                self.stat_store.get_buffer().set_text(statstring)
                print('HERE:', stats)
    
        def restore_regions(self, statstring=''):
            if self.keep_regions:
                for region in self.region_store:
                    cursor, mask = self.region_store[region]
                    # self.chcursor(pos=(x, y), size=size, shape=shape, store=True)
                    self.ax.add_patch(cursor)
                    stats = self.pixel_stats(mask)
                    self.region_stats[region] = stats
                    statstring = '\n\n'.join(['{}:\n{}'.format(region, ' | '.join(map(str, self.region_stats[region])))
                                              for region in self.region_stats])
                self.stat_store.get_buffer().set_text(statstring)
            else:
                self.region_store = {}
                self.region_stats = {}
                buf = self.stat_store.get_buffer()
                buf.delete(*buf.get_bounds())
    
        def create_mask_from_patch(self, x, y, image, patch=None):
            if not self.region_cursor:
                return
            # i, j = np.indices(image.shape)
            # mask = np.array([[not self.region_cursor.contains_point((x, y)) for x in j[0]] for y in i.T[0]])
            yi, xi = np.indices(image.shape)
            r = self.region_size / 2
            if self.region_shape == 'circle':
                mask = (xi - x) ** 2 + (yi - y) ** 2 > r ** 2
            elif self.region_shape == 'square':
                x, y = round(x), round(y)
                mask = ((x - r) <= xi) & (xi < (x + r)) & ((y - r) <= yi) & (yi < (y + r))
                np.invert(mask, mask)
            else:
                mask = np.zeros(image.shape, dtype=bool)
            return mask
    
        def _clear_axes(self, axes):
            axes.set_xticklabels(())
            axes.set_xticks(())
            axes.set_yticks(())
            axes.set_yticklabels(())
    
        def create_stat_settings(self):
            self.region_size_setter = Gtk.SpinButton.new_with_range(2, 200, 1)
            self.region_size_setter.set_value(self.region_size)
            self.region_size_setter.set_tooltip_text('Aperture size in pixels')
            self.region_size_setter.connect('value-changed', self.set_region_size)
            self.circbut = Gtk.RadioButton.new_with_label_from_widget(None, 'circle')
            self.circbut.set_tooltip_text('NO fractional pixels yet!')
            self.sqbut = Gtk.RadioButton.new_with_label_from_widget(self.circbut, 'square')
            self.sqbut.set_active(True)
            self.sqbut.connect('toggled', self.set_region_shape)
    
            grid = Gtk.Grid()
            grid.attach(self.circbut, 0, 0, 1, 1)
            grid.attach(self.sqbut, 0, 1, 1, 1)
            grid.attach(self.region_size_setter, 1, 0, 1, 2)
    
            self.stat_store = Gtk.TextView(monospace=True, cursor_visible=False)
            sv = Gtk.ScrolledWindow()
            sv.set_vexpand(True)
            sv.add(self.stat_store)
            grid.attach(sv, 0, 2, 2, 15)
    
            cbut = Gtk.Button(label='Clear stats')
            cbut.connect('clicked', self.clear_stat_hist)
            grid.attach(cbut, 0, 18, 2, 1)
    
            grid.set_row_spacing(2)
            return grid
    
        def clear_stat_hist(self, widget=None):
            self.region_stats = {}
            self.region_store = {}
            buf = self.stat_store.get_buffer()
            buf.delete(*buf.get_bounds())
            while len(self.ax.patches) > 0:
                for patch in self.ax.patches:
                    patch.remove()
            self.canvas.draw_idle()
    
        def set_region_size(self, widget=None):
            self.region_size = widget.get_value_as_int()
    
        def set_region_shape(self, widget=None):
            if widget.get_active():
                self.region_shape = 'square'
            else:
                self.region_shape = 'circle'
    
        def create_histogram(self):
            fig = Figure()
            self.histogram = fig.add_subplot(111)
            values = self.ax.get_images()[0].get_array().flatten()
            h = self.histogram.hist(values, bins='fd', normed=True, histtype='stepfilled')
            # self._clear_axes(self.histogram)
            self.vmin, self.vmax = h[1].min(), h[1].max()
            self.minline = self.histogram.axvline(self.vmin, color='green', lw=1)
            self.maxline = self.histogram.axvline(self.vmax, color='red', lw=1)
            histcanvas = FigureCanvas(fig)
            histcanvas.set_size_request(200, 100)
            histcanvas.figure.subplots_adjust(top=0.95, bottom=0.25)
            histcanvas.mpl_connect('button_press_event', self.set_clip_line)
            histcanvas.mpl_connect('scroll_event', self._zoom_histogram)
            # histcanvas.mpl_connect('motion_notify_event', self.printxy)
            return histcanvas
    
        def printxy(self, event):
            print(event.xdata, event.ydata, event.guiEvent)
    
        # @delayed(1000)
        def update_histogram(self):
            self.histogram.clear()
            values = self.ax.get_images()[0].get_array().flatten()
            self.histogram.hist(values, bins='fd', normed=True, histtype='stepfilled')
            # self._clear_axes(self.histogram)
            # self.set_clip_line(event=None)
            self.histcanvas.draw_idle()
    
        def _zoom_histogram(self, event):
            if event.step == 0:
                return
            xmin, xmax = self.histogram.get_xlim()
            xc = np.mean((event.xdata, np.mean((xmin, xmax)), np.mean((xmin, xmax))))
            dx = (xmax - xmin) * (1.5 * np.abs(event.step)) ** np.sign(-event.step)
            self.histogram.set_xlim(xc - dx / 2, xc + dx / 2)
            self.histcanvas.draw_idle()
    
        def _onselect_histogram(self, vmin, vmax):
            print(vmin, vmax)
            self.histogram.xlim(vmin, vmax)
            self.histcanvas.draw_idle()
    
        def update_cuts(self, widget=None, minmax=None):
            if minmax is not None:
                self.vmin, self.vmax = minmax
            else:
                try:
                    self.vmin, self.vmax = [float(entry.get_text()) for entry in widget.get_parent().get_children()]
                    if self.vmax < self.vmin:
                        self.vmax = self.vmin
                except ValueError:
                    return
            self.imview.set_clim((self.vmin, self.vmax))
            if not minmax:
                self.set_clip_line(event=None)
            self.canvas.draw_idle()
    
        def set_clip_line(self, event=None):
            if event is None:
                if isinstance(self.minline, matplotlib.lines.Line2D):
                    try:
                        self.minline.remove()
                    except ValueError:
                        pass
                self.minline = self.histogram.axvline(self.vmin, color='green', lw=1)
                self.minbox.set_text('{:.5f}'.format(self.vmin))
                if isinstance(self.maxline, matplotlib.lines.Line2D):
                    try:
                        self.maxline.remove()
                    except ValueError:
                        pass
                self.maxline = self.histogram.axvline(self.vmax, color='red', lw=1)
                self.maxbox.set_text('{:.5f}'.format(self.vmax))
                self.histcanvas.draw_idle()
                return
            if (event.xdata or event.ydata) is None:
                return
            x = event.xdata
            if event.button == 1:
                if x > self.vmax:
                    x = self.vmax
                if isinstance(self.minline, matplotlib.lines.Line2D):
                    try:
                        self.minline.remove()
                    except ValueError:
                        pass
                self.minline = self.histogram.axvline(x, color='green', lw=1)
                self.vmin = x
                self.minbox.set_text('{:.5f}'.format(x))
            elif event.button == 3:
                if x < self.vmin:
                    x = self.vmin
                if isinstance(self.maxline, matplotlib.lines.Line2D):
                    try:
                        self.maxline.remove()
                    except ValueError:
                        pass
                self.maxline = self.histogram.axvline(x, color='red', lw=1)
                self.vmax = x
                self.maxbox.set_text('{:.5f}'.format(x))
            self.histcanvas.draw_idle()
            self.update_cuts(minmax=(self.vmin, self.vmax))
    
        def start_dir_watch(self, directory=None):
            if self.watched_directory is None:
                print("No directory to watch")
                return
            thread = threading.Thread(target=self._dir_watcher)
            thread.daemon = True
            self.dir_watcher_active = True
            thread.start()
    
        def _dir_watcher(self):
            while self.dir_watcher_active:
                fitsfiles = glob.glob(self.watched_directory + "{}*.fits".format(self.prefix))
                fitsfiles.sort()
                for f in fitsfiles:
                    if f not in self.instore:
                        GLib.idle_add(self._update_filestore, f)
                time.sleep(self.refresh_interval)
    
        def _update_filestore(self, f):
            self.filestore.append([f.split('/')[-1], f])
            self.instore.append(f)
    
        def change_watched_dir(self, directory, prefix=""):
            self.dir_watcher_active = False
            self.watched_directory = directory if directory.endswith('/') else directory + '/'
            self.prefix = prefix
            self.selection.disconnect_by_func(self.set_view)
            self.filestore.clear()
            self.selection.connect("changed", self.set_view)
            del self.instore[:]
            self.start_dir_watch()
    
        def _on_change_watched_dir(self, widget):
            dialog = Gtk.FileChooserDialog(title="Change directory", parent=self,
                                           action=Gtk.FileChooserAction.SELECT_FOLDER)
            dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
    
            response = dialog.run()
            if response == Gtk.ResponseType.OK:
                name = dialog.get_filename()
                dialog.destroy()
            else:
                dialog.destroy()
                return
            self.change_watched_dir(directory=name)
    
    
    class Histogram(Gtk.Window):
        def __init__(self, parent):
            Gtk.Window.__init__(self, title="hist", default_height=150,
                                default_width=300, parent=parent)
    
            fig = Figure(figsize=(21, 3))
            self.histogram = fig.add_subplot(111)
            values = np.random.normal(100, 20, 10000)
            h = self.histogram.hist(values, bins='fd', normed=True, histtype='stepfilled')
            self.vmin, self.vmax = h[1].min(), h[1].max()
    
            canvas = FigureCanvas(fig)
    
            self.add(canvas)
            self.show_all()
    
        def __call__(self):
            print('HELLO')
    
    
    class NavBar(NavigationToolbar):
        def set_message(self, msg):
            pass
    
    
    class Formatter(object):
        def __init__(self, im):
            self.im = im
    
        def __call__(self, x, y):
            z = self.im.get_array()[int(y), int(x)]
            return 'x={:1.1f}, y={:1.1f}, z={:1.3f}'.format(x, y, z)
    
    
    if __name__ == "__main__":
        if len(sys.argv) == 2:
            win = ImageViewer(directory=sys.argv[1])
        elif len(sys.argv) > 2:
            win = ImageViewer(directory=sys.argv[1], prefix=sys.argv[2])
        else:
            win = ImageViewer()
        win.connect("delete-event", Gtk.main_quit)
        win.show_all()
        Gtk.main()