Module qimview.image_viewers.gl_image_viewer_base

Expand source code
#
#
# started from https://cyrille.rossant.net/2d-graphics-rendering-tutorial-with-pyopengl/
#
# check also https://doc.qt.io/archives/4.6/opengl-overpainting.html
#

from qimview.utils.qt_imports   import *
from qimview.utils.viewer_image import ImageFormat, ViewerImage
from qimview.image_viewers.image_viewer import ImageViewer, trace_method

import OpenGL
OpenGL.ERROR_ON_COPY = True
import OpenGL.GL as gl
import OpenGL.GLU as glu
import traceback
import numpy as np
from typing import Optional, Tuple

class GLImageViewerBase(QOpenGLWidget, ImageViewer):

    def __init__(self, parent=None):
        QOpenGLWidget.__init__(self, parent)
        ImageViewer.__init__(self, parent)
        self.setAutoFillBackground(False)

        _format = QtGui.QSurfaceFormat()
        print('profile is {}'.format(_format.profile()))
        print('version is {}'.format(_format.version()))
        # _format.setDepthBufferSize(24)
        # _format.setVersion(4,0)
        # _format.setProfile(PySide2.QtGui.QSurfaceFormat.CoreProfile)
        _format.setProfile(QtGui.QSurfaceFormat.CompatibilityProfile)
        self.setFormat(_format)

        # self.setFormat()
        self.textureID  = None
        self.tex_width, self.tex_height = 0, 0
        self.opengl_debug = True
        self.current_text = None
        self.cursor_imx_ratio = 0.5
        self.cursor_imy_ratio = 0.5
        self.trace_calls = False
        self.setMouseTracking(True)

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

    # def __del__(self):
    #     if self.textureID is not None:
    #         gl.glDeleteTextures(np.array([self.textureID]))

    def set_image(self, image):
        if self.trace_calls:
            t = trace_method(self.tab)
        changed = super(GLImageViewerBase, self).set_image(image)

        img_width = self._image.data.shape[1]
        if img_width % 4 != 0:
            print("Image is resized to a multiple of 4 dimension in X")
            img_width = ((img_width >> 4) << 4)
            im = np.ascontiguousarray(self._image.data[:,:img_width, :])
            self._image = ViewerImage(im, precision = self._image.precision, downscale = 1,
                                        channels = self._image.channels)
            print(self._image.data.shape)

        if changed:  # and self.textureID is not None:
            if self.setTexture():
                self.show()
                self.update()
            else:
                print("setTexture() return False")

    def synchronize_data(self, other_viewer):
        super(GLImageViewerBase, self).synchronize_data(other_viewer)
        other_viewer.cursor_imx_ratio = self.cursor_imx_ratio
        other_viewer.cursor_imy_ratio = self.cursor_imy_ratio

    def opengl_error(self, force=False):
        if self.opengl_debug or force:
            status = gl.glGetError()
            if status != gl.GL_NO_ERROR:
                print(self.tab[0]+'gl error %s' % status)

    def setTexture(self):
        """
        :return: set opengl texture based on input numpy array image
        """
        # gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_GENERATE_MIPMAP_SGIS, gl.GL_TRUE)

        if self.trace_calls:
            t = trace_method(self.tab)
        self.start_timing()
        self.makeCurrent()

        # Replace texture only if required
        if self._image is None:
            print("self._image is None")
            return False

        img_height, img_width = self._image.data.shape[:2]

        # default type
        gl_types = {
            'int8'   : gl.GL_BYTE,
            'uint8'  : gl.GL_UNSIGNED_BYTE,
            'int16'  : gl.GL_SHORT,
            'uint16' : gl.GL_UNSIGNED_SHORT,
            'int32'  : gl.GL_INT,
            'uint32' : gl.GL_UNSIGNED_INT,
            'float32': gl.GL_FLOAT,
            'float64': gl.GL_DOUBLE
        }
        gl_type = gl_types[self._image.data.dtype.name]

        # It seems that the dimension in X should be even

        # Not sure what is the right parameter for internal format of 2D texture based
        # on the input data type uint8, uint16, ...
        # need to test with different DXR images
        internal_format = gl.GL_RGB
        if self._image.data.shape[2] == 3:
            if self._image.precision == 8:
                internal_format = gl.GL_RGB
            if self._image.precision == 10:
                internal_format = gl.GL_RGB10
            if self._image.precision == 12:
                internal_format = gl.GL_RGB12
        if self._image.data.shape[2] == 4:
            if self._image.precision == 8:
                internal_format = gl.GL_RGBA
            else:
                if self._image.precision <= 12:
                    internal_format = gl.GL_RGBA12
                else:
                    if self._image.precision <= 16:
                        internal_format = gl.GL_RGBA16
                    else:
                        internal_format = gl.GL_RGBA32F

        channels2format = {
            ImageFormat.CH_RGB  : gl.GL_RGB,
            ImageFormat.CH_BGR  : gl.GL_BGR,
            ImageFormat.CH_Y    : gl.GL_RED, # not sure about this one
            ImageFormat.CH_RGGB : gl.GL_RGBA, # we save 4 component data
            ImageFormat.CH_GRBG : gl.GL_RGBA,
            ImageFormat.CH_GBRG : gl.GL_RGBA,
            ImageFormat.CH_BGGR : gl.GL_RGBA
        }
        texture_pixel_format = channels2format[self._image.channels]

        if (self.tex_width,self.tex_height) != (img_width,img_height):
            if self.textureID is not None:
                gl.glDeleteTextures(np.array([self.textureID]))
            self.textureID = gl.glGenTextures(1)
            gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 4)
            gl.glBindTexture(gl.GL_TEXTURE_2D, self.textureID)
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_BASE_LEVEL, 0)
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAX_LEVEL, 0)
            # gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAX_LEVEL, 10)
            # gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
            # gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST_MIPMAP_NEAREST)
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
            gl.glTexImage2D(gl.GL_TEXTURE_2D, 0,
                            internal_format,
                            img_width, img_height,
                         0, texture_pixel_format, gl_type, self._image.data)
            # gl.glGenerateMipmap(gl.GL_TEXTURE_2D)
            self.tex_width, self.tex_height = img_width, img_height
        else:
            try:
                gl.glTexSubImage2D(gl.GL_TEXTURE_2D, 0, 0, 0, img_width, img_height,
                             texture_pixel_format, gl_type, self._image.data)
                # gl.glGenerateMipmap(gl.GL_TEXTURE_2D)
            except Exception as e:
                print("setTexture failed shape={}: {}".format(self._image.data.shape, e))
                return False

        self.print_timing(add_total=True)
        self.opengl_error()
        return True

    # @abstract_method
    def viewer_update(self):
        self.update()

    # To be defined in children
    def myPaintGL(self):  pass 

    def paintAll(self):
        if self.trace_calls:
            t = trace_method(self.tab)
        if self.textureID is None or not self.isValid() or not self.isVisible():
            print("paintGL()** not ready {} {}".format(self.textureID, self.isValid()))
            return
        # No need for makeCurrent() since it is called from PaintGL() only
        # self.makeCurrent()
        painter = QtGui.QPainter()
        painter.begin(self)
        painter.beginNativePainting()
        im_pos = None
        scale  = None
        try:
            self.updateViewPort()
            scale = self.updateTransforms()
            self.myPaintGL()
            if self.show_cursor:
                im_pos = self.gl_draw_cursor()
        except Exception as e:
            self.print_log(" failed paintGL {}".format(e))
            traceback.print_exc()
        painter.endNativePainting()
        # status = gl.glGetError()

        draw_text = True
        if draw_text:
            # adapt scale depending on the ratio image / viewport
            scale *= self._width/self._image.data.shape[1]
            self.display_text(painter, self.display_message(im_pos, scale))

        # draw histogram
        if self.show_histogram:
            current_image = self._image.data
            rect = QtCore.QRect(0, 0, self.width(), self.height())
            histograms = self.compute_histogram_Cpp(current_image, show_timings=self.display_timing)
            self.display_histogram(histograms, 1,  painter, rect, show_timings=self.display_timing)

        painter.end()
        # Seems required here, at least on linux
        # self.update()

    def set_cursor_image_position(self, cursor_x, cursor_y):
        """
        Sets the image position from the cursor in proportion of the image dimension
        :return:
        """
        self.updateTransforms()
        ratio = self.screen().devicePixelRatio()
        self.print_log("ratio {}".format(ratio))
        pos_x = cursor_x * ratio
        pos_y = (self.height() - cursor_y) * ratio
        self.print_log("pos {} {}".format(pos_x, pos_y))
        x0, x1, y0, y1 = self.image_centered_position()

        gl_posX, gl_posY = self.get_mouse_gl_coordinates(pos_x, pos_y)
        self.cursor_imx_ratio = (gl_posX - x0) / (x1 - x0)
        self.cursor_imy_ratio = 1 - (gl_posY - y0) / (y1 - y0)
        self.print_log("cursor ratio {} {}".format(self.cursor_imx_ratio, self.cursor_imy_ratio))


    def gl_draw_cursor(self) -> Optional[Tuple[int, int]]:
        x0, x1, y0, y1 = self.image_centered_position()

        im_x = int(self.cursor_imx_ratio*self.tex_width)
        im_y = int(self.cursor_imy_ratio*self.tex_height)

        glpos_from_im_x = (im_x+0.5)*(x1-x0)/self.tex_width + x0
        glpos_from_im_y = (self.tex_height - (im_y+0.5))*(y1-y0)/self.tex_height+y0

        # get image coordinates
        length = 20 # /self.current_scale
        width = 4
        gl.glLineWidth(width)
        gl.glColor3f(0.0, 1.0, 1.0)
        gl.glBegin(gl.GL_LINES)
        gl.glVertex3f(glpos_from_im_x-length, glpos_from_im_y, -0.001)
        gl.glVertex3f(glpos_from_im_x+length, glpos_from_im_y, -0.001)
        gl.glVertex3f(glpos_from_im_x, glpos_from_im_y-length, -0.001)
        gl.glVertex3f(glpos_from_im_x, glpos_from_im_y+length, -0.001)
        gl.glEnd()
        if im_x>=0 and im_x<self.tex_width and im_y>=0 and im_y<=self.tex_height:
            return (im_x, im_y) 
        return None


    def image_centered_position(self):
        w = self._width
        h = self._height
        self.print_log(f'self width height {self._width} {self._height} tex {self.tex_width} {self.tex_height}')
        if self.tex_width == 0 or self.tex_height == 0:
            return 0, w, 0, h
        # self.print_log(' {}x{}'.format(w, h))
        image_ratio = float(self.tex_width)/float(self.tex_height)
        if h*image_ratio < w:
            view_width  = int(h*image_ratio+0.5)
            view_height = h
            start_x = int((w - view_width) / 2 + 0.5)
            start_y = 0
        else:
            view_width  = w
            view_height = int(w/image_ratio+0.5)
            start_x = 0
            start_y = int((h - view_height) / 2 + 0.5)

        x0 = start_x
        x1 = start_x+view_width
        y0 = start_y
        y1 = start_y+view_height
        return x0, x1, y0, y1

    def updateViewPort(self):
        if self.trace_calls:
            t = trace_method(self.tab)
        self.start_timing()
        # keep image proportions
        w = self._width
        h = self._height
        # print(f" w, h {w, h}")
        try:
            gl.glViewport(0,0,w,h) # keep everything in viewport
        except Exception as e:
            self.print_log(" failed glViewport {}".format(e))
        self.print_timing(add_total=True)

    def updateTransforms(self) -> float:
        if self.trace_calls:
            t = trace_method(self.tab)
        self.start_timing()
        self.makeCurrent()
        w = self._width
        h = self._height
        dx, dy = self.new_translation()
        scale = self.new_scale(self.mouse_zy, self.tex_height)
        print(f"updateTransforms scale {scale}")
        try:
            # print("current context ", QtOpenGL.QGLContext.currentContext())
            # gl = QtOpenGL.QGLContext.currentContext().functions()
            # update the window size
            gl.glMatrixMode(gl.GL_PROJECTION)
            gl.glLoadIdentity()
            translation_unit = min(w, h)/2
            # self.print_log("scale {}".format(scale))
            gl.glScale(scale, scale, scale)
            gl.glTranslate(dx/translation_unit, dy/translation_unit, 0)
            # the window corner OpenGL coordinates are (-+1, -+1)
            gl.glOrtho(0, w, 0, h, -1, 1)
            gl.glMatrixMode(gl.GL_MODELVIEW)
            gl.glLoadIdentity()
        except Exception as e:
            self.print_log(" setting gl matrices failed {}".format(e))
        self.print_timing(add_total=True)
        self.opengl_error()
        return scale

    def resizeGL(self, width, height):
        """Called upon window resizing: reinitialize the viewport.
        """
        # print("ResizeGL")
        if self.trace_calls:
            t = trace_method(self.tab)
        # size give for opengl are in pixels, qt uses device independent size otherwise
        print(f"self.devicePixelRatio() {self.devicePixelRatio()}")
        self._width = width*self.devicePixelRatio()
        self._height = height*self.devicePixelRatio()
        # print("width height ratios {} {}".format(self._width/self.width(), self._height/self.height()))
        self.viewer_update()

    def get_mouse_gl_coordinates(self, x, y):
        modelview = gl.glGetDoublev(gl.GL_MODELVIEW_MATRIX)
        projection = gl.glGetDoublev(gl.GL_PROJECTION_MATRIX)
        viewport = gl.glGetIntegerv(gl.GL_VIEWPORT)
        posX, posY, posZ = glu.gluUnProject(x, y, 0, modelview, projection, viewport)
        return posX, posY

    def mousePressEvent(self, event):
        self.mouse_press_event(event)

    def mouseMoveEvent(self, event):
        if self.show_cursor:
            self.set_cursor_image_position(event.x(), event.y())
        self.mouse_move_event(event)

    def mouseReleaseEvent(self, event):
        self.mouse_release_event(event)

    def mouseDoubleClickEvent(self, event):
        self.mouse_double_click_event(event)

    def wheelEvent(self, event):
        self.mouse_wheel_event(event)

    def keyPressEvent(self, event):
        # TODO: Fix the correct parameters for selecting image zoom/pan
        x0, x1, y0, y1 = self.image_centered_position()
        print(f"image centered position {x1-x0} x {y1-y0}")
        self.key_press_event(event, wsize=QtCore.QSize(x1-x0, y1-y0))

    def resizeEvent(self, event):
        """Called upon window resizing: reinitialize the viewport.
        """
        if self.trace_calls:
            t = trace_method(self.tab)
        self.print_log(f"resize {event.size()}  self {self.width()} {self.height()}")
        self.evt_width = event.size().width()
        self.evt_height = event.size().height()
        QOpenGLWidget.resizeEvent(self, event)
        self.print_log(f"resize {event.size()}  self {self.width()} {self.height()}")

Classes

class GLImageViewerBase (parent=None)

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

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

Initialize self. See help(type(self)) for accurate signature.

Expand source code
class GLImageViewerBase(QOpenGLWidget, ImageViewer):

    def __init__(self, parent=None):
        QOpenGLWidget.__init__(self, parent)
        ImageViewer.__init__(self, parent)
        self.setAutoFillBackground(False)

        _format = QtGui.QSurfaceFormat()
        print('profile is {}'.format(_format.profile()))
        print('version is {}'.format(_format.version()))
        # _format.setDepthBufferSize(24)
        # _format.setVersion(4,0)
        # _format.setProfile(PySide2.QtGui.QSurfaceFormat.CoreProfile)
        _format.setProfile(QtGui.QSurfaceFormat.CompatibilityProfile)
        self.setFormat(_format)

        # self.setFormat()
        self.textureID  = None
        self.tex_width, self.tex_height = 0, 0
        self.opengl_debug = True
        self.current_text = None
        self.cursor_imx_ratio = 0.5
        self.cursor_imy_ratio = 0.5
        self.trace_calls = False
        self.setMouseTracking(True)

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

    # def __del__(self):
    #     if self.textureID is not None:
    #         gl.glDeleteTextures(np.array([self.textureID]))

    def set_image(self, image):
        if self.trace_calls:
            t = trace_method(self.tab)
        changed = super(GLImageViewerBase, self).set_image(image)

        img_width = self._image.data.shape[1]
        if img_width % 4 != 0:
            print("Image is resized to a multiple of 4 dimension in X")
            img_width = ((img_width >> 4) << 4)
            im = np.ascontiguousarray(self._image.data[:,:img_width, :])
            self._image = ViewerImage(im, precision = self._image.precision, downscale = 1,
                                        channels = self._image.channels)
            print(self._image.data.shape)

        if changed:  # and self.textureID is not None:
            if self.setTexture():
                self.show()
                self.update()
            else:
                print("setTexture() return False")

    def synchronize_data(self, other_viewer):
        super(GLImageViewerBase, self).synchronize_data(other_viewer)
        other_viewer.cursor_imx_ratio = self.cursor_imx_ratio
        other_viewer.cursor_imy_ratio = self.cursor_imy_ratio

    def opengl_error(self, force=False):
        if self.opengl_debug or force:
            status = gl.glGetError()
            if status != gl.GL_NO_ERROR:
                print(self.tab[0]+'gl error %s' % status)

    def setTexture(self):
        """
        :return: set opengl texture based on input numpy array image
        """
        # gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_GENERATE_MIPMAP_SGIS, gl.GL_TRUE)

        if self.trace_calls:
            t = trace_method(self.tab)
        self.start_timing()
        self.makeCurrent()

        # Replace texture only if required
        if self._image is None:
            print("self._image is None")
            return False

        img_height, img_width = self._image.data.shape[:2]

        # default type
        gl_types = {
            'int8'   : gl.GL_BYTE,
            'uint8'  : gl.GL_UNSIGNED_BYTE,
            'int16'  : gl.GL_SHORT,
            'uint16' : gl.GL_UNSIGNED_SHORT,
            'int32'  : gl.GL_INT,
            'uint32' : gl.GL_UNSIGNED_INT,
            'float32': gl.GL_FLOAT,
            'float64': gl.GL_DOUBLE
        }
        gl_type = gl_types[self._image.data.dtype.name]

        # It seems that the dimension in X should be even

        # Not sure what is the right parameter for internal format of 2D texture based
        # on the input data type uint8, uint16, ...
        # need to test with different DXR images
        internal_format = gl.GL_RGB
        if self._image.data.shape[2] == 3:
            if self._image.precision == 8:
                internal_format = gl.GL_RGB
            if self._image.precision == 10:
                internal_format = gl.GL_RGB10
            if self._image.precision == 12:
                internal_format = gl.GL_RGB12
        if self._image.data.shape[2] == 4:
            if self._image.precision == 8:
                internal_format = gl.GL_RGBA
            else:
                if self._image.precision <= 12:
                    internal_format = gl.GL_RGBA12
                else:
                    if self._image.precision <= 16:
                        internal_format = gl.GL_RGBA16
                    else:
                        internal_format = gl.GL_RGBA32F

        channels2format = {
            ImageFormat.CH_RGB  : gl.GL_RGB,
            ImageFormat.CH_BGR  : gl.GL_BGR,
            ImageFormat.CH_Y    : gl.GL_RED, # not sure about this one
            ImageFormat.CH_RGGB : gl.GL_RGBA, # we save 4 component data
            ImageFormat.CH_GRBG : gl.GL_RGBA,
            ImageFormat.CH_GBRG : gl.GL_RGBA,
            ImageFormat.CH_BGGR : gl.GL_RGBA
        }
        texture_pixel_format = channels2format[self._image.channels]

        if (self.tex_width,self.tex_height) != (img_width,img_height):
            if self.textureID is not None:
                gl.glDeleteTextures(np.array([self.textureID]))
            self.textureID = gl.glGenTextures(1)
            gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 4)
            gl.glBindTexture(gl.GL_TEXTURE_2D, self.textureID)
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_BASE_LEVEL, 0)
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAX_LEVEL, 0)
            # gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAX_LEVEL, 10)
            # gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
            # gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST_MIPMAP_NEAREST)
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
            gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
            gl.glTexImage2D(gl.GL_TEXTURE_2D, 0,
                            internal_format,
                            img_width, img_height,
                         0, texture_pixel_format, gl_type, self._image.data)
            # gl.glGenerateMipmap(gl.GL_TEXTURE_2D)
            self.tex_width, self.tex_height = img_width, img_height
        else:
            try:
                gl.glTexSubImage2D(gl.GL_TEXTURE_2D, 0, 0, 0, img_width, img_height,
                             texture_pixel_format, gl_type, self._image.data)
                # gl.glGenerateMipmap(gl.GL_TEXTURE_2D)
            except Exception as e:
                print("setTexture failed shape={}: {}".format(self._image.data.shape, e))
                return False

        self.print_timing(add_total=True)
        self.opengl_error()
        return True

    # @abstract_method
    def viewer_update(self):
        self.update()

    # To be defined in children
    def myPaintGL(self):  pass 

    def paintAll(self):
        if self.trace_calls:
            t = trace_method(self.tab)
        if self.textureID is None or not self.isValid() or not self.isVisible():
            print("paintGL()** not ready {} {}".format(self.textureID, self.isValid()))
            return
        # No need for makeCurrent() since it is called from PaintGL() only
        # self.makeCurrent()
        painter = QtGui.QPainter()
        painter.begin(self)
        painter.beginNativePainting()
        im_pos = None
        scale  = None
        try:
            self.updateViewPort()
            scale = self.updateTransforms()
            self.myPaintGL()
            if self.show_cursor:
                im_pos = self.gl_draw_cursor()
        except Exception as e:
            self.print_log(" failed paintGL {}".format(e))
            traceback.print_exc()
        painter.endNativePainting()
        # status = gl.glGetError()

        draw_text = True
        if draw_text:
            # adapt scale depending on the ratio image / viewport
            scale *= self._width/self._image.data.shape[1]
            self.display_text(painter, self.display_message(im_pos, scale))

        # draw histogram
        if self.show_histogram:
            current_image = self._image.data
            rect = QtCore.QRect(0, 0, self.width(), self.height())
            histograms = self.compute_histogram_Cpp(current_image, show_timings=self.display_timing)
            self.display_histogram(histograms, 1,  painter, rect, show_timings=self.display_timing)

        painter.end()
        # Seems required here, at least on linux
        # self.update()

    def set_cursor_image_position(self, cursor_x, cursor_y):
        """
        Sets the image position from the cursor in proportion of the image dimension
        :return:
        """
        self.updateTransforms()
        ratio = self.screen().devicePixelRatio()
        self.print_log("ratio {}".format(ratio))
        pos_x = cursor_x * ratio
        pos_y = (self.height() - cursor_y) * ratio
        self.print_log("pos {} {}".format(pos_x, pos_y))
        x0, x1, y0, y1 = self.image_centered_position()

        gl_posX, gl_posY = self.get_mouse_gl_coordinates(pos_x, pos_y)
        self.cursor_imx_ratio = (gl_posX - x0) / (x1 - x0)
        self.cursor_imy_ratio = 1 - (gl_posY - y0) / (y1 - y0)
        self.print_log("cursor ratio {} {}".format(self.cursor_imx_ratio, self.cursor_imy_ratio))


    def gl_draw_cursor(self) -> Optional[Tuple[int, int]]:
        x0, x1, y0, y1 = self.image_centered_position()

        im_x = int(self.cursor_imx_ratio*self.tex_width)
        im_y = int(self.cursor_imy_ratio*self.tex_height)

        glpos_from_im_x = (im_x+0.5)*(x1-x0)/self.tex_width + x0
        glpos_from_im_y = (self.tex_height - (im_y+0.5))*(y1-y0)/self.tex_height+y0

        # get image coordinates
        length = 20 # /self.current_scale
        width = 4
        gl.glLineWidth(width)
        gl.glColor3f(0.0, 1.0, 1.0)
        gl.glBegin(gl.GL_LINES)
        gl.glVertex3f(glpos_from_im_x-length, glpos_from_im_y, -0.001)
        gl.glVertex3f(glpos_from_im_x+length, glpos_from_im_y, -0.001)
        gl.glVertex3f(glpos_from_im_x, glpos_from_im_y-length, -0.001)
        gl.glVertex3f(glpos_from_im_x, glpos_from_im_y+length, -0.001)
        gl.glEnd()
        if im_x>=0 and im_x<self.tex_width and im_y>=0 and im_y<=self.tex_height:
            return (im_x, im_y) 
        return None


    def image_centered_position(self):
        w = self._width
        h = self._height
        self.print_log(f'self width height {self._width} {self._height} tex {self.tex_width} {self.tex_height}')
        if self.tex_width == 0 or self.tex_height == 0:
            return 0, w, 0, h
        # self.print_log(' {}x{}'.format(w, h))
        image_ratio = float(self.tex_width)/float(self.tex_height)
        if h*image_ratio < w:
            view_width  = int(h*image_ratio+0.5)
            view_height = h
            start_x = int((w - view_width) / 2 + 0.5)
            start_y = 0
        else:
            view_width  = w
            view_height = int(w/image_ratio+0.5)
            start_x = 0
            start_y = int((h - view_height) / 2 + 0.5)

        x0 = start_x
        x1 = start_x+view_width
        y0 = start_y
        y1 = start_y+view_height
        return x0, x1, y0, y1

    def updateViewPort(self):
        if self.trace_calls:
            t = trace_method(self.tab)
        self.start_timing()
        # keep image proportions
        w = self._width
        h = self._height
        # print(f" w, h {w, h}")
        try:
            gl.glViewport(0,0,w,h) # keep everything in viewport
        except Exception as e:
            self.print_log(" failed glViewport {}".format(e))
        self.print_timing(add_total=True)

    def updateTransforms(self) -> float:
        if self.trace_calls:
            t = trace_method(self.tab)
        self.start_timing()
        self.makeCurrent()
        w = self._width
        h = self._height
        dx, dy = self.new_translation()
        scale = self.new_scale(self.mouse_zy, self.tex_height)
        print(f"updateTransforms scale {scale}")
        try:
            # print("current context ", QtOpenGL.QGLContext.currentContext())
            # gl = QtOpenGL.QGLContext.currentContext().functions()
            # update the window size
            gl.glMatrixMode(gl.GL_PROJECTION)
            gl.glLoadIdentity()
            translation_unit = min(w, h)/2
            # self.print_log("scale {}".format(scale))
            gl.glScale(scale, scale, scale)
            gl.glTranslate(dx/translation_unit, dy/translation_unit, 0)
            # the window corner OpenGL coordinates are (-+1, -+1)
            gl.glOrtho(0, w, 0, h, -1, 1)
            gl.glMatrixMode(gl.GL_MODELVIEW)
            gl.glLoadIdentity()
        except Exception as e:
            self.print_log(" setting gl matrices failed {}".format(e))
        self.print_timing(add_total=True)
        self.opengl_error()
        return scale

    def resizeGL(self, width, height):
        """Called upon window resizing: reinitialize the viewport.
        """
        # print("ResizeGL")
        if self.trace_calls:
            t = trace_method(self.tab)
        # size give for opengl are in pixels, qt uses device independent size otherwise
        print(f"self.devicePixelRatio() {self.devicePixelRatio()}")
        self._width = width*self.devicePixelRatio()
        self._height = height*self.devicePixelRatio()
        # print("width height ratios {} {}".format(self._width/self.width(), self._height/self.height()))
        self.viewer_update()

    def get_mouse_gl_coordinates(self, x, y):
        modelview = gl.glGetDoublev(gl.GL_MODELVIEW_MATRIX)
        projection = gl.glGetDoublev(gl.GL_PROJECTION_MATRIX)
        viewport = gl.glGetIntegerv(gl.GL_VIEWPORT)
        posX, posY, posZ = glu.gluUnProject(x, y, 0, modelview, projection, viewport)
        return posX, posY

    def mousePressEvent(self, event):
        self.mouse_press_event(event)

    def mouseMoveEvent(self, event):
        if self.show_cursor:
            self.set_cursor_image_position(event.x(), event.y())
        self.mouse_move_event(event)

    def mouseReleaseEvent(self, event):
        self.mouse_release_event(event)

    def mouseDoubleClickEvent(self, event):
        self.mouse_double_click_event(event)

    def wheelEvent(self, event):
        self.mouse_wheel_event(event)

    def keyPressEvent(self, event):
        # TODO: Fix the correct parameters for selecting image zoom/pan
        x0, x1, y0, y1 = self.image_centered_position()
        print(f"image centered position {x1-x0} x {y1-y0}")
        self.key_press_event(event, wsize=QtCore.QSize(x1-x0, y1-y0))

    def resizeEvent(self, event):
        """Called upon window resizing: reinitialize the viewport.
        """
        if self.trace_calls:
            t = trace_method(self.tab)
        self.print_log(f"resize {event.size()}  self {self.width()} {self.height()}")
        self.evt_width = event.size().width()
        self.evt_height = event.size().height()
        QOpenGLWidget.resizeEvent(self, event)
        self.print_log(f"resize {event.size()}  self {self.width()} {self.height()}")

Ancestors

  • PySide6.QtOpenGLWidgets.QOpenGLWidget
  • PySide6.QtWidgets.QWidget
  • PySide6.QtCore.QObject
  • PySide6.QtGui.QPaintDevice
  • Shiboken.Object
  • ImageViewer

Subclasses

Class variables

var staticMetaObject

Methods

def get_mouse_gl_coordinates(self, x, y)
Expand source code
def get_mouse_gl_coordinates(self, x, y):
    modelview = gl.glGetDoublev(gl.GL_MODELVIEW_MATRIX)
    projection = gl.glGetDoublev(gl.GL_PROJECTION_MATRIX)
    viewport = gl.glGetIntegerv(gl.GL_VIEWPORT)
    posX, posY, posZ = glu.gluUnProject(x, y, 0, modelview, projection, viewport)
    return posX, posY
def gl_draw_cursor(self) ‑> Optional[Tuple[int, int]]
Expand source code
def gl_draw_cursor(self) -> Optional[Tuple[int, int]]:
    x0, x1, y0, y1 = self.image_centered_position()

    im_x = int(self.cursor_imx_ratio*self.tex_width)
    im_y = int(self.cursor_imy_ratio*self.tex_height)

    glpos_from_im_x = (im_x+0.5)*(x1-x0)/self.tex_width + x0
    glpos_from_im_y = (self.tex_height - (im_y+0.5))*(y1-y0)/self.tex_height+y0

    # get image coordinates
    length = 20 # /self.current_scale
    width = 4
    gl.glLineWidth(width)
    gl.glColor3f(0.0, 1.0, 1.0)
    gl.glBegin(gl.GL_LINES)
    gl.glVertex3f(glpos_from_im_x-length, glpos_from_im_y, -0.001)
    gl.glVertex3f(glpos_from_im_x+length, glpos_from_im_y, -0.001)
    gl.glVertex3f(glpos_from_im_x, glpos_from_im_y-length, -0.001)
    gl.glVertex3f(glpos_from_im_x, glpos_from_im_y+length, -0.001)
    gl.glEnd()
    if im_x>=0 and im_x<self.tex_width and im_y>=0 and im_y<=self.tex_height:
        return (im_x, im_y) 
    return None
def image_centered_position(self)
Expand source code
def image_centered_position(self):
    w = self._width
    h = self._height
    self.print_log(f'self width height {self._width} {self._height} tex {self.tex_width} {self.tex_height}')
    if self.tex_width == 0 or self.tex_height == 0:
        return 0, w, 0, h
    # self.print_log(' {}x{}'.format(w, h))
    image_ratio = float(self.tex_width)/float(self.tex_height)
    if h*image_ratio < w:
        view_width  = int(h*image_ratio+0.5)
        view_height = h
        start_x = int((w - view_width) / 2 + 0.5)
        start_y = 0
    else:
        view_width  = w
        view_height = int(w/image_ratio+0.5)
        start_x = 0
        start_y = int((h - view_height) / 2 + 0.5)

    x0 = start_x
    x1 = start_x+view_width
    y0 = start_y
    y1 = start_y+view_height
    return x0, x1, y0, y1
def keyPressEvent(self, event)

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

Expand source code
def keyPressEvent(self, event):
    # TODO: Fix the correct parameters for selecting image zoom/pan
    x0, x1, y0, y1 = self.image_centered_position()
    print(f"image centered position {x1-x0} x {y1-y0}")
    self.key_press_event(event, wsize=QtCore.QSize(x1-x0, y1-y0))
def mouseDoubleClickEvent(self, event)

mouseDoubleClickEvent(self, event: PySide6.QtGui.QMouseEvent) -> None

Expand source code
def mouseDoubleClickEvent(self, event):
    self.mouse_double_click_event(event)
def mouseMoveEvent(self, event)

mouseMoveEvent(self, event: PySide6.QtGui.QMouseEvent) -> None

Expand source code
def mouseMoveEvent(self, event):
    if self.show_cursor:
        self.set_cursor_image_position(event.x(), event.y())
    self.mouse_move_event(event)
def mousePressEvent(self, event)

mousePressEvent(self, event: PySide6.QtGui.QMouseEvent) -> None

Expand source code
def mousePressEvent(self, event):
    self.mouse_press_event(event)
def mouseReleaseEvent(self, event)

mouseReleaseEvent(self, event: PySide6.QtGui.QMouseEvent) -> None

Expand source code
def mouseReleaseEvent(self, event):
    self.mouse_release_event(event)
def myPaintGL(self)
Expand source code
def myPaintGL(self):  pass 
def opengl_error(self, force=False)
Expand source code
def opengl_error(self, force=False):
    if self.opengl_debug or force:
        status = gl.glGetError()
        if status != gl.GL_NO_ERROR:
            print(self.tab[0]+'gl error %s' % status)
def paintAll(self)
Expand source code
def paintAll(self):
    if self.trace_calls:
        t = trace_method(self.tab)
    if self.textureID is None or not self.isValid() or not self.isVisible():
        print("paintGL()** not ready {} {}".format(self.textureID, self.isValid()))
        return
    # No need for makeCurrent() since it is called from PaintGL() only
    # self.makeCurrent()
    painter = QtGui.QPainter()
    painter.begin(self)
    painter.beginNativePainting()
    im_pos = None
    scale  = None
    try:
        self.updateViewPort()
        scale = self.updateTransforms()
        self.myPaintGL()
        if self.show_cursor:
            im_pos = self.gl_draw_cursor()
    except Exception as e:
        self.print_log(" failed paintGL {}".format(e))
        traceback.print_exc()
    painter.endNativePainting()
    # status = gl.glGetError()

    draw_text = True
    if draw_text:
        # adapt scale depending on the ratio image / viewport
        scale *= self._width/self._image.data.shape[1]
        self.display_text(painter, self.display_message(im_pos, scale))

    # draw histogram
    if self.show_histogram:
        current_image = self._image.data
        rect = QtCore.QRect(0, 0, self.width(), self.height())
        histograms = self.compute_histogram_Cpp(current_image, show_timings=self.display_timing)
        self.display_histogram(histograms, 1,  painter, rect, show_timings=self.display_timing)

    painter.end()
    # Seems required here, at least on linux
    # self.update()
def resizeEvent(self, event)

Called upon window resizing: reinitialize the viewport.

Expand source code
def resizeEvent(self, event):
    """Called upon window resizing: reinitialize the viewport.
    """
    if self.trace_calls:
        t = trace_method(self.tab)
    self.print_log(f"resize {event.size()}  self {self.width()} {self.height()}")
    self.evt_width = event.size().width()
    self.evt_height = event.size().height()
    QOpenGLWidget.resizeEvent(self, event)
    self.print_log(f"resize {event.size()}  self {self.width()} {self.height()}")
def resizeGL(self, width, height)

Called upon window resizing: reinitialize the viewport.

Expand source code
def resizeGL(self, width, height):
    """Called upon window resizing: reinitialize the viewport.
    """
    # print("ResizeGL")
    if self.trace_calls:
        t = trace_method(self.tab)
    # size give for opengl are in pixels, qt uses device independent size otherwise
    print(f"self.devicePixelRatio() {self.devicePixelRatio()}")
    self._width = width*self.devicePixelRatio()
    self._height = height*self.devicePixelRatio()
    # print("width height ratios {} {}".format(self._width/self.width(), self._height/self.height()))
    self.viewer_update()
def setTexture(self)

:return: set opengl texture based on input numpy array image

Expand source code
def setTexture(self):
    """
    :return: set opengl texture based on input numpy array image
    """
    # gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_GENERATE_MIPMAP_SGIS, gl.GL_TRUE)

    if self.trace_calls:
        t = trace_method(self.tab)
    self.start_timing()
    self.makeCurrent()

    # Replace texture only if required
    if self._image is None:
        print("self._image is None")
        return False

    img_height, img_width = self._image.data.shape[:2]

    # default type
    gl_types = {
        'int8'   : gl.GL_BYTE,
        'uint8'  : gl.GL_UNSIGNED_BYTE,
        'int16'  : gl.GL_SHORT,
        'uint16' : gl.GL_UNSIGNED_SHORT,
        'int32'  : gl.GL_INT,
        'uint32' : gl.GL_UNSIGNED_INT,
        'float32': gl.GL_FLOAT,
        'float64': gl.GL_DOUBLE
    }
    gl_type = gl_types[self._image.data.dtype.name]

    # It seems that the dimension in X should be even

    # Not sure what is the right parameter for internal format of 2D texture based
    # on the input data type uint8, uint16, ...
    # need to test with different DXR images
    internal_format = gl.GL_RGB
    if self._image.data.shape[2] == 3:
        if self._image.precision == 8:
            internal_format = gl.GL_RGB
        if self._image.precision == 10:
            internal_format = gl.GL_RGB10
        if self._image.precision == 12:
            internal_format = gl.GL_RGB12
    if self._image.data.shape[2] == 4:
        if self._image.precision == 8:
            internal_format = gl.GL_RGBA
        else:
            if self._image.precision <= 12:
                internal_format = gl.GL_RGBA12
            else:
                if self._image.precision <= 16:
                    internal_format = gl.GL_RGBA16
                else:
                    internal_format = gl.GL_RGBA32F

    channels2format = {
        ImageFormat.CH_RGB  : gl.GL_RGB,
        ImageFormat.CH_BGR  : gl.GL_BGR,
        ImageFormat.CH_Y    : gl.GL_RED, # not sure about this one
        ImageFormat.CH_RGGB : gl.GL_RGBA, # we save 4 component data
        ImageFormat.CH_GRBG : gl.GL_RGBA,
        ImageFormat.CH_GBRG : gl.GL_RGBA,
        ImageFormat.CH_BGGR : gl.GL_RGBA
    }
    texture_pixel_format = channels2format[self._image.channels]

    if (self.tex_width,self.tex_height) != (img_width,img_height):
        if self.textureID is not None:
            gl.glDeleteTextures(np.array([self.textureID]))
        self.textureID = gl.glGenTextures(1)
        gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 4)
        gl.glBindTexture(gl.GL_TEXTURE_2D, self.textureID)
        gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_BASE_LEVEL, 0)
        gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAX_LEVEL, 0)
        # gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAX_LEVEL, 10)
        # gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
        # gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST_MIPMAP_NEAREST)
        gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
        gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
        gl.glTexImage2D(gl.GL_TEXTURE_2D, 0,
                        internal_format,
                        img_width, img_height,
                     0, texture_pixel_format, gl_type, self._image.data)
        # gl.glGenerateMipmap(gl.GL_TEXTURE_2D)
        self.tex_width, self.tex_height = img_width, img_height
    else:
        try:
            gl.glTexSubImage2D(gl.GL_TEXTURE_2D, 0, 0, 0, img_width, img_height,
                         texture_pixel_format, gl_type, self._image.data)
            # gl.glGenerateMipmap(gl.GL_TEXTURE_2D)
        except Exception as e:
            print("setTexture failed shape={}: {}".format(self._image.data.shape, e))
            return False

    self.print_timing(add_total=True)
    self.opengl_error()
    return True
def set_cursor_image_position(self, cursor_x, cursor_y)

Sets the image position from the cursor in proportion of the image dimension :return:

Expand source code
def set_cursor_image_position(self, cursor_x, cursor_y):
    """
    Sets the image position from the cursor in proportion of the image dimension
    :return:
    """
    self.updateTransforms()
    ratio = self.screen().devicePixelRatio()
    self.print_log("ratio {}".format(ratio))
    pos_x = cursor_x * ratio
    pos_y = (self.height() - cursor_y) * ratio
    self.print_log("pos {} {}".format(pos_x, pos_y))
    x0, x1, y0, y1 = self.image_centered_position()

    gl_posX, gl_posY = self.get_mouse_gl_coordinates(pos_x, pos_y)
    self.cursor_imx_ratio = (gl_posX - x0) / (x1 - x0)
    self.cursor_imy_ratio = 1 - (gl_posY - y0) / (y1 - y0)
    self.print_log("cursor ratio {} {}".format(self.cursor_imx_ratio, self.cursor_imy_ratio))
def set_image(self, image)
Expand source code
def set_image(self, image):
    if self.trace_calls:
        t = trace_method(self.tab)
    changed = super(GLImageViewerBase, self).set_image(image)

    img_width = self._image.data.shape[1]
    if img_width % 4 != 0:
        print("Image is resized to a multiple of 4 dimension in X")
        img_width = ((img_width >> 4) << 4)
        im = np.ascontiguousarray(self._image.data[:,:img_width, :])
        self._image = ViewerImage(im, precision = self._image.precision, downscale = 1,
                                    channels = self._image.channels)
        print(self._image.data.shape)

    if changed:  # and self.textureID is not None:
        if self.setTexture():
            self.show()
            self.update()
        else:
            print("setTexture() return False")
def synchronize_data(self, other_viewer)
Expand source code
def synchronize_data(self, other_viewer):
    super(GLImageViewerBase, self).synchronize_data(other_viewer)
    other_viewer.cursor_imx_ratio = self.cursor_imx_ratio
    other_viewer.cursor_imy_ratio = self.cursor_imy_ratio
def updateTransforms(self) ‑> float
Expand source code
def updateTransforms(self) -> float:
    if self.trace_calls:
        t = trace_method(self.tab)
    self.start_timing()
    self.makeCurrent()
    w = self._width
    h = self._height
    dx, dy = self.new_translation()
    scale = self.new_scale(self.mouse_zy, self.tex_height)
    print(f"updateTransforms scale {scale}")
    try:
        # print("current context ", QtOpenGL.QGLContext.currentContext())
        # gl = QtOpenGL.QGLContext.currentContext().functions()
        # update the window size
        gl.glMatrixMode(gl.GL_PROJECTION)
        gl.glLoadIdentity()
        translation_unit = min(w, h)/2
        # self.print_log("scale {}".format(scale))
        gl.glScale(scale, scale, scale)
        gl.glTranslate(dx/translation_unit, dy/translation_unit, 0)
        # the window corner OpenGL coordinates are (-+1, -+1)
        gl.glOrtho(0, w, 0, h, -1, 1)
        gl.glMatrixMode(gl.GL_MODELVIEW)
        gl.glLoadIdentity()
    except Exception as e:
        self.print_log(" setting gl matrices failed {}".format(e))
    self.print_timing(add_total=True)
    self.opengl_error()
    return scale
def updateViewPort(self)
Expand source code
def updateViewPort(self):
    if self.trace_calls:
        t = trace_method(self.tab)
    self.start_timing()
    # keep image proportions
    w = self._width
    h = self._height
    # print(f" w, h {w, h}")
    try:
        gl.glViewport(0,0,w,h) # keep everything in viewport
    except Exception as e:
        self.print_log(" failed glViewport {}".format(e))
    self.print_timing(add_total=True)
def viewer_update(self)
Expand source code
def viewer_update(self):
    self.update()
def wheelEvent(self, event)

wheelEvent(self, event: PySide6.QtGui.QWheelEvent) -> None

Expand source code
def wheelEvent(self, event):
    self.mouse_wheel_event(event)

Inherited members