Module qimview.image_viewers.multi_view

Expand source code
from qimview.utils.qt_imports import *
from qimview.utils.utils import get_time
from qimview.utils.viewer_image  import *
from qimview.utils.menu_selection import MenuSelection
from qimview.utils.mvlabel import MVLabel
from qimview.cache import ImageCache

from qimview.image_viewers import *

from enum import Enum, auto
import math

import types
from typing import List, TYPE_CHECKING, Optional
if TYPE_CHECKING:
    from qimview.image_viewers.image_viewer import ImageViewer

class ViewerType(Enum):
    QT_VIEWER             = auto()
    OPENGL_VIEWER         = auto()
    OPENGL_SHADERS_VIEWER = auto()


class MultiView(QtWidgets.QWidget):

    def __init__(self, parent=None, viewer_mode: ViewerType =ViewerType.QT_VIEWER, nb_viewers: int =1) -> None:
        """
        :param parent:
        :param viewer_mode:
        :param nb_viewers_used:
        """
        QtWidgets.QWidget.__init__(self, parent)

        self.use_opengl = viewer_mode in [ViewerType.OPENGL_SHADERS_VIEWER, ViewerType.OPENGL_VIEWER]

        self.nb_viewers_used : int = nb_viewers
        self.allocated_image_viewers = []  # keep allocated image viewers here
        self.image_viewers = []
        self.image_viewer_classes = {
            ViewerType.QT_VIEWER:             QTImageViewer,
            ViewerType.OPENGL_VIEWER:         GLImageViewer,
            ViewerType.OPENGL_SHADERS_VIEWER: GLImageViewerShaders
        }
        self.image_viewer_class = self.image_viewer_classes[viewer_mode]

        # Create viewer instances
        for n in range(self.nb_viewers_used):
            viewer = self.image_viewer_class()
            viewer.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
            self.allocated_image_viewers.append(viewer)
            self.image_viewers.append(viewer)

        self.viewer_mode = viewer_mode
        self.bold_font = QtGui.QFont()

        self.verbosity_LIGHT = 1
        self.verbosity_TIMING = 1 << 2
        self.verbosity_TIMING_DETAILED = 1 << 3
        self.verbosity_TRACE = 1 << 4
        self.verbosity_DEBUG = 1 << 5
        self.verbosity = 0

        # self.set_verbosity(self.verbosity_LIGHT)
        # self.set_verbosity(self.verbosity_TIMING_DETAILED)
        # self.set_verbosity(self.verbosity_TRACE)

        self.current_image_filename = None
        self.save_image_clipboard = False

        self.filter_params = ImageFilterParameters()
        self.filter_params_gui = ImageFilterParametersGui(self.filter_params)

        self.raw_bayer = {'Read': None, 'Bayer0': ImageFormat.CH_GBRG, 'Bayer1': ImageFormat.CH_BGGR, 'Bayer2': ImageFormat.CH_RGGB, 'Bayer3': ImageFormat.CH_GRBG}
        self.default_raw_bayer = 'Read'
        self.current_raw_bayer = self.default_raw_bayer

        # Number of viewers currently displayed
        self.nb_viewers_used : int = 0

        # save images of last visited row
        self.cache = ImageCache()
        self.image_dict = { }
        self.read_size = 'full'
        self.image1 = dict()
        self.image2 = dict()
        self.button_layout = None
        self.message_cb = None
        self.replacing_widget = self.before_max_parent = None

        if 'ClickFocus' in QtCore.Qt.FocusPolicy.__dict__:
            self.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus)
        else:
            self.setFocusPolicy(QtCore.Qt.ClickFocus)

        self.key_up_callback = None
        self.key_down_callback = None
        self.output_image_label = dict()

        self.output_label_current_image   : str = ''
        self.output_label_reference_image : str = ''
        self.add_context_menu()
        
        # Parameter to set the number of columns in the viewer grid layout
        # if 0: computed automatically
        self.max_columns : int = 0 

    def set_key_up_callback(self, c):
        self.key_up_callback = c

    def set_key_down_callback(self, c):
        self.key_down_callback = c

    def add_context_menu(self):
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_context_menu)
        self._context_menu = QtWidgets.QMenu()
        self.viewer_modes = {}
        for v in ViewerType:
            self.viewer_modes[v.name] = v
        self._default_viewer_mode = ViewerType.QT_VIEWER.name
        self.viewer_mode_selection = MenuSelection("Viewer mode", 
            self._context_menu, self.viewer_modes, self._default_viewer_mode, self.update_viewer_mode)
        self._context_menu.addSeparator()
        action = self._context_menu.addAction("Reset viewers")
        action.triggered.connect(self.reset_viewers)

    def reset_viewers(self):
        for v in self.image_viewers:
            v.hide()
            self.viewer_grid_layout.removeWidget(v)
        self.allocated_image_viewers.clear()
        self.image_viewers.clear()
        # Create viewer instances
        for n in range(self.nb_viewers_used):
            viewer = self.image_viewer_class()
            viewer.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
            self.allocated_image_viewers.append(viewer)
            self.image_viewers.append(viewer)
        self.set_number_of_viewers(self.nb_viewers_used)
        self.viewer_grid_layout.update()
        self.update_image()

    def update_viewer_mode(self):
        viewer_mode = self.viewer_mode_selection.get_selection_value()
        self.image_viewer_class = self.image_viewer_classes[viewer_mode]

    def show_context_menu(self, pos):
        # allow to switch between images by pressing Alt+'image position' (Alt+0, Alt+1, etc)
        self._context_menu.show()
        self._context_menu.popup( self.mapToGlobal(pos) )

    def set_cache_memory_bar(self, progress_bar):
        self.cache.set_memory_bar(progress_bar)

    def set_verbosity(self, flag, enable=True):
        """
        :param v: verbosity flags
        :param b: boolean to enable or disable flag
        :return:
        """
        if enable:
            self.verbosity = self.verbosity | flag
        else:
            self.verbosity = self.verbosity & ~flag

    def check_verbosity(self, flag):
        return self.verbosity & flag

    def print_log(self, mess):
        if self.verbosity & self.verbosity_LIGHT:
            print(mess)

    def show_timing(self):
        return self.check_verbosity(self.verbosity_TIMING) or self.check_verbosity(self.verbosity_TIMING_DETAILED)

    def show_timing_detailed(self):
        return self.check_verbosity(self.verbosity_TIMING_DETAILED)

    def show_trace(self):
        return self.check_verbosity(self.verbosity_TRACE)

    def make_mouse_press(self, image_name):
        def mouse_press(obj, event):
            print('mouse_press')
            obj.update_image(image_name)

        return types.MethodType(mouse_press, self)

    def mouse_release(self, event):
        self.update_image(self.output_label_reference_image)

    def make_mouse_double_click(self, image_name):
        def mouse_double_click(obj, event):
            '''
            Sets the double clicked label as the reference image
            :param obj:
            :param event:
            '''
            print('mouse_double_click {}'.format(image_name))
            obj.output_label_reference_image = image_name
            obj.output_label_current_image = obj.output_label_reference_image
            obj.update_image()

        return types.MethodType(mouse_double_click, self)

    def set_read_size(self, read_size):
        self.read_size = read_size
        # reset cache
        self.cache.reset()

    def update_image_intensity_event(self):
        self.update_image_parameters()

    def reset_intensities(self):
        self.filter_params_gui.reset_all()

    def update_image_parameters(self):
        '''
        Uses the variable self.output_label_current_image
        :return:
        '''
        self.print_log('update_image_parameters')
        update_start = get_time()

        for n in range(self.nb_viewers_used):
            self.image_viewers[n].filter_params.copy_from(self.filter_params)
            self.image_viewers[n].update()

        if self.show_timing():
            time_spent = get_time() - update_start
            self.print_log(" Update image took {0:0.3f} sec.".format(time_spent))

    def set_images(self, images, set_viewers=False):
        self.print_log(f"MultiView.set_images() {images}")
        if images.keys() == self.image_dict.keys():
            self.image_dict = images
            self.update_reference()
        else:
            self.image_dict = images
            self.update_image_buttons()

    def set_viewer_images(self):
        """
        Set viewer images based on self.image_dict.keys()
        :return:
        """
        # if set_viewers, we force the viewer layout and images based on the list
        # be sure to have enough image viewers allocated
        while self.nb_viewers_used > len(self.allocated_image_viewers):
            viewer = self.image_viewer_class()
            viewer.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
            self.allocated_image_viewers.append(viewer)
        self.image_viewers = self.allocated_image_viewers[:self.nb_viewers_used]
        image_names = list(self.image_dict.keys())
        for n in range(self.nb_viewers_used):
            if n < len(image_names):
                self.image_viewers[n].image_name = image_names[n]
            else:
                self.image_viewers[n].image_name = image_names[len(image_names)-1]

    def update_reference(self) -> None:
        reference_image = self.get_output_image(self.output_label_reference_image)
        for n in range(self.nb_viewers_used):
            viewer = self.image_viewers[n]
            # set reference image
            viewer.set_image_ref(reference_image)

    def set_reference_label(self, ref: str, update_viewers=False) -> None:
        try:
            if ref is not None:
                if ref!=self.output_label_reference_image:
                    self.output_label_reference_image = ref
                    if update_viewers:
                        self.update_reference()
        except Exception as e:
            print(f' Failed to set reference label {e}')

    def update_image_buttons(self):
        # choose image to display
        self.clear_buttons()
        self.image_list = list(self.image_dict.keys())
        self.print_log("MultiView.update_image_buttons() {}".format(self.image_list))
        self.label = dict()
        for image_name in self.image_list:
            # possibility to disable an image using the string 'none', especially useful for input image
            if image_name != 'none':
                self.label[image_name] = MVLabel(image_name, self)
                self.label[image_name].setFrameShape(QtWidgets.QFrame.Panel)
                self.label[image_name].setFrameShadow(QtWidgets.QFrame.Sunken)
                # self.label[image_name].setLineWidth(3)
                self.label[image_name].setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)
                # self.label[image_name].setFixedHeight(40)
                self.label[image_name].mousePressEvent = self.make_mouse_press(image_name)
                self.label[image_name].mouseReleaseEvent = self.mouse_release
                self.label[image_name].mouseDoubleClickEvent = self.make_mouse_double_click(image_name)
        self.create_buttons()

        # the crop area can be changed using the mouse wheel
        self.output_label_crop = (0., 0., 1., 1.)

        if len(self.image_list)>0:
            self.output_label_current_image = self.image_list[0]
            self.set_reference_label(self.image_list[0], update_viewers=True)
        else:
            self.output_label_current_image = ''
            self.output_label_reference_image = ''

    def clear_buttons(self):
        if self.button_layout is not None:
            # start clearing the layout
            # for i in range(self.button_layout.count()): self.button_layout.itemAt(i).widget().close()
            self.print_log(f"MultiView.clear_buttons() {self.image_list}")
            for image_name in reversed(self.image_list):
                if image_name in self.label:
                    self.button_layout.removeWidget(self.label[image_name])
                    self.label[image_name].close()

    def create_buttons(self):
        if self.button_layout is not None:
            max_grid_columns = 10
            idx = 0
            for image_name in self.image_list:
                # possibility to disable an image using the string 'none', especially useful for input image
                if image_name != 'none':
                    self.button_layout.addWidget(self.label[image_name], idx // max_grid_columns, idx % max_grid_columns)
                    idx += 1

    def layout_buttons(self, vertical_layout):
        self.button_widget = QtWidgets.QWidget(self)
        self.button_layout = QtWidgets.QGridLayout()
        self.button_layout.setHorizontalSpacing(0)
        self.button_layout.setVerticalSpacing(0)
        # button_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)
        self.create_buttons()
        vertical_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)
        # vertical_layout.setSizeConstraint(QtWidgets.QLayout.SetNoConstraint)
        self.button_widget.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
        self.button_widget.setLayout(self.button_layout)
        vertical_layout.addWidget(self.button_widget, 0, QtCore.Qt.AlignTop)

    def layout_parameters(self, parameters_layout):
        # Add Profiles and keep zoom options
        self.display_profiles = QtWidgets.QCheckBox("Profiles")
        self.display_profiles.stateChanged.connect(self.toggle_display_profiles)
        self.display_profiles.setChecked(False)
        parameters_layout.addWidget(self.display_profiles)
        self.keep_zoom = QtWidgets.QCheckBox("Keep zoom")
        self.keep_zoom.setChecked(False)
        parameters_layout.addWidget(self.keep_zoom)

        # Reset button
        self.reset_button = QtWidgets.QPushButton("reset")
        parameters_layout.addWidget(self.reset_button)
        self.reset_button.clicked.connect(self.reset_intensities)

        # Add color difference slider
        self.filter_params_gui.add_imdiff_factor(parameters_layout, self.update_image_intensity_event)

        # --- Saturation adjustment
        self.filter_params_gui.add_saturation(parameters_layout, self.update_image_intensity_event)
        # --- Black point adjustment
        self.filter_params_gui.add_blackpoint(parameters_layout, self.update_image_intensity_event)
        # --- white point adjustment
        self.filter_params_gui.add_whitepoint(parameters_layout, self.update_image_intensity_event)
        # --- Gamma adjustment
        self.filter_params_gui.add_gamma(parameters_layout, self.update_image_intensity_event)

    def layout_parameters_2(self, parameters2_layout):
        # --- G_R adjustment
        self.filter_params_gui.add_g_r(parameters2_layout, self.update_image_intensity_event)
        # --- G_B adjustment
        self.filter_params_gui.add_g_b(parameters2_layout, self.update_image_intensity_event)

    def update_layout(self):
        self.print_log("update_layout")
        vertical_layout = QtWidgets.QVBoxLayout()
        self.layout_buttons(vertical_layout)

        # First line of parameter control
        parameters_layout = QtWidgets.QHBoxLayout()
        self.layout_parameters(parameters_layout)
        vertical_layout.addLayout(parameters_layout, 1)

        # Second line of parameter control
        parameters2_layout = QtWidgets.QHBoxLayout()
        self.layout_parameters_2(parameters2_layout)
        vertical_layout.addLayout(parameters2_layout, 1)

        self.viewer_grid_layout = QtWidgets.QGridLayout()
        self.viewer_grid_layout.setHorizontalSpacing(1)
        self.viewer_grid_layout.setVerticalSpacing(1)
        self.set_number_of_viewers(1)
        vertical_layout.addLayout(self.viewer_grid_layout, 1)

        self.figures_widget = QtWidgets.QWidget()
        self.figures_layout = QtWidgets.QHBoxLayout()
        self.figures_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)
        # for the moment ignore this
        # self.figures_layout.addWidget(self.value_in_range_canvas)
        # self.figures_widget.setLayout(self.figures_layout)

        vertical_layout.addWidget(self.figures_widget)
        self.toggle_display_profiles()
        self.setLayout(vertical_layout)
        print("update_layout done")

    def toggle_display_profiles(self):
        self.figures_widget.setVisible(self.display_profiles.isChecked())
        self.update_image()

    def get_output_image(self, im_string_id):
        """
        Search for the image with given label in the current row
        if not in cache reads it and add it to the cache
        :param im_string_id: string that identifies the image to display
        :return:
        """
        # print(f"get_output_image({im_string_id}) ")
        start = get_time()

        image_filename = self.image_dict[im_string_id]
        image_transform = None
        self.print_log(f"MultiView.get_output_image() image_filename:{image_filename}")

        image_data, _ = self.cache.get_image(image_filename, self.read_size, verbose=self.show_timing_detailed(),
                                             use_RGB=not self.use_opengl, image_transform=image_transform)

        if image_data is not None:
            self.output_image_label[im_string_id] = image_filename
            output_image = image_data
        else:
            print(f"failed to get image {im_string_id}: {image_filename}")
            return None

        if self.show_timing_detailed():
            print(f" get_output_image took {int((get_time() - start)*1000+0.5)} ms".format)

        # force image bayer information if selected from menu
        res = output_image
        set_bayer = self.raw_bayer[self.current_raw_bayer]
        if res.channels in [ImageFormat.CH_BGGR, ImageFormat.CH_GBRG, ImageFormat.CH_GRBG, ImageFormat.CH_RGGB] and set_bayer is not None:
            print(f"Setting bayer {set_bayer}")
            res.channels = set_bayer

        return res

    def set_message_callback(self, message_cb):
        self.message_cb = message_cb

    def setMessage(self, mess):
        if self.message_cb is not None:
            self.message_cb(mess)

    def cache_read_images(self, image_filenames: List[str], reload: bool =False) -> None:
        """ Read the list of images into the cache, with option to reload them from disk

        Args:
            image_filenames (List[str]): list of image filenames
            reload (bool, optional): reload removes first the images from the ImageCache 
                before adding them. Defaults to False.
        """        
        # print(f"cache_read_images({image_filenames}) ")
        image_transform = None
        if reload:
            for f in image_filenames:
                self.cache.remove(f)
        self.cache.add_images(image_filenames, self.read_size, verbose=False, use_RGB=not self.use_opengl,
                             image_transform=image_transform)

    def update_label_fonts(self):
        # Update selected image label, we could do it later too
        for im_name in self.image_list:
            # possibility to disable an image using the string 'none', especially useful for input image
            if im_name != 'none':
                is_bold      = im_name == self.output_label_current_image
                is_underline = im_name == self.output_label_reference_image
                is_bold |= is_underline
                self.bold_font.setBold(is_bold)
                self.bold_font.setUnderline(is_underline)
                self.bold_font.setPointSize(8)
                self.label[im_name].setFont(self.bold_font)
                self.label[im_name].setWordWrap(True)
            # self.label[im_name].setMaximumWidth(160)

    def update_image(self, image_name=None, reload=False):
        """
        Uses the variable self.output_label_current_image
        :return:
        """
        self.print_log('update_image {} current: {}'.format(image_name, self.output_label_current_image))
        update_image_start = get_time()

        # Define the current selected image
        if image_name is not None:
            self.output_label_current_image = image_name
        if self.output_label_current_image == "":
            return

        if self.image_dict[self.output_label_current_image] is None:
            print(" No image filename for current image")
            return

        self.update_label_fonts()

        # find first active window
        first_active_window = 0
        for n in range(self.nb_viewers_used):
            self.image_viewers[n].display_timing = self.show_timing()>0
            if self.image_viewers[n].is_active():
                first_active_window = n
                break

        # Read images in parallel to improve preformances
        # list all required image filenames
        # set all viewers image names (labels)
        image_filenames = [self.image_dict[self.output_label_current_image]]
        # define image associated to each used viewer and add it to the list of images to get
        for n in range(self.nb_viewers_used):
            viewer : ImageViewer = self.image_viewers[n]
            # Set active only the first active window
            viewer.set_active(n == first_active_window)
            if viewer.get_image() is None:
                if n < len(self.image_list):
                    viewer.image_name = self.image_list[n]
                    image_filenames.append(self.image_dict[self.image_list[n]])
                else:
                    viewer.image_name = self.output_label_current_image
            else:
                # image_name should belong to image_dict
                if viewer.image_name in self.image_dict:
                    image_filenames.append(self.image_dict[viewer.image_name])
                else:
                    viewer.image_name = self.output_label_current_image

        # remove duplicates
        image_filenames = list(set(image_filenames))
        # print(f"image filenames {image_filenames}")
        self.cache_read_images(image_filenames, reload=reload)

        try:
            current_image = self.get_output_image(self.output_label_current_image)
            if current_image is None:
                return
        except Exception as e:
            print("Error: failed to get image {}: {}".format(self.output_label_current_image, e))
            return

        # print(f"cur {self.output_label_current_image}")
        current_filename = self.output_image_label[self.output_label_current_image]

        if self.show_timing_detailed():
            time_spent = get_time() - update_image_start

        self.setMessage("Image: {0}".format(current_filename))

        current_viewer = self.image_viewers[first_active_window]
        if self.save_image_clipboard:
            print("set save image to clipboard")
            current_viewer.set_clipboard(self.clip, True)
        current_viewer.set_active(True)
        current_viewer.image_name = self.output_label_current_image
        current_viewer.set_image(current_image)
        if self.save_image_clipboard:
            print("end save image to clipboard")
            current_viewer.set_clipboard(None, False)

        # print(f"ref {self.output_label_reference_image}")
        if self.output_label_reference_image==self.output_label_current_image:
            reference_image = current_image
        else:
            reference_image = self.get_output_image(self.output_label_reference_image)

        if self.nb_viewers_used >= 2:
            prev_n = first_active_window
            for n in range(1, self.nb_viewers_used):
                n1 = (first_active_window + n) % self.nb_viewers_used
                viewer = self.image_viewers[n1]
                # viewer image has already been defined
                # try to update corresponding images in row
                try:
                    viewer_image = self.get_output_image(viewer.image_name)
                except Exception as e:
                    print("Error: failed to get image {}: {}".format(viewer.image_name, e))
                    viewer.set_image(current_image)
                else:
                    viewer.set_image(viewer_image)

                # set reference image
                viewer.set_image_ref(reference_image)

                self.image_viewers[prev_n].set_synchronize(viewer)
                prev_n = n1
            # Create a synchronization loop
            if prev_n != first_active_window:
                self.image_viewers[prev_n].set_synchronize(self.image_viewers[first_active_window])

        # Be sure to show the required viewers
        for n in range(self.nb_viewers_used):
            viewer = self.image_viewers[n]
            # print(f"show viewer {n}")
            # Note: calling show in any case seems to avoid double calls to paint event that update() triggers
            # viewer.show()
            if viewer.isHidden():
                # print(f"show viewer {n}")
                viewer.show()
            else:
                # print(f"update viewer {n}")
                viewer.update()


        # self.image_scroll_area.adjustSize()
        # if self.show_timing():
        print(f" Update image took {(get_time() - update_image_start)*1000:0.0f} ms")

    def set_number_of_viewers(self, nb_viewers: int = 1, max_columns : int = 0) -> None:
        self.print_log("*** set_number_of_viewers()")

        # 1. remove current viewers from grid layout
        # self.viewer_grid_layout.hide()
        for v in self.image_viewers:
            v.hide()
            self.viewer_grid_layout.removeWidget(v)

        self.nb_viewers_used : int = nb_viewers
        print(f"max_columns = {max_columns}")
        if max_columns>0:
            row_length = min(self.nb_viewers_used, max_columns)
            col_length = int(math.ceil(self.nb_viewers_used / row_length))
        else:
            # Find best configuration to fill the space based on image size and widget size?
            col_length = int(math.sqrt(self.nb_viewers_used))
            row_length = int(math.ceil(self.nb_viewers_used / col_length))
        self.print_log('col_length = {} row_length = {}'.format(col_length, row_length))
        # be sure to have enough image viewers allocated
        while self.nb_viewers_used > len(self.allocated_image_viewers):
            viewer = self.image_viewer_class()
            viewer.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
            self.allocated_image_viewers.append(viewer)

        self.image_viewers = self.allocated_image_viewers[:self.nb_viewers_used]

        for n in range(self.nb_viewers_used):
            self.viewer_grid_layout.addWidget(self.image_viewers[n], int(n / float(row_length)), n % row_length)
            self.image_viewers[n].hide()

        # for n in range(self.nb_viewers_used):
        #     print("Viewer {} size {}".format(n, (self.image_viewers[n].width(), self.image_viewers[n].height())))

    def set_number_of_viewers_callback(self):
        self.set_number_of_viewers()
        self.viewer_grid_layout.update()
        self.update_image()

    def keyReleaseEvent(self, event):
        if type(event) == QtGui.QKeyEvent:
            modifiers = QtWidgets.QApplication.keyboardModifiers()
            # allow to switch between images by pressing Alt+'image position' (Alt+0, Alt+1, etc)
            if modifiers & (QtCore.Qt.AltModifier | QtCore.Qt.ControlModifier):
                event.accept()
            # else:
            #     try:
            #         # reset reference image
            #         if self.output_label_current_image != self.output_label_reference_image:
            #             self.update_image(self.output_label_reference_image)
            #     except Exception as e:
            #         print(" Error: {}".format(e))

    def find_in_layout(self, layout: QtWidgets.QLayout) -> Optional[QtWidgets.QLayout]:
        """ Search Recursivement in Layouts for the current widget

        Args:
            layout (QtWidgets.QLayout): input layout for search

        Returns:
            layout containing the current widget or None if not found
        """
        print("find_in_layout()")
        if layout.indexOf(self) != -1: return layout
        for i in range(layout.count()):
            item = layout.itemAt(i)
            if item.widget() == self: return layout
            if (l := item.layout()) and (found:=self.find_in_layout(l)): return l
        print("find_in_layout() return None")
        return None

    def toggle_fullscreen(self, event):
        print(f"toggle_fullscreen")
        if not issubclass(self.__class__,QtWidgets.QWidget):
            print(f"Cannot use toggle_fullscreen on a class that is not a QWidget")
            return
        # Should be inside a layout
        if self.before_max_parent is None:
            print(f"self.parent() is not None {self.parent() is not None}")
            print(f"self.parent().layout() {self.parent().layout()} ")
            if self.parent() is not None and (playout := self.parent().layout()) is not None:
                if self.find_in_layout(playout):
                    self.before_max_parent = self.parent()
                    self.replacing_widget = QtWidgets.QWidget(self.before_max_parent)
                    self.parent().layout().replaceWidget(self, self.replacing_widget)
                    # We need to go up from the parent widget to the main window to get its geometry
                    # so that the fullscreen is display on the same monitor
                    toplevel_parent : Optional[QtWidgets.QWidget] = self.parentWidget()
                    while toplevel_parent.parentWidget(): toplevel_parent = toplevel_parent.parentWidget()
                    self.setParent(None)
                    if toplevel_parent: self.setGeometry(toplevel_parent.geometry())
                    self.showFullScreen()
                    event.accept()
                    return
        if self.before_max_parent is not None:
            self.setParent(self.before_max_parent)
            self.parent().layout().replaceWidget(self.replacing_widget, self)
            self.replacing_widget = self.before_max_parent = None
            # self.resize(self.before_max_size)
            self.show()
            self.parent().update()
            self.setFocus()
            event.accept()
            return



    def keyPressEvent(self, event):
        if type(event) == QtGui.QKeyEvent:
            # print("key is ", event.key())
            self.print_log(f" QKeySequence() {QtGui.QKeySequence(event.key()).toString()}")
            # print( QtGui.QKeySequence(event.key()).toString())
            # print(f" capslock: {event.getModifierState('CapsLock')}")
            if self.show_trace():
                print("key is ", event.key())
            modifiers = QtWidgets.QApplication.keyboardModifiers()
            # F1: open help in browser
            if event.key() == QtCore.Qt.Key_F1:
                import qimview
                mb = QtWidgets.QMessageBox(self)
                mb.setWindowTitle(f"qimview {qimview.__version__}: MultiView help")
                mb.setTextFormat(QtCore.Qt.TextFormat.RichText)
                mb.setText(
                    "<a href='https://github.com/qimview/qimview/wiki'>qimview</a><br>"
                    "<a href='https://github.com/qimview/qimview/wiki/4.-Multi%E2%80%90image-viewer'>MultiImage Viewer</a><br>"
                    "<a href='https://github.com/qimview/qimview/wiki/3.-Image-Viewers'>Image Viewer</a>")
                mb.exec()
                event.accept()
                return

            # F5: reload images
            if event.key() == QtCore.Qt.Key_F5:
                self.update_image(reload=True)
                event.accept()
                return

            if event.key() == QtCore.Qt.Key_F11:
                # Should be inside a layout
                print("MultiView F11 pressed")
                self.toggle_fullscreen(event)
                return

            # allow to switch between images by pressing Alt+'image position' (Alt+0, Alt+1, etc)
            if modifiers & QtCore.Qt.AltModifier:
                for n in range(len(self.image_list)):
                    if self.image_list[n] is not None:
                        if event.key() == QtCore.Qt.Key_0 + n:
                            if self.output_label_current_image != self.image_list[n]:
                                # with Alt+Ctrl, change reference image
                                # if modifiers & QtCore.Qt.ControlModifier:
                                #     self.set_reference_label(self.image_list[n])
                                self.update_image(self.image_list[n])
                                self.setFocus()
                                return
                event.accept()
                return

            if event.modifiers() & QtCore.Qt.ControlModifier:
                # allow to switch between images by pressing Ctrl+'image position' (Ctrl+0, Ctrl+1, etc)
                for n in range(len(self.image_list)):
                    if self.image_list[n] != 'none':
                        if event.key() == QtCore.Qt.Key_0 + n:
                            if self.output_label_current_image != self.image_list[n]:
                                self.set_reference_label(self.image_list[n], update_viewers=True)
                                self.update_image()
                                event.accept()
                                return
                return
            # print(f"event.modifiers {event.modifiers()}")
            # if not event.modifiers():
            for n in range(1, 10):
                if event.key() == QtCore.Qt.Key_0 + n:
                    self.set_number_of_viewers(n)
                    self.viewer_grid_layout.update()
                    self.update_image()
                    self.setFocus()
                    event.accept()
                    return

            if event.key() == QtCore.Qt.Key_Up:
                if self.key_up_callback is not None:
                    self.key_up_callback()
                event.accept()
                return

            if event.key() == QtCore.Qt.Key_Down:
                if self.key_down_callback is not None:
                    self.key_down_callback()
                event.accept()
                return

            nb_images = len(self.image_list)
            if event.key() == QtCore.Qt.Key_Left:
                for n in range(nb_images):
                    if self.output_label_current_image == self.image_list[n]:
                        print(f"setting new image index {(n+nb_images-1)%nb_images}")
                        self.update_image(self.image_list[(n+nb_images-1)%nb_images])
                        event.accept()
                        return

            if event.key() == QtCore.Qt.Key_Right:
                for n in range(nb_images):
                    if self.output_label_current_image == self.image_list[n]:
                        print(f"setting new image index {(n+nb_images+1)%nb_images}")
                        self.update_image(self.image_list[(n+1)%nb_images])
                        event.accept()
                        return
                
            # G: display number of columns
            if event.key() == QtCore.Qt.Key_G:
                self.max_columns = int ((self.max_columns + 1) % self.nb_viewers_used + 1)
                self.set_number_of_viewers(self.nb_viewers_used, max_columns=self.max_columns)
                self.update_image(reload=True)
                self.setFocus()
                event.accept()
                return

        else:
            event.ignore()

Classes

class MultiView (parent=None, viewer_mode: ViewerType = ViewerType.QT_VIEWER, nb_viewers: int = 1)

QWidget(self, parent: Optional[PySide6.QtWidgets.QWidget] = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags)) -> None

:param parent: :param viewer_mode: :param nb_viewers_used:

Expand source code
class MultiView(QtWidgets.QWidget):

    def __init__(self, parent=None, viewer_mode: ViewerType =ViewerType.QT_VIEWER, nb_viewers: int =1) -> None:
        """
        :param parent:
        :param viewer_mode:
        :param nb_viewers_used:
        """
        QtWidgets.QWidget.__init__(self, parent)

        self.use_opengl = viewer_mode in [ViewerType.OPENGL_SHADERS_VIEWER, ViewerType.OPENGL_VIEWER]

        self.nb_viewers_used : int = nb_viewers
        self.allocated_image_viewers = []  # keep allocated image viewers here
        self.image_viewers = []
        self.image_viewer_classes = {
            ViewerType.QT_VIEWER:             QTImageViewer,
            ViewerType.OPENGL_VIEWER:         GLImageViewer,
            ViewerType.OPENGL_SHADERS_VIEWER: GLImageViewerShaders
        }
        self.image_viewer_class = self.image_viewer_classes[viewer_mode]

        # Create viewer instances
        for n in range(self.nb_viewers_used):
            viewer = self.image_viewer_class()
            viewer.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
            self.allocated_image_viewers.append(viewer)
            self.image_viewers.append(viewer)

        self.viewer_mode = viewer_mode
        self.bold_font = QtGui.QFont()

        self.verbosity_LIGHT = 1
        self.verbosity_TIMING = 1 << 2
        self.verbosity_TIMING_DETAILED = 1 << 3
        self.verbosity_TRACE = 1 << 4
        self.verbosity_DEBUG = 1 << 5
        self.verbosity = 0

        # self.set_verbosity(self.verbosity_LIGHT)
        # self.set_verbosity(self.verbosity_TIMING_DETAILED)
        # self.set_verbosity(self.verbosity_TRACE)

        self.current_image_filename = None
        self.save_image_clipboard = False

        self.filter_params = ImageFilterParameters()
        self.filter_params_gui = ImageFilterParametersGui(self.filter_params)

        self.raw_bayer = {'Read': None, 'Bayer0': ImageFormat.CH_GBRG, 'Bayer1': ImageFormat.CH_BGGR, 'Bayer2': ImageFormat.CH_RGGB, 'Bayer3': ImageFormat.CH_GRBG}
        self.default_raw_bayer = 'Read'
        self.current_raw_bayer = self.default_raw_bayer

        # Number of viewers currently displayed
        self.nb_viewers_used : int = 0

        # save images of last visited row
        self.cache = ImageCache()
        self.image_dict = { }
        self.read_size = 'full'
        self.image1 = dict()
        self.image2 = dict()
        self.button_layout = None
        self.message_cb = None
        self.replacing_widget = self.before_max_parent = None

        if 'ClickFocus' in QtCore.Qt.FocusPolicy.__dict__:
            self.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus)
        else:
            self.setFocusPolicy(QtCore.Qt.ClickFocus)

        self.key_up_callback = None
        self.key_down_callback = None
        self.output_image_label = dict()

        self.output_label_current_image   : str = ''
        self.output_label_reference_image : str = ''
        self.add_context_menu()
        
        # Parameter to set the number of columns in the viewer grid layout
        # if 0: computed automatically
        self.max_columns : int = 0 

    def set_key_up_callback(self, c):
        self.key_up_callback = c

    def set_key_down_callback(self, c):
        self.key_down_callback = c

    def add_context_menu(self):
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_context_menu)
        self._context_menu = QtWidgets.QMenu()
        self.viewer_modes = {}
        for v in ViewerType:
            self.viewer_modes[v.name] = v
        self._default_viewer_mode = ViewerType.QT_VIEWER.name
        self.viewer_mode_selection = MenuSelection("Viewer mode", 
            self._context_menu, self.viewer_modes, self._default_viewer_mode, self.update_viewer_mode)
        self._context_menu.addSeparator()
        action = self._context_menu.addAction("Reset viewers")
        action.triggered.connect(self.reset_viewers)

    def reset_viewers(self):
        for v in self.image_viewers:
            v.hide()
            self.viewer_grid_layout.removeWidget(v)
        self.allocated_image_viewers.clear()
        self.image_viewers.clear()
        # Create viewer instances
        for n in range(self.nb_viewers_used):
            viewer = self.image_viewer_class()
            viewer.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
            self.allocated_image_viewers.append(viewer)
            self.image_viewers.append(viewer)
        self.set_number_of_viewers(self.nb_viewers_used)
        self.viewer_grid_layout.update()
        self.update_image()

    def update_viewer_mode(self):
        viewer_mode = self.viewer_mode_selection.get_selection_value()
        self.image_viewer_class = self.image_viewer_classes[viewer_mode]

    def show_context_menu(self, pos):
        # allow to switch between images by pressing Alt+'image position' (Alt+0, Alt+1, etc)
        self._context_menu.show()
        self._context_menu.popup( self.mapToGlobal(pos) )

    def set_cache_memory_bar(self, progress_bar):
        self.cache.set_memory_bar(progress_bar)

    def set_verbosity(self, flag, enable=True):
        """
        :param v: verbosity flags
        :param b: boolean to enable or disable flag
        :return:
        """
        if enable:
            self.verbosity = self.verbosity | flag
        else:
            self.verbosity = self.verbosity & ~flag

    def check_verbosity(self, flag):
        return self.verbosity & flag

    def print_log(self, mess):
        if self.verbosity & self.verbosity_LIGHT:
            print(mess)

    def show_timing(self):
        return self.check_verbosity(self.verbosity_TIMING) or self.check_verbosity(self.verbosity_TIMING_DETAILED)

    def show_timing_detailed(self):
        return self.check_verbosity(self.verbosity_TIMING_DETAILED)

    def show_trace(self):
        return self.check_verbosity(self.verbosity_TRACE)

    def make_mouse_press(self, image_name):
        def mouse_press(obj, event):
            print('mouse_press')
            obj.update_image(image_name)

        return types.MethodType(mouse_press, self)

    def mouse_release(self, event):
        self.update_image(self.output_label_reference_image)

    def make_mouse_double_click(self, image_name):
        def mouse_double_click(obj, event):
            '''
            Sets the double clicked label as the reference image
            :param obj:
            :param event:
            '''
            print('mouse_double_click {}'.format(image_name))
            obj.output_label_reference_image = image_name
            obj.output_label_current_image = obj.output_label_reference_image
            obj.update_image()

        return types.MethodType(mouse_double_click, self)

    def set_read_size(self, read_size):
        self.read_size = read_size
        # reset cache
        self.cache.reset()

    def update_image_intensity_event(self):
        self.update_image_parameters()

    def reset_intensities(self):
        self.filter_params_gui.reset_all()

    def update_image_parameters(self):
        '''
        Uses the variable self.output_label_current_image
        :return:
        '''
        self.print_log('update_image_parameters')
        update_start = get_time()

        for n in range(self.nb_viewers_used):
            self.image_viewers[n].filter_params.copy_from(self.filter_params)
            self.image_viewers[n].update()

        if self.show_timing():
            time_spent = get_time() - update_start
            self.print_log(" Update image took {0:0.3f} sec.".format(time_spent))

    def set_images(self, images, set_viewers=False):
        self.print_log(f"MultiView.set_images() {images}")
        if images.keys() == self.image_dict.keys():
            self.image_dict = images
            self.update_reference()
        else:
            self.image_dict = images
            self.update_image_buttons()

    def set_viewer_images(self):
        """
        Set viewer images based on self.image_dict.keys()
        :return:
        """
        # if set_viewers, we force the viewer layout and images based on the list
        # be sure to have enough image viewers allocated
        while self.nb_viewers_used > len(self.allocated_image_viewers):
            viewer = self.image_viewer_class()
            viewer.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
            self.allocated_image_viewers.append(viewer)
        self.image_viewers = self.allocated_image_viewers[:self.nb_viewers_used]
        image_names = list(self.image_dict.keys())
        for n in range(self.nb_viewers_used):
            if n < len(image_names):
                self.image_viewers[n].image_name = image_names[n]
            else:
                self.image_viewers[n].image_name = image_names[len(image_names)-1]

    def update_reference(self) -> None:
        reference_image = self.get_output_image(self.output_label_reference_image)
        for n in range(self.nb_viewers_used):
            viewer = self.image_viewers[n]
            # set reference image
            viewer.set_image_ref(reference_image)

    def set_reference_label(self, ref: str, update_viewers=False) -> None:
        try:
            if ref is not None:
                if ref!=self.output_label_reference_image:
                    self.output_label_reference_image = ref
                    if update_viewers:
                        self.update_reference()
        except Exception as e:
            print(f' Failed to set reference label {e}')

    def update_image_buttons(self):
        # choose image to display
        self.clear_buttons()
        self.image_list = list(self.image_dict.keys())
        self.print_log("MultiView.update_image_buttons() {}".format(self.image_list))
        self.label = dict()
        for image_name in self.image_list:
            # possibility to disable an image using the string 'none', especially useful for input image
            if image_name != 'none':
                self.label[image_name] = MVLabel(image_name, self)
                self.label[image_name].setFrameShape(QtWidgets.QFrame.Panel)
                self.label[image_name].setFrameShadow(QtWidgets.QFrame.Sunken)
                # self.label[image_name].setLineWidth(3)
                self.label[image_name].setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)
                # self.label[image_name].setFixedHeight(40)
                self.label[image_name].mousePressEvent = self.make_mouse_press(image_name)
                self.label[image_name].mouseReleaseEvent = self.mouse_release
                self.label[image_name].mouseDoubleClickEvent = self.make_mouse_double_click(image_name)
        self.create_buttons()

        # the crop area can be changed using the mouse wheel
        self.output_label_crop = (0., 0., 1., 1.)

        if len(self.image_list)>0:
            self.output_label_current_image = self.image_list[0]
            self.set_reference_label(self.image_list[0], update_viewers=True)
        else:
            self.output_label_current_image = ''
            self.output_label_reference_image = ''

    def clear_buttons(self):
        if self.button_layout is not None:
            # start clearing the layout
            # for i in range(self.button_layout.count()): self.button_layout.itemAt(i).widget().close()
            self.print_log(f"MultiView.clear_buttons() {self.image_list}")
            for image_name in reversed(self.image_list):
                if image_name in self.label:
                    self.button_layout.removeWidget(self.label[image_name])
                    self.label[image_name].close()

    def create_buttons(self):
        if self.button_layout is not None:
            max_grid_columns = 10
            idx = 0
            for image_name in self.image_list:
                # possibility to disable an image using the string 'none', especially useful for input image
                if image_name != 'none':
                    self.button_layout.addWidget(self.label[image_name], idx // max_grid_columns, idx % max_grid_columns)
                    idx += 1

    def layout_buttons(self, vertical_layout):
        self.button_widget = QtWidgets.QWidget(self)
        self.button_layout = QtWidgets.QGridLayout()
        self.button_layout.setHorizontalSpacing(0)
        self.button_layout.setVerticalSpacing(0)
        # button_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)
        self.create_buttons()
        vertical_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)
        # vertical_layout.setSizeConstraint(QtWidgets.QLayout.SetNoConstraint)
        self.button_widget.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
        self.button_widget.setLayout(self.button_layout)
        vertical_layout.addWidget(self.button_widget, 0, QtCore.Qt.AlignTop)

    def layout_parameters(self, parameters_layout):
        # Add Profiles and keep zoom options
        self.display_profiles = QtWidgets.QCheckBox("Profiles")
        self.display_profiles.stateChanged.connect(self.toggle_display_profiles)
        self.display_profiles.setChecked(False)
        parameters_layout.addWidget(self.display_profiles)
        self.keep_zoom = QtWidgets.QCheckBox("Keep zoom")
        self.keep_zoom.setChecked(False)
        parameters_layout.addWidget(self.keep_zoom)

        # Reset button
        self.reset_button = QtWidgets.QPushButton("reset")
        parameters_layout.addWidget(self.reset_button)
        self.reset_button.clicked.connect(self.reset_intensities)

        # Add color difference slider
        self.filter_params_gui.add_imdiff_factor(parameters_layout, self.update_image_intensity_event)

        # --- Saturation adjustment
        self.filter_params_gui.add_saturation(parameters_layout, self.update_image_intensity_event)
        # --- Black point adjustment
        self.filter_params_gui.add_blackpoint(parameters_layout, self.update_image_intensity_event)
        # --- white point adjustment
        self.filter_params_gui.add_whitepoint(parameters_layout, self.update_image_intensity_event)
        # --- Gamma adjustment
        self.filter_params_gui.add_gamma(parameters_layout, self.update_image_intensity_event)

    def layout_parameters_2(self, parameters2_layout):
        # --- G_R adjustment
        self.filter_params_gui.add_g_r(parameters2_layout, self.update_image_intensity_event)
        # --- G_B adjustment
        self.filter_params_gui.add_g_b(parameters2_layout, self.update_image_intensity_event)

    def update_layout(self):
        self.print_log("update_layout")
        vertical_layout = QtWidgets.QVBoxLayout()
        self.layout_buttons(vertical_layout)

        # First line of parameter control
        parameters_layout = QtWidgets.QHBoxLayout()
        self.layout_parameters(parameters_layout)
        vertical_layout.addLayout(parameters_layout, 1)

        # Second line of parameter control
        parameters2_layout = QtWidgets.QHBoxLayout()
        self.layout_parameters_2(parameters2_layout)
        vertical_layout.addLayout(parameters2_layout, 1)

        self.viewer_grid_layout = QtWidgets.QGridLayout()
        self.viewer_grid_layout.setHorizontalSpacing(1)
        self.viewer_grid_layout.setVerticalSpacing(1)
        self.set_number_of_viewers(1)
        vertical_layout.addLayout(self.viewer_grid_layout, 1)

        self.figures_widget = QtWidgets.QWidget()
        self.figures_layout = QtWidgets.QHBoxLayout()
        self.figures_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)
        # for the moment ignore this
        # self.figures_layout.addWidget(self.value_in_range_canvas)
        # self.figures_widget.setLayout(self.figures_layout)

        vertical_layout.addWidget(self.figures_widget)
        self.toggle_display_profiles()
        self.setLayout(vertical_layout)
        print("update_layout done")

    def toggle_display_profiles(self):
        self.figures_widget.setVisible(self.display_profiles.isChecked())
        self.update_image()

    def get_output_image(self, im_string_id):
        """
        Search for the image with given label in the current row
        if not in cache reads it and add it to the cache
        :param im_string_id: string that identifies the image to display
        :return:
        """
        # print(f"get_output_image({im_string_id}) ")
        start = get_time()

        image_filename = self.image_dict[im_string_id]
        image_transform = None
        self.print_log(f"MultiView.get_output_image() image_filename:{image_filename}")

        image_data, _ = self.cache.get_image(image_filename, self.read_size, verbose=self.show_timing_detailed(),
                                             use_RGB=not self.use_opengl, image_transform=image_transform)

        if image_data is not None:
            self.output_image_label[im_string_id] = image_filename
            output_image = image_data
        else:
            print(f"failed to get image {im_string_id}: {image_filename}")
            return None

        if self.show_timing_detailed():
            print(f" get_output_image took {int((get_time() - start)*1000+0.5)} ms".format)

        # force image bayer information if selected from menu
        res = output_image
        set_bayer = self.raw_bayer[self.current_raw_bayer]
        if res.channels in [ImageFormat.CH_BGGR, ImageFormat.CH_GBRG, ImageFormat.CH_GRBG, ImageFormat.CH_RGGB] and set_bayer is not None:
            print(f"Setting bayer {set_bayer}")
            res.channels = set_bayer

        return res

    def set_message_callback(self, message_cb):
        self.message_cb = message_cb

    def setMessage(self, mess):
        if self.message_cb is not None:
            self.message_cb(mess)

    def cache_read_images(self, image_filenames: List[str], reload: bool =False) -> None:
        """ Read the list of images into the cache, with option to reload them from disk

        Args:
            image_filenames (List[str]): list of image filenames
            reload (bool, optional): reload removes first the images from the ImageCache 
                before adding them. Defaults to False.
        """        
        # print(f"cache_read_images({image_filenames}) ")
        image_transform = None
        if reload:
            for f in image_filenames:
                self.cache.remove(f)
        self.cache.add_images(image_filenames, self.read_size, verbose=False, use_RGB=not self.use_opengl,
                             image_transform=image_transform)

    def update_label_fonts(self):
        # Update selected image label, we could do it later too
        for im_name in self.image_list:
            # possibility to disable an image using the string 'none', especially useful for input image
            if im_name != 'none':
                is_bold      = im_name == self.output_label_current_image
                is_underline = im_name == self.output_label_reference_image
                is_bold |= is_underline
                self.bold_font.setBold(is_bold)
                self.bold_font.setUnderline(is_underline)
                self.bold_font.setPointSize(8)
                self.label[im_name].setFont(self.bold_font)
                self.label[im_name].setWordWrap(True)
            # self.label[im_name].setMaximumWidth(160)

    def update_image(self, image_name=None, reload=False):
        """
        Uses the variable self.output_label_current_image
        :return:
        """
        self.print_log('update_image {} current: {}'.format(image_name, self.output_label_current_image))
        update_image_start = get_time()

        # Define the current selected image
        if image_name is not None:
            self.output_label_current_image = image_name
        if self.output_label_current_image == "":
            return

        if self.image_dict[self.output_label_current_image] is None:
            print(" No image filename for current image")
            return

        self.update_label_fonts()

        # find first active window
        first_active_window = 0
        for n in range(self.nb_viewers_used):
            self.image_viewers[n].display_timing = self.show_timing()>0
            if self.image_viewers[n].is_active():
                first_active_window = n
                break

        # Read images in parallel to improve preformances
        # list all required image filenames
        # set all viewers image names (labels)
        image_filenames = [self.image_dict[self.output_label_current_image]]
        # define image associated to each used viewer and add it to the list of images to get
        for n in range(self.nb_viewers_used):
            viewer : ImageViewer = self.image_viewers[n]
            # Set active only the first active window
            viewer.set_active(n == first_active_window)
            if viewer.get_image() is None:
                if n < len(self.image_list):
                    viewer.image_name = self.image_list[n]
                    image_filenames.append(self.image_dict[self.image_list[n]])
                else:
                    viewer.image_name = self.output_label_current_image
            else:
                # image_name should belong to image_dict
                if viewer.image_name in self.image_dict:
                    image_filenames.append(self.image_dict[viewer.image_name])
                else:
                    viewer.image_name = self.output_label_current_image

        # remove duplicates
        image_filenames = list(set(image_filenames))
        # print(f"image filenames {image_filenames}")
        self.cache_read_images(image_filenames, reload=reload)

        try:
            current_image = self.get_output_image(self.output_label_current_image)
            if current_image is None:
                return
        except Exception as e:
            print("Error: failed to get image {}: {}".format(self.output_label_current_image, e))
            return

        # print(f"cur {self.output_label_current_image}")
        current_filename = self.output_image_label[self.output_label_current_image]

        if self.show_timing_detailed():
            time_spent = get_time() - update_image_start

        self.setMessage("Image: {0}".format(current_filename))

        current_viewer = self.image_viewers[first_active_window]
        if self.save_image_clipboard:
            print("set save image to clipboard")
            current_viewer.set_clipboard(self.clip, True)
        current_viewer.set_active(True)
        current_viewer.image_name = self.output_label_current_image
        current_viewer.set_image(current_image)
        if self.save_image_clipboard:
            print("end save image to clipboard")
            current_viewer.set_clipboard(None, False)

        # print(f"ref {self.output_label_reference_image}")
        if self.output_label_reference_image==self.output_label_current_image:
            reference_image = current_image
        else:
            reference_image = self.get_output_image(self.output_label_reference_image)

        if self.nb_viewers_used >= 2:
            prev_n = first_active_window
            for n in range(1, self.nb_viewers_used):
                n1 = (first_active_window + n) % self.nb_viewers_used
                viewer = self.image_viewers[n1]
                # viewer image has already been defined
                # try to update corresponding images in row
                try:
                    viewer_image = self.get_output_image(viewer.image_name)
                except Exception as e:
                    print("Error: failed to get image {}: {}".format(viewer.image_name, e))
                    viewer.set_image(current_image)
                else:
                    viewer.set_image(viewer_image)

                # set reference image
                viewer.set_image_ref(reference_image)

                self.image_viewers[prev_n].set_synchronize(viewer)
                prev_n = n1
            # Create a synchronization loop
            if prev_n != first_active_window:
                self.image_viewers[prev_n].set_synchronize(self.image_viewers[first_active_window])

        # Be sure to show the required viewers
        for n in range(self.nb_viewers_used):
            viewer = self.image_viewers[n]
            # print(f"show viewer {n}")
            # Note: calling show in any case seems to avoid double calls to paint event that update() triggers
            # viewer.show()
            if viewer.isHidden():
                # print(f"show viewer {n}")
                viewer.show()
            else:
                # print(f"update viewer {n}")
                viewer.update()


        # self.image_scroll_area.adjustSize()
        # if self.show_timing():
        print(f" Update image took {(get_time() - update_image_start)*1000:0.0f} ms")

    def set_number_of_viewers(self, nb_viewers: int = 1, max_columns : int = 0) -> None:
        self.print_log("*** set_number_of_viewers()")

        # 1. remove current viewers from grid layout
        # self.viewer_grid_layout.hide()
        for v in self.image_viewers:
            v.hide()
            self.viewer_grid_layout.removeWidget(v)

        self.nb_viewers_used : int = nb_viewers
        print(f"max_columns = {max_columns}")
        if max_columns>0:
            row_length = min(self.nb_viewers_used, max_columns)
            col_length = int(math.ceil(self.nb_viewers_used / row_length))
        else:
            # Find best configuration to fill the space based on image size and widget size?
            col_length = int(math.sqrt(self.nb_viewers_used))
            row_length = int(math.ceil(self.nb_viewers_used / col_length))
        self.print_log('col_length = {} row_length = {}'.format(col_length, row_length))
        # be sure to have enough image viewers allocated
        while self.nb_viewers_used > len(self.allocated_image_viewers):
            viewer = self.image_viewer_class()
            viewer.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
            self.allocated_image_viewers.append(viewer)

        self.image_viewers = self.allocated_image_viewers[:self.nb_viewers_used]

        for n in range(self.nb_viewers_used):
            self.viewer_grid_layout.addWidget(self.image_viewers[n], int(n / float(row_length)), n % row_length)
            self.image_viewers[n].hide()

        # for n in range(self.nb_viewers_used):
        #     print("Viewer {} size {}".format(n, (self.image_viewers[n].width(), self.image_viewers[n].height())))

    def set_number_of_viewers_callback(self):
        self.set_number_of_viewers()
        self.viewer_grid_layout.update()
        self.update_image()

    def keyReleaseEvent(self, event):
        if type(event) == QtGui.QKeyEvent:
            modifiers = QtWidgets.QApplication.keyboardModifiers()
            # allow to switch between images by pressing Alt+'image position' (Alt+0, Alt+1, etc)
            if modifiers & (QtCore.Qt.AltModifier | QtCore.Qt.ControlModifier):
                event.accept()
            # else:
            #     try:
            #         # reset reference image
            #         if self.output_label_current_image != self.output_label_reference_image:
            #             self.update_image(self.output_label_reference_image)
            #     except Exception as e:
            #         print(" Error: {}".format(e))

    def find_in_layout(self, layout: QtWidgets.QLayout) -> Optional[QtWidgets.QLayout]:
        """ Search Recursivement in Layouts for the current widget

        Args:
            layout (QtWidgets.QLayout): input layout for search

        Returns:
            layout containing the current widget or None if not found
        """
        print("find_in_layout()")
        if layout.indexOf(self) != -1: return layout
        for i in range(layout.count()):
            item = layout.itemAt(i)
            if item.widget() == self: return layout
            if (l := item.layout()) and (found:=self.find_in_layout(l)): return l
        print("find_in_layout() return None")
        return None

    def toggle_fullscreen(self, event):
        print(f"toggle_fullscreen")
        if not issubclass(self.__class__,QtWidgets.QWidget):
            print(f"Cannot use toggle_fullscreen on a class that is not a QWidget")
            return
        # Should be inside a layout
        if self.before_max_parent is None:
            print(f"self.parent() is not None {self.parent() is not None}")
            print(f"self.parent().layout() {self.parent().layout()} ")
            if self.parent() is not None and (playout := self.parent().layout()) is not None:
                if self.find_in_layout(playout):
                    self.before_max_parent = self.parent()
                    self.replacing_widget = QtWidgets.QWidget(self.before_max_parent)
                    self.parent().layout().replaceWidget(self, self.replacing_widget)
                    # We need to go up from the parent widget to the main window to get its geometry
                    # so that the fullscreen is display on the same monitor
                    toplevel_parent : Optional[QtWidgets.QWidget] = self.parentWidget()
                    while toplevel_parent.parentWidget(): toplevel_parent = toplevel_parent.parentWidget()
                    self.setParent(None)
                    if toplevel_parent: self.setGeometry(toplevel_parent.geometry())
                    self.showFullScreen()
                    event.accept()
                    return
        if self.before_max_parent is not None:
            self.setParent(self.before_max_parent)
            self.parent().layout().replaceWidget(self.replacing_widget, self)
            self.replacing_widget = self.before_max_parent = None
            # self.resize(self.before_max_size)
            self.show()
            self.parent().update()
            self.setFocus()
            event.accept()
            return



    def keyPressEvent(self, event):
        if type(event) == QtGui.QKeyEvent:
            # print("key is ", event.key())
            self.print_log(f" QKeySequence() {QtGui.QKeySequence(event.key()).toString()}")
            # print( QtGui.QKeySequence(event.key()).toString())
            # print(f" capslock: {event.getModifierState('CapsLock')}")
            if self.show_trace():
                print("key is ", event.key())
            modifiers = QtWidgets.QApplication.keyboardModifiers()
            # F1: open help in browser
            if event.key() == QtCore.Qt.Key_F1:
                import qimview
                mb = QtWidgets.QMessageBox(self)
                mb.setWindowTitle(f"qimview {qimview.__version__}: MultiView help")
                mb.setTextFormat(QtCore.Qt.TextFormat.RichText)
                mb.setText(
                    "<a href='https://github.com/qimview/qimview/wiki'>qimview</a><br>"
                    "<a href='https://github.com/qimview/qimview/wiki/4.-Multi%E2%80%90image-viewer'>MultiImage Viewer</a><br>"
                    "<a href='https://github.com/qimview/qimview/wiki/3.-Image-Viewers'>Image Viewer</a>")
                mb.exec()
                event.accept()
                return

            # F5: reload images
            if event.key() == QtCore.Qt.Key_F5:
                self.update_image(reload=True)
                event.accept()
                return

            if event.key() == QtCore.Qt.Key_F11:
                # Should be inside a layout
                print("MultiView F11 pressed")
                self.toggle_fullscreen(event)
                return

            # allow to switch between images by pressing Alt+'image position' (Alt+0, Alt+1, etc)
            if modifiers & QtCore.Qt.AltModifier:
                for n in range(len(self.image_list)):
                    if self.image_list[n] is not None:
                        if event.key() == QtCore.Qt.Key_0 + n:
                            if self.output_label_current_image != self.image_list[n]:
                                # with Alt+Ctrl, change reference image
                                # if modifiers & QtCore.Qt.ControlModifier:
                                #     self.set_reference_label(self.image_list[n])
                                self.update_image(self.image_list[n])
                                self.setFocus()
                                return
                event.accept()
                return

            if event.modifiers() & QtCore.Qt.ControlModifier:
                # allow to switch between images by pressing Ctrl+'image position' (Ctrl+0, Ctrl+1, etc)
                for n in range(len(self.image_list)):
                    if self.image_list[n] != 'none':
                        if event.key() == QtCore.Qt.Key_0 + n:
                            if self.output_label_current_image != self.image_list[n]:
                                self.set_reference_label(self.image_list[n], update_viewers=True)
                                self.update_image()
                                event.accept()
                                return
                return
            # print(f"event.modifiers {event.modifiers()}")
            # if not event.modifiers():
            for n in range(1, 10):
                if event.key() == QtCore.Qt.Key_0 + n:
                    self.set_number_of_viewers(n)
                    self.viewer_grid_layout.update()
                    self.update_image()
                    self.setFocus()
                    event.accept()
                    return

            if event.key() == QtCore.Qt.Key_Up:
                if self.key_up_callback is not None:
                    self.key_up_callback()
                event.accept()
                return

            if event.key() == QtCore.Qt.Key_Down:
                if self.key_down_callback is not None:
                    self.key_down_callback()
                event.accept()
                return

            nb_images = len(self.image_list)
            if event.key() == QtCore.Qt.Key_Left:
                for n in range(nb_images):
                    if self.output_label_current_image == self.image_list[n]:
                        print(f"setting new image index {(n+nb_images-1)%nb_images}")
                        self.update_image(self.image_list[(n+nb_images-1)%nb_images])
                        event.accept()
                        return

            if event.key() == QtCore.Qt.Key_Right:
                for n in range(nb_images):
                    if self.output_label_current_image == self.image_list[n]:
                        print(f"setting new image index {(n+nb_images+1)%nb_images}")
                        self.update_image(self.image_list[(n+1)%nb_images])
                        event.accept()
                        return
                
            # G: display number of columns
            if event.key() == QtCore.Qt.Key_G:
                self.max_columns = int ((self.max_columns + 1) % self.nb_viewers_used + 1)
                self.set_number_of_viewers(self.nb_viewers_used, max_columns=self.max_columns)
                self.update_image(reload=True)
                self.setFocus()
                event.accept()
                return

        else:
            event.ignore()

Ancestors

  • PySide6.QtWidgets.QWidget
  • PySide6.QtCore.QObject
  • PySide6.QtGui.QPaintDevice
  • Shiboken.Object

Class variables

var staticMetaObject

Methods

def add_context_menu(self)
Expand source code
def add_context_menu(self):
    self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
    self.customContextMenuRequested.connect(self.show_context_menu)
    self._context_menu = QtWidgets.QMenu()
    self.viewer_modes = {}
    for v in ViewerType:
        self.viewer_modes[v.name] = v
    self._default_viewer_mode = ViewerType.QT_VIEWER.name
    self.viewer_mode_selection = MenuSelection("Viewer mode", 
        self._context_menu, self.viewer_modes, self._default_viewer_mode, self.update_viewer_mode)
    self._context_menu.addSeparator()
    action = self._context_menu.addAction("Reset viewers")
    action.triggered.connect(self.reset_viewers)
def cache_read_images(self, image_filenames: List[str], reload: bool = False) ‑> None

Read the list of images into the cache, with option to reload them from disk

Args

image_filenames : List[str]
list of image filenames
reload : bool, optional
reload removes first the images from the ImageCache before adding them. Defaults to False.
Expand source code
def cache_read_images(self, image_filenames: List[str], reload: bool =False) -> None:
    """ Read the list of images into the cache, with option to reload them from disk

    Args:
        image_filenames (List[str]): list of image filenames
        reload (bool, optional): reload removes first the images from the ImageCache 
            before adding them. Defaults to False.
    """        
    # print(f"cache_read_images({image_filenames}) ")
    image_transform = None
    if reload:
        for f in image_filenames:
            self.cache.remove(f)
    self.cache.add_images(image_filenames, self.read_size, verbose=False, use_RGB=not self.use_opengl,
                         image_transform=image_transform)
def check_verbosity(self, flag)
Expand source code
def check_verbosity(self, flag):
    return self.verbosity & flag
def clear_buttons(self)
Expand source code
def clear_buttons(self):
    if self.button_layout is not None:
        # start clearing the layout
        # for i in range(self.button_layout.count()): self.button_layout.itemAt(i).widget().close()
        self.print_log(f"MultiView.clear_buttons() {self.image_list}")
        for image_name in reversed(self.image_list):
            if image_name in self.label:
                self.button_layout.removeWidget(self.label[image_name])
                self.label[image_name].close()
def create_buttons(self)
Expand source code
def create_buttons(self):
    if self.button_layout is not None:
        max_grid_columns = 10
        idx = 0
        for image_name in self.image_list:
            # possibility to disable an image using the string 'none', especially useful for input image
            if image_name != 'none':
                self.button_layout.addWidget(self.label[image_name], idx // max_grid_columns, idx % max_grid_columns)
                idx += 1
def find_in_layout(self, layout: PySide6.QtWidgets.QLayout) ‑> Optional[PySide6.QtWidgets.QLayout]

Search Recursivement in Layouts for the current widget

Args

layout : QtWidgets.QLayout
input layout for search

Returns

layout containing the current widget or None if not found

Expand source code
def find_in_layout(self, layout: QtWidgets.QLayout) -> Optional[QtWidgets.QLayout]:
    """ Search Recursivement in Layouts for the current widget

    Args:
        layout (QtWidgets.QLayout): input layout for search

    Returns:
        layout containing the current widget or None if not found
    """
    print("find_in_layout()")
    if layout.indexOf(self) != -1: return layout
    for i in range(layout.count()):
        item = layout.itemAt(i)
        if item.widget() == self: return layout
        if (l := item.layout()) and (found:=self.find_in_layout(l)): return l
    print("find_in_layout() return None")
    return None
def get_output_image(self, im_string_id)

Search for the image with given label in the current row if not in cache reads it and add it to the cache :param im_string_id: string that identifies the image to display :return:

Expand source code
def get_output_image(self, im_string_id):
    """
    Search for the image with given label in the current row
    if not in cache reads it and add it to the cache
    :param im_string_id: string that identifies the image to display
    :return:
    """
    # print(f"get_output_image({im_string_id}) ")
    start = get_time()

    image_filename = self.image_dict[im_string_id]
    image_transform = None
    self.print_log(f"MultiView.get_output_image() image_filename:{image_filename}")

    image_data, _ = self.cache.get_image(image_filename, self.read_size, verbose=self.show_timing_detailed(),
                                         use_RGB=not self.use_opengl, image_transform=image_transform)

    if image_data is not None:
        self.output_image_label[im_string_id] = image_filename
        output_image = image_data
    else:
        print(f"failed to get image {im_string_id}: {image_filename}")
        return None

    if self.show_timing_detailed():
        print(f" get_output_image took {int((get_time() - start)*1000+0.5)} ms".format)

    # force image bayer information if selected from menu
    res = output_image
    set_bayer = self.raw_bayer[self.current_raw_bayer]
    if res.channels in [ImageFormat.CH_BGGR, ImageFormat.CH_GBRG, ImageFormat.CH_GRBG, ImageFormat.CH_RGGB] and set_bayer is not None:
        print(f"Setting bayer {set_bayer}")
        res.channels = set_bayer

    return res
def keyPressEvent(self, event)

keyPressEvent(self, event: PySide6.QtGui.QKeyEvent) -> None

Expand source code
def keyPressEvent(self, event):
    if type(event) == QtGui.QKeyEvent:
        # print("key is ", event.key())
        self.print_log(f" QKeySequence() {QtGui.QKeySequence(event.key()).toString()}")
        # print( QtGui.QKeySequence(event.key()).toString())
        # print(f" capslock: {event.getModifierState('CapsLock')}")
        if self.show_trace():
            print("key is ", event.key())
        modifiers = QtWidgets.QApplication.keyboardModifiers()
        # F1: open help in browser
        if event.key() == QtCore.Qt.Key_F1:
            import qimview
            mb = QtWidgets.QMessageBox(self)
            mb.setWindowTitle(f"qimview {qimview.__version__}: MultiView help")
            mb.setTextFormat(QtCore.Qt.TextFormat.RichText)
            mb.setText(
                "<a href='https://github.com/qimview/qimview/wiki'>qimview</a><br>"
                "<a href='https://github.com/qimview/qimview/wiki/4.-Multi%E2%80%90image-viewer'>MultiImage Viewer</a><br>"
                "<a href='https://github.com/qimview/qimview/wiki/3.-Image-Viewers'>Image Viewer</a>")
            mb.exec()
            event.accept()
            return

        # F5: reload images
        if event.key() == QtCore.Qt.Key_F5:
            self.update_image(reload=True)
            event.accept()
            return

        if event.key() == QtCore.Qt.Key_F11:
            # Should be inside a layout
            print("MultiView F11 pressed")
            self.toggle_fullscreen(event)
            return

        # allow to switch between images by pressing Alt+'image position' (Alt+0, Alt+1, etc)
        if modifiers & QtCore.Qt.AltModifier:
            for n in range(len(self.image_list)):
                if self.image_list[n] is not None:
                    if event.key() == QtCore.Qt.Key_0 + n:
                        if self.output_label_current_image != self.image_list[n]:
                            # with Alt+Ctrl, change reference image
                            # if modifiers & QtCore.Qt.ControlModifier:
                            #     self.set_reference_label(self.image_list[n])
                            self.update_image(self.image_list[n])
                            self.setFocus()
                            return
            event.accept()
            return

        if event.modifiers() & QtCore.Qt.ControlModifier:
            # allow to switch between images by pressing Ctrl+'image position' (Ctrl+0, Ctrl+1, etc)
            for n in range(len(self.image_list)):
                if self.image_list[n] != 'none':
                    if event.key() == QtCore.Qt.Key_0 + n:
                        if self.output_label_current_image != self.image_list[n]:
                            self.set_reference_label(self.image_list[n], update_viewers=True)
                            self.update_image()
                            event.accept()
                            return
            return
        # print(f"event.modifiers {event.modifiers()}")
        # if not event.modifiers():
        for n in range(1, 10):
            if event.key() == QtCore.Qt.Key_0 + n:
                self.set_number_of_viewers(n)
                self.viewer_grid_layout.update()
                self.update_image()
                self.setFocus()
                event.accept()
                return

        if event.key() == QtCore.Qt.Key_Up:
            if self.key_up_callback is not None:
                self.key_up_callback()
            event.accept()
            return

        if event.key() == QtCore.Qt.Key_Down:
            if self.key_down_callback is not None:
                self.key_down_callback()
            event.accept()
            return

        nb_images = len(self.image_list)
        if event.key() == QtCore.Qt.Key_Left:
            for n in range(nb_images):
                if self.output_label_current_image == self.image_list[n]:
                    print(f"setting new image index {(n+nb_images-1)%nb_images}")
                    self.update_image(self.image_list[(n+nb_images-1)%nb_images])
                    event.accept()
                    return

        if event.key() == QtCore.Qt.Key_Right:
            for n in range(nb_images):
                if self.output_label_current_image == self.image_list[n]:
                    print(f"setting new image index {(n+nb_images+1)%nb_images}")
                    self.update_image(self.image_list[(n+1)%nb_images])
                    event.accept()
                    return
            
        # G: display number of columns
        if event.key() == QtCore.Qt.Key_G:
            self.max_columns = int ((self.max_columns + 1) % self.nb_viewers_used + 1)
            self.set_number_of_viewers(self.nb_viewers_used, max_columns=self.max_columns)
            self.update_image(reload=True)
            self.setFocus()
            event.accept()
            return

    else:
        event.ignore()
def keyReleaseEvent(self, event)

keyReleaseEvent(self, event: PySide6.QtGui.QKeyEvent) -> None

Expand source code
def keyReleaseEvent(self, event):
    if type(event) == QtGui.QKeyEvent:
        modifiers = QtWidgets.QApplication.keyboardModifiers()
        # allow to switch between images by pressing Alt+'image position' (Alt+0, Alt+1, etc)
        if modifiers & (QtCore.Qt.AltModifier | QtCore.Qt.ControlModifier):
            event.accept()
        # else:
        #     try:
        #         # reset reference image
        #         if self.output_label_current_image != self.output_label_reference_image:
        #             self.update_image(self.output_label_reference_image)
        #     except Exception as e:
        #         print(" Error: {}".format(e))
def layout_buttons(self, vertical_layout)
Expand source code
def layout_buttons(self, vertical_layout):
    self.button_widget = QtWidgets.QWidget(self)
    self.button_layout = QtWidgets.QGridLayout()
    self.button_layout.setHorizontalSpacing(0)
    self.button_layout.setVerticalSpacing(0)
    # button_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)
    self.create_buttons()
    vertical_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)
    # vertical_layout.setSizeConstraint(QtWidgets.QLayout.SetNoConstraint)
    self.button_widget.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
    self.button_widget.setLayout(self.button_layout)
    vertical_layout.addWidget(self.button_widget, 0, QtCore.Qt.AlignTop)
def layout_parameters(self, parameters_layout)
Expand source code
def layout_parameters(self, parameters_layout):
    # Add Profiles and keep zoom options
    self.display_profiles = QtWidgets.QCheckBox("Profiles")
    self.display_profiles.stateChanged.connect(self.toggle_display_profiles)
    self.display_profiles.setChecked(False)
    parameters_layout.addWidget(self.display_profiles)
    self.keep_zoom = QtWidgets.QCheckBox("Keep zoom")
    self.keep_zoom.setChecked(False)
    parameters_layout.addWidget(self.keep_zoom)

    # Reset button
    self.reset_button = QtWidgets.QPushButton("reset")
    parameters_layout.addWidget(self.reset_button)
    self.reset_button.clicked.connect(self.reset_intensities)

    # Add color difference slider
    self.filter_params_gui.add_imdiff_factor(parameters_layout, self.update_image_intensity_event)

    # --- Saturation adjustment
    self.filter_params_gui.add_saturation(parameters_layout, self.update_image_intensity_event)
    # --- Black point adjustment
    self.filter_params_gui.add_blackpoint(parameters_layout, self.update_image_intensity_event)
    # --- white point adjustment
    self.filter_params_gui.add_whitepoint(parameters_layout, self.update_image_intensity_event)
    # --- Gamma adjustment
    self.filter_params_gui.add_gamma(parameters_layout, self.update_image_intensity_event)
def layout_parameters_2(self, parameters2_layout)
Expand source code
def layout_parameters_2(self, parameters2_layout):
    # --- G_R adjustment
    self.filter_params_gui.add_g_r(parameters2_layout, self.update_image_intensity_event)
    # --- G_B adjustment
    self.filter_params_gui.add_g_b(parameters2_layout, self.update_image_intensity_event)
def make_mouse_double_click(self, image_name)
Expand source code
def make_mouse_double_click(self, image_name):
    def mouse_double_click(obj, event):
        '''
        Sets the double clicked label as the reference image
        :param obj:
        :param event:
        '''
        print('mouse_double_click {}'.format(image_name))
        obj.output_label_reference_image = image_name
        obj.output_label_current_image = obj.output_label_reference_image
        obj.update_image()

    return types.MethodType(mouse_double_click, self)
def make_mouse_press(self, image_name)
Expand source code
def make_mouse_press(self, image_name):
    def mouse_press(obj, event):
        print('mouse_press')
        obj.update_image(image_name)

    return types.MethodType(mouse_press, self)
def mouse_release(self, event)
Expand source code
def mouse_release(self, event):
    self.update_image(self.output_label_reference_image)
def print_log(self, mess)
Expand source code
def print_log(self, mess):
    if self.verbosity & self.verbosity_LIGHT:
        print(mess)
def reset_intensities(self)
Expand source code
def reset_intensities(self):
    self.filter_params_gui.reset_all()
def reset_viewers(self)
Expand source code
def reset_viewers(self):
    for v in self.image_viewers:
        v.hide()
        self.viewer_grid_layout.removeWidget(v)
    self.allocated_image_viewers.clear()
    self.image_viewers.clear()
    # Create viewer instances
    for n in range(self.nb_viewers_used):
        viewer = self.image_viewer_class()
        viewer.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
        self.allocated_image_viewers.append(viewer)
        self.image_viewers.append(viewer)
    self.set_number_of_viewers(self.nb_viewers_used)
    self.viewer_grid_layout.update()
    self.update_image()
def setMessage(self, mess)
Expand source code
def setMessage(self, mess):
    if self.message_cb is not None:
        self.message_cb(mess)
def set_cache_memory_bar(self, progress_bar)
Expand source code
def set_cache_memory_bar(self, progress_bar):
    self.cache.set_memory_bar(progress_bar)
def set_images(self, images, set_viewers=False)
Expand source code
def set_images(self, images, set_viewers=False):
    self.print_log(f"MultiView.set_images() {images}")
    if images.keys() == self.image_dict.keys():
        self.image_dict = images
        self.update_reference()
    else:
        self.image_dict = images
        self.update_image_buttons()
def set_key_down_callback(self, c)
Expand source code
def set_key_down_callback(self, c):
    self.key_down_callback = c
def set_key_up_callback(self, c)
Expand source code
def set_key_up_callback(self, c):
    self.key_up_callback = c
def set_message_callback(self, message_cb)
Expand source code
def set_message_callback(self, message_cb):
    self.message_cb = message_cb
def set_number_of_viewers(self, nb_viewers: int = 1, max_columns: int = 0) ‑> None
Expand source code
def set_number_of_viewers(self, nb_viewers: int = 1, max_columns : int = 0) -> None:
    self.print_log("*** set_number_of_viewers()")

    # 1. remove current viewers from grid layout
    # self.viewer_grid_layout.hide()
    for v in self.image_viewers:
        v.hide()
        self.viewer_grid_layout.removeWidget(v)

    self.nb_viewers_used : int = nb_viewers
    print(f"max_columns = {max_columns}")
    if max_columns>0:
        row_length = min(self.nb_viewers_used, max_columns)
        col_length = int(math.ceil(self.nb_viewers_used / row_length))
    else:
        # Find best configuration to fill the space based on image size and widget size?
        col_length = int(math.sqrt(self.nb_viewers_used))
        row_length = int(math.ceil(self.nb_viewers_used / col_length))
    self.print_log('col_length = {} row_length = {}'.format(col_length, row_length))
    # be sure to have enough image viewers allocated
    while self.nb_viewers_used > len(self.allocated_image_viewers):
        viewer = self.image_viewer_class()
        viewer.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
        self.allocated_image_viewers.append(viewer)

    self.image_viewers = self.allocated_image_viewers[:self.nb_viewers_used]

    for n in range(self.nb_viewers_used):
        self.viewer_grid_layout.addWidget(self.image_viewers[n], int(n / float(row_length)), n % row_length)
        self.image_viewers[n].hide()

    # for n in range(self.nb_viewers_used):
    #     print("Viewer {} size {}".format(n, (self.image_viewers[n].width(), self.image_viewers[n].height())))
def set_number_of_viewers_callback(self)
Expand source code
def set_number_of_viewers_callback(self):
    self.set_number_of_viewers()
    self.viewer_grid_layout.update()
    self.update_image()
def set_read_size(self, read_size)
Expand source code
def set_read_size(self, read_size):
    self.read_size = read_size
    # reset cache
    self.cache.reset()
def set_reference_label(self, ref: str, update_viewers=False) ‑> None
Expand source code
def set_reference_label(self, ref: str, update_viewers=False) -> None:
    try:
        if ref is not None:
            if ref!=self.output_label_reference_image:
                self.output_label_reference_image = ref
                if update_viewers:
                    self.update_reference()
    except Exception as e:
        print(f' Failed to set reference label {e}')
def set_verbosity(self, flag, enable=True)

:param v: verbosity flags :param b: boolean to enable or disable flag :return:

Expand source code
def set_verbosity(self, flag, enable=True):
    """
    :param v: verbosity flags
    :param b: boolean to enable or disable flag
    :return:
    """
    if enable:
        self.verbosity = self.verbosity | flag
    else:
        self.verbosity = self.verbosity & ~flag
def set_viewer_images(self)

Set viewer images based on self.image_dict.keys() :return:

Expand source code
def set_viewer_images(self):
    """
    Set viewer images based on self.image_dict.keys()
    :return:
    """
    # if set_viewers, we force the viewer layout and images based on the list
    # be sure to have enough image viewers allocated
    while self.nb_viewers_used > len(self.allocated_image_viewers):
        viewer = self.image_viewer_class()
        viewer.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
        self.allocated_image_viewers.append(viewer)
    self.image_viewers = self.allocated_image_viewers[:self.nb_viewers_used]
    image_names = list(self.image_dict.keys())
    for n in range(self.nb_viewers_used):
        if n < len(image_names):
            self.image_viewers[n].image_name = image_names[n]
        else:
            self.image_viewers[n].image_name = image_names[len(image_names)-1]
def show_context_menu(self, pos)
Expand source code
def show_context_menu(self, pos):
    # allow to switch between images by pressing Alt+'image position' (Alt+0, Alt+1, etc)
    self._context_menu.show()
    self._context_menu.popup( self.mapToGlobal(pos) )
def show_timing(self)
Expand source code
def show_timing(self):
    return self.check_verbosity(self.verbosity_TIMING) or self.check_verbosity(self.verbosity_TIMING_DETAILED)
def show_timing_detailed(self)
Expand source code
def show_timing_detailed(self):
    return self.check_verbosity(self.verbosity_TIMING_DETAILED)
def show_trace(self)
Expand source code
def show_trace(self):
    return self.check_verbosity(self.verbosity_TRACE)
def toggle_display_profiles(self)
Expand source code
def toggle_display_profiles(self):
    self.figures_widget.setVisible(self.display_profiles.isChecked())
    self.update_image()
def toggle_fullscreen(self, event)
Expand source code
def toggle_fullscreen(self, event):
    print(f"toggle_fullscreen")
    if not issubclass(self.__class__,QtWidgets.QWidget):
        print(f"Cannot use toggle_fullscreen on a class that is not a QWidget")
        return
    # Should be inside a layout
    if self.before_max_parent is None:
        print(f"self.parent() is not None {self.parent() is not None}")
        print(f"self.parent().layout() {self.parent().layout()} ")
        if self.parent() is not None and (playout := self.parent().layout()) is not None:
            if self.find_in_layout(playout):
                self.before_max_parent = self.parent()
                self.replacing_widget = QtWidgets.QWidget(self.before_max_parent)
                self.parent().layout().replaceWidget(self, self.replacing_widget)
                # We need to go up from the parent widget to the main window to get its geometry
                # so that the fullscreen is display on the same monitor
                toplevel_parent : Optional[QtWidgets.QWidget] = self.parentWidget()
                while toplevel_parent.parentWidget(): toplevel_parent = toplevel_parent.parentWidget()
                self.setParent(None)
                if toplevel_parent: self.setGeometry(toplevel_parent.geometry())
                self.showFullScreen()
                event.accept()
                return
    if self.before_max_parent is not None:
        self.setParent(self.before_max_parent)
        self.parent().layout().replaceWidget(self.replacing_widget, self)
        self.replacing_widget = self.before_max_parent = None
        # self.resize(self.before_max_size)
        self.show()
        self.parent().update()
        self.setFocus()
        event.accept()
        return
def update_image(self, image_name=None, reload=False)

Uses the variable self.output_label_current_image :return:

Expand source code
def update_image(self, image_name=None, reload=False):
    """
    Uses the variable self.output_label_current_image
    :return:
    """
    self.print_log('update_image {} current: {}'.format(image_name, self.output_label_current_image))
    update_image_start = get_time()

    # Define the current selected image
    if image_name is not None:
        self.output_label_current_image = image_name
    if self.output_label_current_image == "":
        return

    if self.image_dict[self.output_label_current_image] is None:
        print(" No image filename for current image")
        return

    self.update_label_fonts()

    # find first active window
    first_active_window = 0
    for n in range(self.nb_viewers_used):
        self.image_viewers[n].display_timing = self.show_timing()>0
        if self.image_viewers[n].is_active():
            first_active_window = n
            break

    # Read images in parallel to improve preformances
    # list all required image filenames
    # set all viewers image names (labels)
    image_filenames = [self.image_dict[self.output_label_current_image]]
    # define image associated to each used viewer and add it to the list of images to get
    for n in range(self.nb_viewers_used):
        viewer : ImageViewer = self.image_viewers[n]
        # Set active only the first active window
        viewer.set_active(n == first_active_window)
        if viewer.get_image() is None:
            if n < len(self.image_list):
                viewer.image_name = self.image_list[n]
                image_filenames.append(self.image_dict[self.image_list[n]])
            else:
                viewer.image_name = self.output_label_current_image
        else:
            # image_name should belong to image_dict
            if viewer.image_name in self.image_dict:
                image_filenames.append(self.image_dict[viewer.image_name])
            else:
                viewer.image_name = self.output_label_current_image

    # remove duplicates
    image_filenames = list(set(image_filenames))
    # print(f"image filenames {image_filenames}")
    self.cache_read_images(image_filenames, reload=reload)

    try:
        current_image = self.get_output_image(self.output_label_current_image)
        if current_image is None:
            return
    except Exception as e:
        print("Error: failed to get image {}: {}".format(self.output_label_current_image, e))
        return

    # print(f"cur {self.output_label_current_image}")
    current_filename = self.output_image_label[self.output_label_current_image]

    if self.show_timing_detailed():
        time_spent = get_time() - update_image_start

    self.setMessage("Image: {0}".format(current_filename))

    current_viewer = self.image_viewers[first_active_window]
    if self.save_image_clipboard:
        print("set save image to clipboard")
        current_viewer.set_clipboard(self.clip, True)
    current_viewer.set_active(True)
    current_viewer.image_name = self.output_label_current_image
    current_viewer.set_image(current_image)
    if self.save_image_clipboard:
        print("end save image to clipboard")
        current_viewer.set_clipboard(None, False)

    # print(f"ref {self.output_label_reference_image}")
    if self.output_label_reference_image==self.output_label_current_image:
        reference_image = current_image
    else:
        reference_image = self.get_output_image(self.output_label_reference_image)

    if self.nb_viewers_used >= 2:
        prev_n = first_active_window
        for n in range(1, self.nb_viewers_used):
            n1 = (first_active_window + n) % self.nb_viewers_used
            viewer = self.image_viewers[n1]
            # viewer image has already been defined
            # try to update corresponding images in row
            try:
                viewer_image = self.get_output_image(viewer.image_name)
            except Exception as e:
                print("Error: failed to get image {}: {}".format(viewer.image_name, e))
                viewer.set_image(current_image)
            else:
                viewer.set_image(viewer_image)

            # set reference image
            viewer.set_image_ref(reference_image)

            self.image_viewers[prev_n].set_synchronize(viewer)
            prev_n = n1
        # Create a synchronization loop
        if prev_n != first_active_window:
            self.image_viewers[prev_n].set_synchronize(self.image_viewers[first_active_window])

    # Be sure to show the required viewers
    for n in range(self.nb_viewers_used):
        viewer = self.image_viewers[n]
        # print(f"show viewer {n}")
        # Note: calling show in any case seems to avoid double calls to paint event that update() triggers
        # viewer.show()
        if viewer.isHidden():
            # print(f"show viewer {n}")
            viewer.show()
        else:
            # print(f"update viewer {n}")
            viewer.update()


    # self.image_scroll_area.adjustSize()
    # if self.show_timing():
    print(f" Update image took {(get_time() - update_image_start)*1000:0.0f} ms")
def update_image_buttons(self)
Expand source code
def update_image_buttons(self):
    # choose image to display
    self.clear_buttons()
    self.image_list = list(self.image_dict.keys())
    self.print_log("MultiView.update_image_buttons() {}".format(self.image_list))
    self.label = dict()
    for image_name in self.image_list:
        # possibility to disable an image using the string 'none', especially useful for input image
        if image_name != 'none':
            self.label[image_name] = MVLabel(image_name, self)
            self.label[image_name].setFrameShape(QtWidgets.QFrame.Panel)
            self.label[image_name].setFrameShadow(QtWidgets.QFrame.Sunken)
            # self.label[image_name].setLineWidth(3)
            self.label[image_name].setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)
            # self.label[image_name].setFixedHeight(40)
            self.label[image_name].mousePressEvent = self.make_mouse_press(image_name)
            self.label[image_name].mouseReleaseEvent = self.mouse_release
            self.label[image_name].mouseDoubleClickEvent = self.make_mouse_double_click(image_name)
    self.create_buttons()

    # the crop area can be changed using the mouse wheel
    self.output_label_crop = (0., 0., 1., 1.)

    if len(self.image_list)>0:
        self.output_label_current_image = self.image_list[0]
        self.set_reference_label(self.image_list[0], update_viewers=True)
    else:
        self.output_label_current_image = ''
        self.output_label_reference_image = ''
def update_image_intensity_event(self)
Expand source code
def update_image_intensity_event(self):
    self.update_image_parameters()
def update_image_parameters(self)

Uses the variable self.output_label_current_image :return:

Expand source code
def update_image_parameters(self):
    '''
    Uses the variable self.output_label_current_image
    :return:
    '''
    self.print_log('update_image_parameters')
    update_start = get_time()

    for n in range(self.nb_viewers_used):
        self.image_viewers[n].filter_params.copy_from(self.filter_params)
        self.image_viewers[n].update()

    if self.show_timing():
        time_spent = get_time() - update_start
        self.print_log(" Update image took {0:0.3f} sec.".format(time_spent))
def update_label_fonts(self)
Expand source code
def update_label_fonts(self):
    # Update selected image label, we could do it later too
    for im_name in self.image_list:
        # possibility to disable an image using the string 'none', especially useful for input image
        if im_name != 'none':
            is_bold      = im_name == self.output_label_current_image
            is_underline = im_name == self.output_label_reference_image
            is_bold |= is_underline
            self.bold_font.setBold(is_bold)
            self.bold_font.setUnderline(is_underline)
            self.bold_font.setPointSize(8)
            self.label[im_name].setFont(self.bold_font)
            self.label[im_name].setWordWrap(True)
        # self.label[im_name].setMaximumWidth(160)
def update_layout(self)
Expand source code
def update_layout(self):
    self.print_log("update_layout")
    vertical_layout = QtWidgets.QVBoxLayout()
    self.layout_buttons(vertical_layout)

    # First line of parameter control
    parameters_layout = QtWidgets.QHBoxLayout()
    self.layout_parameters(parameters_layout)
    vertical_layout.addLayout(parameters_layout, 1)

    # Second line of parameter control
    parameters2_layout = QtWidgets.QHBoxLayout()
    self.layout_parameters_2(parameters2_layout)
    vertical_layout.addLayout(parameters2_layout, 1)

    self.viewer_grid_layout = QtWidgets.QGridLayout()
    self.viewer_grid_layout.setHorizontalSpacing(1)
    self.viewer_grid_layout.setVerticalSpacing(1)
    self.set_number_of_viewers(1)
    vertical_layout.addLayout(self.viewer_grid_layout, 1)

    self.figures_widget = QtWidgets.QWidget()
    self.figures_layout = QtWidgets.QHBoxLayout()
    self.figures_layout.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)
    # for the moment ignore this
    # self.figures_layout.addWidget(self.value_in_range_canvas)
    # self.figures_widget.setLayout(self.figures_layout)

    vertical_layout.addWidget(self.figures_widget)
    self.toggle_display_profiles()
    self.setLayout(vertical_layout)
    print("update_layout done")
def update_reference(self) ‑> None
Expand source code
def update_reference(self) -> None:
    reference_image = self.get_output_image(self.output_label_reference_image)
    for n in range(self.nb_viewers_used):
        viewer = self.image_viewers[n]
        # set reference image
        viewer.set_image_ref(reference_image)
def update_viewer_mode(self)
Expand source code
def update_viewer_mode(self):
    viewer_mode = self.viewer_mode_selection.get_selection_value()
    self.image_viewer_class = self.image_viewer_classes[viewer_mode]
class ViewerType (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code
class ViewerType(Enum):
    QT_VIEWER             = auto()
    OPENGL_VIEWER         = auto()
    OPENGL_SHADERS_VIEWER = auto()

Ancestors

  • enum.Enum

Class variables

var OPENGL_SHADERS_VIEWER
var OPENGL_VIEWER
var QT_VIEWER