Module qimview.image_viewers

Expand source code
# Import from the less dependent to the most dependent module
from .image_filter_parameters     import ImageFilterParameters
from .image_filter_parameters_gui import ImageFilterParametersGui
from .qt_image_viewer             import QTImageViewer
from .gl_image_viewer             import GLImageViewer
from .gl_image_viewer_shaders     import GLImageViewerShaders
from .multi_view                  import MultiView, ViewerType

__all__ = [
    'ImageFilterParameters',
    'ImageFilterParametersGui',
    'QTImageViewer',
    'GLImageViewer',
    'GLImageViewerShaders',
    'ViewerType',
    'MultiView',
]

Sub-modules

qimview.image_viewers.gl_image_viewer
qimview.image_viewers.gl_image_viewer_base
qimview.image_viewers.gl_image_viewer_shaders
qimview.image_viewers.image_filter_parameters
qimview.image_viewers.image_filter_parameters_gui
qimview.image_viewers.image_viewer
qimview.image_viewers.multi_view
qimview.image_viewers.qt_image_viewer

Classes

class GLImageViewer (parent=None, event_recorder=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 GLImageViewer(GLImageViewerBase):

    def __init__(self, parent=None, event_recorder=None):
        self.event_recorder = event_recorder
        super().__init__(parent)
        self.setAutoFillBackground(False)
        self.textureID  = None
        self.tex_width, self.tex_height = 0, 0
        self.opengl_debug = True
        self.trace_calls  = False

    def initializeGL(self):
        """Initialize OpenGL, VBOs, upload data on the GPU, etc.
        """
        # self.setTexture()
        pass

    def viewer_update(self):
        self.update()

    def paintGL(self):
        self.paintAll()

    def myPaintGL(self):
        """Paint the scene.
        """
        if self.trace_calls:
            t = trace_method(self.tab)
        self.start_timing()
        if self.textureID is None:
            print("GLImageViewer paintGL not textureID")
            return
        gl.glClear(gl.GL_COLOR_BUFFER_BIT)
        gl.glTexEnvi(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_DECAL)
        gl.glBindTexture(gl.GL_TEXTURE_2D, self.textureID)
        # gl.glGenerateMipmap (gl.GL_TEXTURE_2D)
        gl.glEnable(gl.GL_TEXTURE_2D)
        gl.glBegin(gl.GL_QUADS)

        x0, x1, y0, y1 = self.image_centered_position()
        x0 = int(x0)
        x1 = int(x1)
        y0 = int(y0)
        y1 = int(y1)
        # print("{} {} {} {}".format(x0,x1,y0,y1))

        gl.glTexCoord2i(0, 0)
        gl.glVertex2i(x0, y1)

        gl.glTexCoord2i(0, 1)
        gl.glVertex2i(x0, y0)

        gl.glTexCoord2i(1, 1)
        gl.glVertex2i(x1, y0)

        gl.glTexCoord2i(1, 0)
        gl.glVertex2i(x1, y1)

        gl.glEnd()

        gl.glDisable(gl.GL_TEXTURE_2D)
        gl.glTexEnvi(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_MODULATE)

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

    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 event(self, evt):
        if self.event_recorder is not None:
            self.event_recorder.store_event(self, evt)
        return super().event(evt)

Ancestors

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

Class variables

var staticMetaObject

Methods

def event(self, evt)

event(self, e: PySide6.QtCore.QEvent) -> bool

Expand source code
def event(self, evt):
    if self.event_recorder is not None:
        self.event_recorder.store_event(self, evt)
    return super().event(evt)
def initializeGL(self)

Initialize OpenGL, VBOs, upload data on the GPU, etc.

Expand source code
def initializeGL(self):
    """Initialize OpenGL, VBOs, upload data on the GPU, etc.
    """
    # self.setTexture()
    pass
def myPaintGL(self)

Paint the scene.

Expand source code
def myPaintGL(self):
    """Paint the scene.
    """
    if self.trace_calls:
        t = trace_method(self.tab)
    self.start_timing()
    if self.textureID is None:
        print("GLImageViewer paintGL not textureID")
        return
    gl.glClear(gl.GL_COLOR_BUFFER_BIT)
    gl.glTexEnvi(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_DECAL)
    gl.glBindTexture(gl.GL_TEXTURE_2D, self.textureID)
    # gl.glGenerateMipmap (gl.GL_TEXTURE_2D)
    gl.glEnable(gl.GL_TEXTURE_2D)
    gl.glBegin(gl.GL_QUADS)

    x0, x1, y0, y1 = self.image_centered_position()
    x0 = int(x0)
    x1 = int(x1)
    y0 = int(y0)
    y1 = int(y1)
    # print("{} {} {} {}".format(x0,x1,y0,y1))

    gl.glTexCoord2i(0, 0)
    gl.glVertex2i(x0, y1)

    gl.glTexCoord2i(0, 1)
    gl.glVertex2i(x0, y0)

    gl.glTexCoord2i(1, 1)
    gl.glVertex2i(x1, y0)

    gl.glTexCoord2i(1, 0)
    gl.glVertex2i(x1, y1)

    gl.glEnd()

    gl.glDisable(gl.GL_TEXTURE_2D)
    gl.glTexEnvi(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_MODULATE)

    self.print_timing(add_total=True)
    self.opengl_error()
def paintGL(self)

paintGL(self) -> None

Expand source code
def paintGL(self):
    self.paintAll()
def viewer_update(self)
Expand source code
def viewer_update(self):
    self.update()

Inherited members

class GLImageViewerShaders (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 GLImageViewerShaders(GLImageViewerBase):
    # vertex shader program
    vertexShader = """
        #version 330 core

        attribute vec3 vert;
        attribute vec2 uV;
        uniform mat4 mvMatrix;
        uniform mat4 pMatrix;
        out vec2 UV;

        void main() {
          gl_Position = pMatrix * mvMatrix * vec4(vert, 1.0);
          UV = uV;
        }
        """
    # fragment shader program
    fragmentShader_RGB = """
        #version 330 core
    
        in vec2 UV;
        uniform sampler2D backgroundTexture;
        uniform int channels; // channel representation
        uniform float white_level;
        uniform float black_level;
        uniform float g_r_coeff;
        uniform float g_b_coeff;
        uniform float max_value; // maximal value based on image precision
        uniform float max_type;  // maximal value based on image type (uint8, etc...)
        uniform float gamma;
        out vec3 colour;
    
        void main() {
          colour = texture(backgroundTexture, UV).rgb;

          // black level
          colour.rgb = colour.rgb/max_value*max_type;
          colour.rgb = max((colour.rgb-vec3(black_level).rgb),0);

          // white balance
          colour.r = colour.r*g_r_coeff;
          colour.b = colour.b*g_b_coeff;

          // rescale to white level as saturation level
          colour.rgb = colour.rgb/(white_level-black_level);
          
          // apply gamma
          colour.rgb = pow(colour.rgb, vec3(1.0/gamma).rgb);

        }
    """

    fragmentShader_RAW = """
        #version 330 core
        
        in vec2 UV;
        uniform sampler2D backgroundTexture;
        uniform int channels; // channel representation
        uniform float white_level;
        uniform float black_level;
        uniform float g_r_coeff;
        uniform float g_b_coeff;
        uniform float max_value; // maximal value based on image precision
        uniform float max_type;  // maximal value based on image type (uint8, etc...)
        uniform float gamma;
        out vec3 colour;

        void main() {

           const int CH_RGGB = 4; // phase 0, bayer 2
           const int CH_GRBG = 5; // phase 1, bayer 3 (Boilers)
           const int CH_GBRG = 6; // phase 2, bayer 0
           const int CH_BGGR = 7; // phase 3, bayer 1 (Coconuts)

          vec4 bayer = texture(backgroundTexture, UV);
          // transform bayer data to RGB
          int r,gr,gb,b;
          switch (channels) {
            case 4:   r = 0; gr = 1; gb = 2; b = 3;  break; // CH_RGGB = 4 phase 0, bayer 2
            case 5:   r = 1; gr = 0; gb = 3; b = 2;  break; // CH_GRBG = 5 phase 1, bayer 3 (Boilers)
            case 6:   r = 2; gr = 3; gb = 0; b = 1;  break; // CH_GBRG = 6 phase 2, bayer 0
            case 7:   r = 3; gr = 2; gb = 1; b = 0;  break; // CH_BGGR = 7 phase 3, bayer 1 (Coconuts)
            default:        r = 0; gr = 1; gb = 2; b = 3;  break; // this should not happen
          }

          // first retreive black point to get the coefficients right ...
          // 5% of dynamics?
          
          // bayer 2 rgb
          colour.r   = bayer[r];
          colour.g = (bayer[gr]+bayer[gb])/2.0;
          colour.b = bayer[b];

          // black level
          colour.rgb = colour.rgb/max_value*max_type;
          colour.rgb = max((colour.rgb-vec3(black_level).rgb),0);
          
          // white balance
          colour.r = colour.r*g_r_coeff;
          colour.b = colour.b*g_b_coeff;

          // rescale to white level as saturation level
          colour.rgb = colour.rgb/(white_level-black_level);
          
          // apply gamma
          colour.rgb = pow(colour.rgb, vec3(1.0/gamma).rgb);
        }
    """

    def __init__(self, parent=None):
        super().__init__(parent)

        self.setAutoFillBackground(False)
        self.textureID = None
        self.tex_width, self.tex_height = 0, 0
        self.opengl_debug = False
        self.synchronize_viewer = None
        self.pMatrix  = np.identity(4, dtype=np.float32)
        self.mvMatrix = np.identity(4, dtype=np.float32)
        self.program_RGB = None
        self.program_RAW = None
        self.program = None
        self.vertexBuffer = None

    def set_shaders(self):
        if self.program_RGB is None:
            vs = shaders.compileShader(self.vertexShader, gl.GL_VERTEX_SHADER)
            fs = shaders.compileShader(self.fragmentShader_RGB, gl.GL_FRAGMENT_SHADER)
            try:
                self.program_RGB = shaders.compileProgram(vs, fs, validate=False)
                print("\n***** self.program_RGB = {} *****\n".format(self.program_RGB))
            except Exception as e:
                print('failed RGB shaders.compileProgram() {}'.format(e))
            shaders.glDeleteShader(vs)
            shaders.glDeleteShader(fs)

        if self.program_RAW is None:
            vs = shaders.compileShader(self.vertexShader, gl.GL_VERTEX_SHADER)
            fs = shaders.compileShader(self.fragmentShader_RAW, gl.GL_FRAGMENT_SHADER)
            try:
                self.program_RAW = shaders.compileProgram(vs, fs, validate=False)
                print("\n***** self.program_RAW = {} *****\n".format(self.program_RAW))
            except Exception as e:
                print('failed RAW shaders.compileProgram() {}'.format(e))
            shaders.glDeleteShader(vs)
            shaders.glDeleteShader(fs)

    def setVerticesBufferData(self):
        try:
            x0, x1, y0, y1 = self.image_centered_position()
            # print(" x0, x1, y0, y1 {} {} {} {}".format(x0, x1, y0, y1))
        except Exception as e:
            print(" Failed image_centered_position() {}".format(e))
            x0, x1, y0, y1 = 0, 100, 0, 100
            # set background vertices
        backgroundVertices = [
            x0, y1, 0.0,
            x0, y0, 0.0,
            x1, y1, 0.0,
            x1, y1, 0.0,
            x0, y0, 0.0,
            x1, y0, 0.0]
        vertexData = np.array(backgroundVertices, np.float32)

        if self.vertexBuffer is not None:
            self.vertexBuffer.destroy()
        self.vertexBuffer = QOpenGLBuffer()
        self.vertexBuffer.create()
        self.vertexBuffer.bind()
        self.vertexBuffer.allocate(vertexData, 4 * len(vertexData))

    def setBufferData(self):
        # set background UV
        backgroundUV = [
            0.0, 0.0,
            0.0, 1.0,
            1.0, 0.0,
            1.0, 0.0,
            0.0, 1.0,
            1.0, 1.0]
        uvData = np.array(backgroundUV, np.float32)

        self.uvBuffer = QOpenGLBuffer()
        self.uvBuffer.create()
        self.uvBuffer.bind()
        self.uvBuffer.allocate(uvData, 4 * len(uvData))

    def setTexture(self):
        texture_ok = super(GLImageViewerShaders, self).setTexture()
        self.setVerticesBufferData()
        return texture_ok

    def resizeGL(self, width, height):
        """Called upon window resizing: reinitialize the viewport.
        """
        # print(f"resizeGL {width}x{height}")
        if self.trace_calls:
            t = trace_method(self.tab)
        self._width = width*self.devicePixelRatio()
        self._height = height*self.devicePixelRatio()
        self.setVerticesBufferData()
        self.update()

    def initializeGL(self):
        """
        Initialize OpenGL, VBOs, upload data on the GPU, etc.
        """
        self.start_timing()

        time1 = get_time()
        self.set_shaders()
        self.add_time('set_shaders', time1)

        self.setVerticesBufferData()
        self.setBufferData()
        self.print_timing()

    def viewer_update(self):
        self.update()

    def paintGL(self):
        self.paintAll()

    def myPaintGL(self):
        """Paint the scene.
        """
        if self.textureID is None or not self.isValid():
            print("paintGL() not ready")
            return
        self.opengl_error()
        self.start_timing()

        gl.glClear(gl.GL_COLOR_BUFFER_BIT)

        if self._image.data.shape[2] == 4:
            self.program = self.program_RAW
        else:
            # TODO: check for other types: scalar ...
            self.program = self.program_RGB

        # Obtain uniforms and attributes
        self.aVert              = shaders.glGetAttribLocation(self.program, "vert")
        self.aUV                = shaders.glGetAttribLocation(self.program, "uV")
        self.uPMatrix           = shaders.glGetUniformLocation(self.program, 'pMatrix')
        self.uMVMatrix          = shaders.glGetUniformLocation(self.program, "mvMatrix")
        self.uBackgroundTexture = shaders.glGetUniformLocation(self.program, "backgroundTexture")
        self.channels_location    = shaders.glGetUniformLocation(self.program, "channels")
        self.black_level_location = shaders.glGetUniformLocation(self.program, "black_level")
        self.white_level_location = shaders.glGetUniformLocation(self.program, "white_level")
        self.g_r_coeff_location   = shaders.glGetUniformLocation(self.program, "g_r_coeff")
        self.g_b_coeff_location   = shaders.glGetUniformLocation(self.program, "g_b_coeff")
        self.max_value_location   = shaders.glGetUniformLocation(self.program, "max_value")
        self.max_type_location    = shaders.glGetUniformLocation(self.program, "max_type")
        self.gamma_location       = shaders.glGetUniformLocation(self.program, "gamma")

        # use shader program
        self.print_log("self.program = {}".format(self.program))
        shaders.glUseProgram(self.program)

        # set uniforms
        gl.glUniformMatrix4fv(self.uPMatrix, 1, gl.GL_FALSE, self.pMatrix)
        gl.glUniformMatrix4fv(self.uMVMatrix, 1, gl.GL_FALSE, self.mvMatrix)
        gl.glUniform1i(self.uBackgroundTexture, 0)

        gl.glUniform1i( self.channels_location, self._image.channels)

        # set color transformation parameters
        self.print_log("levels {} {}".format(self.filter_params.black_level.value,
                                             self.filter_params.white_level.value))
        gl.glUniform1f( self.black_level_location, self.filter_params.black_level.float)
        gl.glUniform1f( self.white_level_location, self.filter_params.white_level.float)

        # white balance coefficients
        gl.glUniform1f(self.g_r_coeff_location, self.filter_params.g_r.float)
        gl.glUniform1f(self.g_b_coeff_location, self.filter_params.g_b.float)

        # Should work for unsigned types for the moment
        gl.glUniform1f( self.max_value_location, (1 << self._image.precision)-1)
        gl.glUniform1f( self.max_type_location,  np.iinfo(self._image.data.dtype).max)

        gl.glUniform1f( self.gamma_location,       self.filter_params.gamma.float)

        # enable attribute arrays
        gl.glEnableVertexAttribArray(self.aVert)
        gl.glEnableVertexAttribArray(self.aUV)

        # set vertex and UV buffers
        # vert_buffers = VertexBuffers()
        # vert_buffers.vert_pos_buffer = vert_pos_buffer
        # vert_buffers.normal_buffer = normal_buffer
        # vert_buffers.tex_coord_buffer = tex_coord_buffer
        # vert_buffers.amount_of_vertices = int(len(index_array) / 3)

        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexBuffer.bufferId())
        gl.glVertexAttribPointer(self.aVert, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None)
        gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.uvBuffer.bufferId())
        gl.glVertexAttribPointer(self.aUV, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, None)

        # bind background texture
        # gl.glActiveTexture(gl.GL_TEXTURE0)
        gl.glBindTexture(gl.GL_TEXTURE_2D, self.textureID)
        gl.glEnable(gl.GL_TEXTURE_2D)

        # draw
        gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6)

        # disable attribute arrays
        gl.glDisableVertexAttribArray(self.aVert)
        gl.glDisableVertexAttribArray(self.aUV)
        gl.glDisable(gl.GL_TEXTURE_2D)

        shaders.glUseProgram(0)

        self.print_timing(force=True)

    def updateTransforms(self) -> float:
        if self.trace_calls:
            t = trace_method(self.tab)
        if self.display_timing:
            start_time = get_time()
        self.makeCurrent()
        w = self._width
        h = self._height
        dx, dy = self.new_translation()
        # scale = max(self.mouse_zx, self.mouse_zy)
        scale = self.new_scale(self.mouse_zy, self.tex_height)
        # update the window size
        gl.glMatrixMode(gl.GL_PROJECTION)
        gl.glLoadIdentity()
        translation_unit = min(w, h)/2
        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)
        self.pMatrix = np.array(gl.glGetFloatv(gl.GL_PROJECTION_MATRIX), dtype=np.float32).flatten()

        gl.glMatrixMode(gl.GL_MODELVIEW)
        gl.glLoadIdentity()
        self.mvMatrix = np.array(gl.glGetFloatv(gl.GL_MODELVIEW_MATRIX), dtype=np.float32).flatten()
        if self.display_timing:
            self.print_log('updateTransforms time {:0.1f} ms'.format((get_time()-start_time)*1000))
        return scale

Ancestors

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

Class variables

var fragmentShader_RAW
var fragmentShader_RGB
var staticMetaObject
var vertexShader

Methods

def initializeGL(self)

Initialize OpenGL, VBOs, upload data on the GPU, etc.

Expand source code
def initializeGL(self):
    """
    Initialize OpenGL, VBOs, upload data on the GPU, etc.
    """
    self.start_timing()

    time1 = get_time()
    self.set_shaders()
    self.add_time('set_shaders', time1)

    self.setVerticesBufferData()
    self.setBufferData()
    self.print_timing()
def myPaintGL(self)

Paint the scene.

Expand source code
def myPaintGL(self):
    """Paint the scene.
    """
    if self.textureID is None or not self.isValid():
        print("paintGL() not ready")
        return
    self.opengl_error()
    self.start_timing()

    gl.glClear(gl.GL_COLOR_BUFFER_BIT)

    if self._image.data.shape[2] == 4:
        self.program = self.program_RAW
    else:
        # TODO: check for other types: scalar ...
        self.program = self.program_RGB

    # Obtain uniforms and attributes
    self.aVert              = shaders.glGetAttribLocation(self.program, "vert")
    self.aUV                = shaders.glGetAttribLocation(self.program, "uV")
    self.uPMatrix           = shaders.glGetUniformLocation(self.program, 'pMatrix')
    self.uMVMatrix          = shaders.glGetUniformLocation(self.program, "mvMatrix")
    self.uBackgroundTexture = shaders.glGetUniformLocation(self.program, "backgroundTexture")
    self.channels_location    = shaders.glGetUniformLocation(self.program, "channels")
    self.black_level_location = shaders.glGetUniformLocation(self.program, "black_level")
    self.white_level_location = shaders.glGetUniformLocation(self.program, "white_level")
    self.g_r_coeff_location   = shaders.glGetUniformLocation(self.program, "g_r_coeff")
    self.g_b_coeff_location   = shaders.glGetUniformLocation(self.program, "g_b_coeff")
    self.max_value_location   = shaders.glGetUniformLocation(self.program, "max_value")
    self.max_type_location    = shaders.glGetUniformLocation(self.program, "max_type")
    self.gamma_location       = shaders.glGetUniformLocation(self.program, "gamma")

    # use shader program
    self.print_log("self.program = {}".format(self.program))
    shaders.glUseProgram(self.program)

    # set uniforms
    gl.glUniformMatrix4fv(self.uPMatrix, 1, gl.GL_FALSE, self.pMatrix)
    gl.glUniformMatrix4fv(self.uMVMatrix, 1, gl.GL_FALSE, self.mvMatrix)
    gl.glUniform1i(self.uBackgroundTexture, 0)

    gl.glUniform1i( self.channels_location, self._image.channels)

    # set color transformation parameters
    self.print_log("levels {} {}".format(self.filter_params.black_level.value,
                                         self.filter_params.white_level.value))
    gl.glUniform1f( self.black_level_location, self.filter_params.black_level.float)
    gl.glUniform1f( self.white_level_location, self.filter_params.white_level.float)

    # white balance coefficients
    gl.glUniform1f(self.g_r_coeff_location, self.filter_params.g_r.float)
    gl.glUniform1f(self.g_b_coeff_location, self.filter_params.g_b.float)

    # Should work for unsigned types for the moment
    gl.glUniform1f( self.max_value_location, (1 << self._image.precision)-1)
    gl.glUniform1f( self.max_type_location,  np.iinfo(self._image.data.dtype).max)

    gl.glUniform1f( self.gamma_location,       self.filter_params.gamma.float)

    # enable attribute arrays
    gl.glEnableVertexAttribArray(self.aVert)
    gl.glEnableVertexAttribArray(self.aUV)

    # set vertex and UV buffers
    # vert_buffers = VertexBuffers()
    # vert_buffers.vert_pos_buffer = vert_pos_buffer
    # vert_buffers.normal_buffer = normal_buffer
    # vert_buffers.tex_coord_buffer = tex_coord_buffer
    # vert_buffers.amount_of_vertices = int(len(index_array) / 3)

    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexBuffer.bufferId())
    gl.glVertexAttribPointer(self.aVert, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None)
    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.uvBuffer.bufferId())
    gl.glVertexAttribPointer(self.aUV, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, None)

    # bind background texture
    # gl.glActiveTexture(gl.GL_TEXTURE0)
    gl.glBindTexture(gl.GL_TEXTURE_2D, self.textureID)
    gl.glEnable(gl.GL_TEXTURE_2D)

    # draw
    gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6)

    # disable attribute arrays
    gl.glDisableVertexAttribArray(self.aVert)
    gl.glDisableVertexAttribArray(self.aUV)
    gl.glDisable(gl.GL_TEXTURE_2D)

    shaders.glUseProgram(0)

    self.print_timing(force=True)
def paintGL(self)

paintGL(self) -> None

Expand source code
def paintGL(self):
    self.paintAll()
def setBufferData(self)
Expand source code
def setBufferData(self):
    # set background UV
    backgroundUV = [
        0.0, 0.0,
        0.0, 1.0,
        1.0, 0.0,
        1.0, 0.0,
        0.0, 1.0,
        1.0, 1.0]
    uvData = np.array(backgroundUV, np.float32)

    self.uvBuffer = QOpenGLBuffer()
    self.uvBuffer.create()
    self.uvBuffer.bind()
    self.uvBuffer.allocate(uvData, 4 * len(uvData))
def setVerticesBufferData(self)
Expand source code
def setVerticesBufferData(self):
    try:
        x0, x1, y0, y1 = self.image_centered_position()
        # print(" x0, x1, y0, y1 {} {} {} {}".format(x0, x1, y0, y1))
    except Exception as e:
        print(" Failed image_centered_position() {}".format(e))
        x0, x1, y0, y1 = 0, 100, 0, 100
        # set background vertices
    backgroundVertices = [
        x0, y1, 0.0,
        x0, y0, 0.0,
        x1, y1, 0.0,
        x1, y1, 0.0,
        x0, y0, 0.0,
        x1, y0, 0.0]
    vertexData = np.array(backgroundVertices, np.float32)

    if self.vertexBuffer is not None:
        self.vertexBuffer.destroy()
    self.vertexBuffer = QOpenGLBuffer()
    self.vertexBuffer.create()
    self.vertexBuffer.bind()
    self.vertexBuffer.allocate(vertexData, 4 * len(vertexData))
def set_shaders(self)
Expand source code
def set_shaders(self):
    if self.program_RGB is None:
        vs = shaders.compileShader(self.vertexShader, gl.GL_VERTEX_SHADER)
        fs = shaders.compileShader(self.fragmentShader_RGB, gl.GL_FRAGMENT_SHADER)
        try:
            self.program_RGB = shaders.compileProgram(vs, fs, validate=False)
            print("\n***** self.program_RGB = {} *****\n".format(self.program_RGB))
        except Exception as e:
            print('failed RGB shaders.compileProgram() {}'.format(e))
        shaders.glDeleteShader(vs)
        shaders.glDeleteShader(fs)

    if self.program_RAW is None:
        vs = shaders.compileShader(self.vertexShader, gl.GL_VERTEX_SHADER)
        fs = shaders.compileShader(self.fragmentShader_RAW, gl.GL_FRAGMENT_SHADER)
        try:
            self.program_RAW = shaders.compileProgram(vs, fs, validate=False)
            print("\n***** self.program_RAW = {} *****\n".format(self.program_RAW))
        except Exception as e:
            print('failed RAW shaders.compileProgram() {}'.format(e))
        shaders.glDeleteShader(vs)
        shaders.glDeleteShader(fs)
def updateTransforms(self) ‑> float
Expand source code
def updateTransforms(self) -> float:
    if self.trace_calls:
        t = trace_method(self.tab)
    if self.display_timing:
        start_time = get_time()
    self.makeCurrent()
    w = self._width
    h = self._height
    dx, dy = self.new_translation()
    # scale = max(self.mouse_zx, self.mouse_zy)
    scale = self.new_scale(self.mouse_zy, self.tex_height)
    # update the window size
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    translation_unit = min(w, h)/2
    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)
    self.pMatrix = np.array(gl.glGetFloatv(gl.GL_PROJECTION_MATRIX), dtype=np.float32).flatten()

    gl.glMatrixMode(gl.GL_MODELVIEW)
    gl.glLoadIdentity()
    self.mvMatrix = np.array(gl.glGetFloatv(gl.GL_MODELVIEW_MATRIX), dtype=np.float32).flatten()
    if self.display_timing:
        self.print_log('updateTransforms time {:0.1f} ms'.format((get_time()-start_time)*1000))
    return scale
def viewer_update(self)
Expand source code
def viewer_update(self):
    self.update()

Inherited members

class ImageFilterParameters
Expand source code
class ImageFilterParameters:
    def __init__(self):
        # white/black levels
        # default_black = int(4095*5/100)
        default_black = 0
        self.black_level = NumericParameter(default_black, default_black, [0, 4095]  , 4095)
        self.white_level = NumericParameter(4095, 4095,            [480, 4095], 4095)
        # gamma curve coefficient
        self.gamma       = NumericParameter(100, 100,            [50, 300], 100)
        # white balance coefficients
        self.g_b = NumericParameter(256, 256, [50, 512], 256)
        self.g_r = NumericParameter(256, 256, [50, 512], 256)
        # Saturation
        self.saturation = NumericParameter(50, 50, [0, 150], 50)
        # Image difference factor
        self.imdiff_factor = NumericParameter(30, 30, [1, 100], 10)

    def copy_from(self, p):
        for v in vars(self):
            if isinstance(self.__dict__[v], NumericParameter):
                self.__dict__[v].copy_from(p.__dict__[v])

    def is_equal(self, other):
        if not isinstance(other, ImageFilterParameters):
            return NotImplemented
        for v in vars(self):
            var1 = self.__dict__[v]
            if isinstance(var1, NumericParameter):
                var2 = other.__dict__[v]
                if var1.float != var2.float:
                    return False
        return True

    def __repr__(self):
        return f"<ImageFilterParameters {id(self)}>"

    def __str__(self):
        res = ""
        for v in vars(self):
            res += f"{v}:{self.__dict__[v]}; "
        return res

Methods

def copy_from(self, p)
Expand source code
def copy_from(self, p):
    for v in vars(self):
        if isinstance(self.__dict__[v], NumericParameter):
            self.__dict__[v].copy_from(p.__dict__[v])
def is_equal(self, other)
Expand source code
def is_equal(self, other):
    if not isinstance(other, ImageFilterParameters):
        return NotImplemented
    for v in vars(self):
        var1 = self.__dict__[v]
        if isinstance(var1, NumericParameter):
            var2 = other.__dict__[v]
            if var1.float != var2.float:
                return False
    return True
class ImageFilterParametersGui (parameters, name='')

:param parameters: instance of ImageFilterParameters

Expand source code
class ImageFilterParametersGui:
    def __init__(self, parameters, name=""):
        """
        :param parameters: instance of ImageFilterParameters
        """
        self.params    = parameters
        self.bl_gui    = None
        self.wl_gui    = None
        self.gamma_gui = None
        self.g_r_gui   = None
        self.g_b_gui   = None
        self.saturation_gui    = None
        self.imdiff_factor_gui = None
        self.event_recorder    = None
        self.name              = name

    def set_event_recorder(self, evtrec):
        self.event_recorder = evtrec

    def add_blackpoint(self, layout, callback):
        self.bl_gui = NumericParameterGui("Black", self.params.black_level, callback, layout, self.name)
        self.bl_gui.set_event_recorder(self.event_recorder)

    def add_whitepoint(self, layout, callback):
        self.wl_gui = NumericParameterGui("White", self.params.white_level, callback, layout, self.name)
        self.wl_gui.set_event_recorder(self.event_recorder)

    def add_gamma(self, layout, callback):
        self.gamma_gui = NumericParameterGui("Gamma", self.params.gamma, callback, layout, self.name)
        self.gamma_gui.set_event_recorder(self.event_recorder)

    def add_g_r(self, layout, callback):
        self.g_r_gui = NumericParameterGui("G/R", self.params.g_r, callback, layout, self.name)
        self.g_r_gui.set_event_recorder(self.event_recorder)

    def add_g_b(self, layout, callback):
        self.g_b_gui = NumericParameterGui("G/B", self.params.g_b, callback, layout, self.name)
        self.g_b_gui.set_event_recorder(self.event_recorder)

    def add_saturation(self, layout, callback):
        self.saturation_gui = NumericParameterGui("Saturation", self.params.saturation, callback, layout, self.name)
        self.saturation_gui.set_event_recorder(self.event_recorder)

    def add_imdiff_factor(self, layout, callback):
        self.imdiff_factor_gui = NumericParameterGui("Image diff factor", self.params.imdiff_factor, callback, layout, self.name)
        self.imdiff_factor_gui.set_event_recorder(self.event_recorder)

    def register_event_player(self, event_player):
        for v in vars(self):
            if 'gui' in v and self.__dict__[v]:
                self.__dict__[v].register_event_player(event_player)

    def reset_all(self):
        for v in vars(self):
            if 'gui' in v:
                self.__dict__[v].reset()

Methods

def add_blackpoint(self, layout, callback)
Expand source code
def add_blackpoint(self, layout, callback):
    self.bl_gui = NumericParameterGui("Black", self.params.black_level, callback, layout, self.name)
    self.bl_gui.set_event_recorder(self.event_recorder)
def add_g_b(self, layout, callback)
Expand source code
def add_g_b(self, layout, callback):
    self.g_b_gui = NumericParameterGui("G/B", self.params.g_b, callback, layout, self.name)
    self.g_b_gui.set_event_recorder(self.event_recorder)
def add_g_r(self, layout, callback)
Expand source code
def add_g_r(self, layout, callback):
    self.g_r_gui = NumericParameterGui("G/R", self.params.g_r, callback, layout, self.name)
    self.g_r_gui.set_event_recorder(self.event_recorder)
def add_gamma(self, layout, callback)
Expand source code
def add_gamma(self, layout, callback):
    self.gamma_gui = NumericParameterGui("Gamma", self.params.gamma, callback, layout, self.name)
    self.gamma_gui.set_event_recorder(self.event_recorder)
def add_imdiff_factor(self, layout, callback)
Expand source code
def add_imdiff_factor(self, layout, callback):
    self.imdiff_factor_gui = NumericParameterGui("Image diff factor", self.params.imdiff_factor, callback, layout, self.name)
    self.imdiff_factor_gui.set_event_recorder(self.event_recorder)
def add_saturation(self, layout, callback)
Expand source code
def add_saturation(self, layout, callback):
    self.saturation_gui = NumericParameterGui("Saturation", self.params.saturation, callback, layout, self.name)
    self.saturation_gui.set_event_recorder(self.event_recorder)
def add_whitepoint(self, layout, callback)
Expand source code
def add_whitepoint(self, layout, callback):
    self.wl_gui = NumericParameterGui("White", self.params.white_level, callback, layout, self.name)
    self.wl_gui.set_event_recorder(self.event_recorder)
def register_event_player(self, event_player)
Expand source code
def register_event_player(self, event_player):
    for v in vars(self):
        if 'gui' in v and self.__dict__[v]:
            self.__dict__[v].register_event_player(event_player)
def reset_all(self)
Expand source code
def reset_all(self):
    for v in vars(self):
        if 'gui' in v:
            self.__dict__[v].reset()
def set_event_recorder(self, evtrec)
Expand source code
def set_event_recorder(self, evtrec):
    self.event_recorder = evtrec
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 QTImageViewer (parent=None, event_recorder=None)

QWidget(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 QTImageViewer(BaseWidget, ImageViewer ):

    def __init__(self, parent=None, event_recorder=None):
        super().__init__(parent)
        self.event_recorder = event_recorder
        self.setMouseTracking(True)
        self.anti_aliasing = True
        size_policy = QtWidgets.QSizePolicy()
        size_policy.setHorizontalPolicy(QtWidgets.QSizePolicy.Ignored)
        size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Ignored)
        self.setSizePolicy(size_policy)
        # self.setAlignment(QtCore.Qt.AlignCenter )
        self.output_crop = np.array([0., 0., 1., 1.], dtype=np.float32)
        self.zoom_center = np.array([0.5, 0.5, 0.5, 0.5])


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

        self.paint_cache      = None
        self.paint_diff_cache = None
        self.diff_image       = None

        # self.display_timing = False
        if BaseWidget is QOpenGLWidget:
            self.setAutoFillBackground(True)

        # TODO: how can I set the background color to black without impacting display speed?
        # p = self.palette()
        # p.setColor(self.backgroundRole(), QtCore.Qt.black) 
        # self.setPalette(p)
        # self.setAutoFillBackground(True)

        self.verbose = False
        # self.trace_calls = True

    #def __del__(self):
    #    pass

    def set_image(self, image):
        super().set_image(image)

    def apply_zoom(self, crop):
        (height, width) = self._image.data.shape[:2]
        # print(f"height, width = {height, width}")
        # Apply zoom
        coeff = 1.0/self.new_scale(self.mouse_zy, height)
        # zoom from the center of the image
        center = self.zoom_center
        new_crop = center + (crop - center) * coeff

        # print("new crop zoom 1 {}".format(new_crop))

        # allow crop increase based on the available space
        label_width = self.width()
        # print(f"label_width {label_width}")
        label_height = self.height()

        new_width = width * coeff
        new_height = height * coeff

        ratio_width = float(label_width) / new_width
        ratio_height = float(label_height) / new_height

        # print(f" ratio_width {ratio_width} ratio_height {ratio_height}")
        ratio = min(ratio_width, ratio_height)

        if ratio_width<ratio_height:
            # margin to increase height
            margin_pixels = label_height/ratio - new_height
            margin_height = margin_pixels/height
            new_crop[1] -= margin_height/2
            new_crop[3] += margin_height/2
        else:
            # margin to increase width
            margin_pixels = label_width/ratio - new_width
            margin_width = margin_pixels/width
            new_crop[0] -= margin_width/2
            new_crop[2] += margin_width/2
        # print("new crop zoom 2 {}".format(new_crop))

        return new_crop

    def apply_translation(self, crop):
        """
        :param crop:
        :return: the new crop
        """
        # Apply translation
        diff_x, diff_y = self.new_translation()
        diff_y = - diff_y
        # print(" new translation {} {}".format(diff_x, diff_y))
        # apply the maximal allowed translation
        tr_x = float(diff_x) / self.width()
        tr_y = float(diff_y) / self.height()
        tr_x = clip_value(tr_x, crop[2]-1, crop[0])
        tr_y = clip_value(tr_y, crop[3]-1, crop[1])
        # normalized position relative to the full image
        crop[0] -= tr_x
        crop[1] -= tr_y
        crop[2] -= tr_x
        crop[3] -= tr_y

    def check_translation(self):
        """
        This method computes the translation really applied based on the current requested translation
        :return:
        """
        # Apply zoom
        crop = self.apply_zoom(self.output_crop)

        # Compute the translation that is really applied after applying the constraints
        diff_x, diff_y = self.new_translation()
        diff_y = - diff_y
        # print(" new translation {} {}".format(diff_x, diff_y))
        # apply the maximal allowed translation
        w, h = self.width(), self.height()
        diff_x = clip_value(diff_x, w*(crop[2]-1), w*(crop[0]))
        diff_y = - clip_value(diff_y, h*(crop[3]-1), h*(crop[1]))
        # normalized position relative to the full image
        return diff_x, diff_y

    def update_crop(self):
        # Apply zoom
        new_crop = self.apply_zoom(self.output_crop)
        # print(f"update_crop {self.output_crop} --> {new_crop}")
        # Apply translation
        self.apply_translation(new_crop)
        new_crop = np.clip(new_crop, 0, 1)
        # print("move new crop {}".format(new_crop))
        # print(f"output_crop {self.output_crop} new crop {new_crop}")
        return new_crop

    def update_crop_new(self):
        # 1. transform crop to display coordinates
        
        # Apply zoom
        new_crop = self.apply_zoom(self.output_crop)
        # print(f"update_crop {self.output_crop} --> {new_crop}")
        # Apply translation
        self.apply_translation(new_crop)
        new_crop = np.clip(new_crop, 0, 1)
        # print("move new crop {}".format(new_crop))
        # print(f"output_crop {self.output_crop} new crop {new_crop}")
        return new_crop

    def apply_filters(self, current_image):
        self.print_log(f"current_image.data.shape {current_image.data.shape}")
        # return current_image

        self.start_timing(title='apply_filters()')

        # Output RGB from input
        ch = self._image.channels
        if has_cppbind:
            channels = current_image.channels
            black_level = self.filter_params.black_level.float
            white_level = self.filter_params.white_level.float
            g_r_coeff = self.filter_params.g_r.float
            g_b_coeff = self.filter_params.g_b.float
            saturation = self.filter_params.saturation.float
            max_value = ((1<<current_image.precision)-1)
            max_type = 1  # not used
            gamma = self.filter_params.gamma.float  # not used

            rgb_image = np.empty((current_image.data.shape[0], current_image.data.shape[1], 3), dtype=np.uint8)
            time1 = get_time()
            ok = False
            if ch in ImageFormat.CH_RAWFORMATS() or ch in ImageFormat.CH_RGBFORMATS():
                cases = {
                    'uint8':  { 'func': qimview_cpp.apply_filters_u8_u8  , 'name': 'apply_filters_u8_u8'},
                    'uint16': { 'func': qimview_cpp.apply_filters_u16_u8, 'name': 'apply_filters_u16_u8'},
                    'uint32': { 'func': qimview_cpp.apply_filters_u32_u8, 'name': 'apply_filters_u32_u8'},
                    'int16': { 'func': qimview_cpp.apply_filters_s16_u8, 'name': 'apply_filters_s16_u8'},
                    'int32': { 'func': qimview_cpp.apply_filters_s32_u8, 'name': 'apply_filters_s32_u8'}
                }
                if current_image.data.dtype.name in cases:
                    func = cases[current_image.data.dtype.name]['func']
                    name = cases[current_image.data.dtype.name]['name']
                    self.print_log(f"qimview_cpp.{name}(current_image, rgb_image, channels, "
                          f"black_level={black_level}, white_level={white_level}, "
                          f"g_r_coeff={g_r_coeff}, g_b_coeff={g_b_coeff}, "
                          f"max_value={max_value}, max_type={max_type}, gamma={gamma})")
                    ok = func(current_image.data, rgb_image, channels, black_level, white_level, g_r_coeff,
                                g_b_coeff, max_value, max_type, gamma, saturation)
                    self.add_time(f'{name}()',time1, force=True, title='apply_filters()')
                else:
                    print(f"apply_filters() not available for {current_image.data.dtype} data type !")
            else:
                cases = {
                    'uint8': { 'func': qimview_cpp.apply_filters_scalar_u8_u8, 'name': 'apply_filters_scalar_u8_u8'},
                    'uint16': { 'func': qimview_cpp.apply_filters_scalar_u16_u8, 'name': 'apply_filters_scalar_u16_u8'},
                    'int16': { 'func': qimview_cpp.apply_filters_scalar_s16_u8, 'name': 'apply_filters_scalar_s16_u8'},
                    'uint32': { 'func': qimview_cpp.apply_filters_scalar_u32_u8, 'name': 'apply_filters_scalar_u32_u8'},
                    'float64': { 'func': qimview_cpp.apply_filters_scalar_f64_u8, 'name': 'apply_filters_scalar_f64_u8'},
                }
                if current_image.data.dtype.name.startswith('float'):
                    max_value = 1.0
                if current_image.data.dtype.name in cases:
                    func = cases[current_image.data.dtype.name]['func']
                    name = cases[current_image.data.dtype.name]['name']
                    self.print_log(f"qimview_cpp.{name}(current_image, rgb_image, "
                          f"black_level={black_level}, white_level={white_level}, "
                          f"max_value={max_value}, max_type={max_type}, gamma={gamma})")
                    ok = func(current_image.data, rgb_image, black_level, white_level, max_value, max_type, gamma)
                    self.add_time(f'{name}()', time1, force=True, title='apply_filters()')
                else:
                    print(f"apply_filters_scalar() not available for {current_image.data.dtype} data type !")
            if not ok:
                self.print_log("Failed running wrap_num.apply_filters_u16_u8 ...", force=True)
        else:
            # self.print_log("current channels {}".format(ch))
            if ch in ImageFormat.CH_RAWFORMATS():
                channel_pos = channel_position[current_image.channels]
                self.print_log("Converting to RGB")
                # convert Bayer to RGB
                rgb_image = np.empty((current_image.data.shape[0], current_image.data.shape[1], 3), 
                                        dtype=current_image.data.dtype)
                rgb_image[:, :, 0] = current_image.data[:, :, channel_pos['r']]
                rgb_image[:, :, 1] = (current_image.data[:, :, channel_pos['gr']]+current_image.data[:, :, channel_pos['gb']])/2
                rgb_image[:, :, 2] = current_image.data[:, :, channel_pos['b']]
            else:
                if ch == ImageFormat.CH_Y:
                    # Transform to RGB is it a good idea?
                    rgb_image = np.empty((current_image.data.shape[0], current_image.data.shape[1], 3), 
                                            dtype=current_image.data.dtype)
                    rgb_image[:, :, 0] = current_image.data
                    rgb_image[:, :, 1] = current_image.data
                    rgb_image[:, :, 2] = current_image.data
                else:
                    rgb_image = current_image.data

            # Use cv2.convertScaleAbs(I,a,b) function for fast processing
            # res = sat(|I*a+b|)
            # if current_image is not in 8 bits, we need to rescale
            min_val = self.filter_params.black_level.float
            max_val = self.filter_params.white_level.float

            if min_val != 0 or max_val != 1 or current_image.precision!=8:
                min_val = self.filter_params.black_level.float
                max_val = self.filter_params.white_level.float
                # adjust levels to precision
                precision = current_image.precision
                min_val = min_val*((1 << precision)-1)
                max_val = max_val*((1 << precision)-1)
                if rgb_image.dtype == np.uint32:
                    # Formula a bit complicated, we need to be careful with unsigned processing
                    rgb_image =np.clip(((np.clip(rgb_image, min_val, None) - min_val)*(255/(max_val-min_val)))+0.5,
                                       None, 255).astype(np.uint8)
                else:
                    # to rescale: add min_val and multiply by (max_val-min_val)/255
                    if min_val != 0:
                        rgb_image = cv2.add(rgb_image, (-min_val, -min_val, -min_val, 0))
                    rgb_image = cv2.convertScaleAbs(rgb_image, alpha=255. / float(max_val - min_val), beta=0)

            # # if gamma changed
            # if self.filter_params.gamma.value != self.filter_params.gamma.default_value and work_image.dtype == np.uint8:
            #     gamma_coeff = self.filter_params.gamma.float
            #     # self.gamma_label.setText("Gamma  {}".format(gamma_coeff))
            #     invGamma = 1.0 / gamma_coeff
            #     table = np.array([((i / 255.0) ** invGamma) * 255 for i in np.arange(0, 256)]).astype("uint8")
            #     work_image = cv2.LUT(work_image, table)

        self.print_timing(title='apply_filters()')
        return rgb_image

    def viewer_update(self):
        if BaseWidget is QOpenGLWidget:
            self.paint_image()
            self.repaint()
        else:
            self.update()

    def draw_overlay_separation(self, cropped_image_shape, rect, painter):
        (height, width) = cropped_image_shape[:2]
        im_x = int((self.mouse_x - rect.x())/rect.width()*width)
        im_x = max(0, min(width - 1, im_x))
        # im_y = int((self.mouse_y - rect.y())/rect.height()*height)
        # Set position at the beginning of the pixel
        pos_from_im_x = int(im_x*rect.width()/width + rect.x())
        # pos_from_im_y = int((im_y+0.5)*rect.height()/height+ rect.y())
        pen_width = 2
        color = QtGui.QColor(255, 255, 0 , 128)
        pen = QtGui.QPen()
        pen.setColor(color)
        pen.setWidth(pen_width)
        painter.setPen(pen)
        painter.drawLine(pos_from_im_x, rect.y(), pos_from_im_x, rect.y() + rect.height())

    def draw_cursor(self, cropped_image_shape, crop_xmin, crop_ymin, rect, painter, full=False) -> Optional[Tuple[int, int]]:
        """
        :param cropped_image_shape: dimensions of current crop
        :param crop_xmin: left pixel of current crop
        :param crop_ymin: top pixel of current crop
        :param rect: displayed image area
        :param painter:
        :return:
            tuple: (posx, posy) image pixel position of the cursor, if None cursor is out of image
        """
        # Draw cursor
        if self.display_timing: self.start_timing()
        # get image position
        (height, width) = cropped_image_shape[:2]
        im_x = int((self.mouse_x -rect.x())/rect.width()*width)
        im_y = int((self.mouse_y -rect.y())/rect.height()*height)

        pos_from_im_x = int((im_x+0.5)*rect.width()/width +rect.x())
        pos_from_im_y = int((im_y+0.5)*rect.height()/height+rect.y())

        # ratio = self.screen().devicePixelRatio()
        # print("ratio = {}".format(ratio))
        pos_x = pos_from_im_x  # *ratio
        pos_y = pos_from_im_y  # *ratio
        length_percent = 0.04
        # use percentage of the displayed image dimensions
        length = int(max(self.width(),self.height())*length_percent)
        pen_width = 2 if full else 3
        color = QtGui.QColor(0, 255, 255, 200)
        pen = QtGui.QPen()
        pen.setColor(color)
        pen.setWidth(pen_width)
        painter.setPen(pen)
        if not full:
            painter.drawLine(pos_x-length, pos_y, pos_x+length, pos_y)
            painter.drawLine(pos_x, pos_y-length, pos_x, pos_y+length)
        else:
            painter.drawLine(rect.x(), pos_y, rect.x()+rect.width(), pos_y)
            painter.drawLine(pos_x, rect.y(), pos_x, rect.y()+rect.height())

        # Update text
        if im_x>=0 and im_x<cropped_image_shape[1] and im_y>=0 and im_y<cropped_image_shape[0]:
            # values = cropped_image[im_y, im_x]
            im_x += crop_xmin
            im_y += crop_ymin
            im_pos = (im_x, im_y)
        else:
            im_pos = None
        if self.display_timing: self.print_timing()
        return im_pos

    def get_difference_image(self, verbose=True):

        factor = self.filter_params.imdiff_factor.float
        if self.paint_diff_cache is not None:
            use_cache = self.paint_diff_cache['imid'] == self.image_id and \
                        self.paint_diff_cache['imrefid'] == self.image_ref_id and \
                        self.paint_diff_cache['factor'] == factor
        else:
            use_cache = False

        if not use_cache:
            im1 = self._image.data
            im2 = self._image_ref.data
            # TODO: get factor from parameters ...
            # factor = int(self.diff_color_slider.value())
            print(f'factor = {factor}')
            print(f' im1.dtype {im1.dtype} im2.dtype {im2.dtype}')
            # Fast OpenCV code
            start = get_time()
            # positive diffs in unsigned 8 bits, OpenCV puts negative values to 0
            try:
                if im1.dtype.name == 'uint8' and im2.dtype.name == 'uint8':
                    diff_plus = cv2.subtract(im1, im2)
                    diff_minus = cv2.subtract(im2, im1)
                    res = cv2.addWeighted(diff_plus, factor, diff_minus, -factor, 127)
                    if verbose:
                        print(f" qtImageViewer.difference_image()  took {int((get_time() - start)*1000)} ms")
                        vmin = np.min(res)
                        vmax = np.max(res)
                        print(f"min-max diff = {vmin} - {vmax}")
                        histo,_ = np.histogram(res, bins=int(vmax-vmin+0.5), range=(vmin, vmax))
                        sum = 0
                        for v in range(vmin,vmax):
                            if v!=127:
                                nb = histo[v-vmin]
                                if nb >0:
                                    print(f"{v-127}:{nb} ",end='')
                                    sum += nb
                        print('')
                        print(f'nb pixel diff  {sum}')
                    res = ViewerImage(res,  precision=self._image.precision, 
                                            downscale=self._image.downscale,
                                            channels=self._image.channels)
                    self.paint_diff_cache = {  'imid': self.image_id, 'imrefid': self.image_ref_id, 
                        'factor': self.filter_params.imdiff_factor.float
                    }
                    self.diff_image = res
                else:
                    d = (im1.astype(np.float32)-im2.astype(np.float32))*factor
                    d[d<-127] = -127
                    d[d>128] = 128
                    d = (d+127).astype(np.uint8)*255
                    res = ViewerImage(d,  precision=8, 
                                            downscale=self._image.downscale,
                                            channels=self._image.channels)
                    self.paint_diff_cache = {  'imid': self.image_id, 'imrefid': self.image_ref_id, 
                        'factor': self.filter_params.imdiff_factor.float
                    }
                    self.diff_image = res
            except Exception as e:
                print(f"Error {e}")
                res = (im1!=im2).astype(np.uint8)*255
                res = ViewerImage(res,  precision=8, 
                                        downscale=self._image.downscale,
                                        channels=ImageFormat.CH_Y)
                self.diff_image = res

        return self.diff_image

    def paint_image(self):
        # print(f"paint_image display_timing {self.display_timing}")
        if self.trace_calls: t = trace_method(self.tab)
        self.start_timing()
        time0 = time1 = get_time()

        label_width = self.size().width()
        label_height = self.size().height()

        show_diff = self.show_image_differences and self._image is not self._image_ref and \
                    self._image_ref is not None and self._image.data.shape == self._image_ref.data.shape

        c = self.update_crop()
        # check paint_cache
        if self.paint_cache is not None:
            use_cache = self.paint_cache['imid'] == self.image_id and \
                        np.array_equal(self.paint_cache['crop'],c) and \
                        self.paint_cache['labelw'] == label_width and \
                        self.paint_cache['labelh'] == label_height and \
                        self.paint_cache['filterp'].is_equal(self.filter_params) and \
                        (self.paint_cache['showhist'] == self.show_histogram or not self.show_histogram) and \
                        self.paint_cache['show_diff'] == show_diff and \
                        self.paint_cache['antialiasing'] == self.antialiasing and \
                        not self.show_overlay
        else:
            use_cache = False

        # if show_diff, compute the image difference (put it in cache??)
        if show_diff:
            # Cache does not work well with differences
            use_cache = False
            # don't save the difference
            current_image = self.get_difference_image()
        else:
            current_image = self._image

        precision  = current_image.precision
        downscale  = current_image.downscale
        channels   = current_image.channels

        # TODO: get data based on the display ratio?
        image_data = current_image.data

        # could_use_cache = use_cache
        # if could_use_cache:
        #     print(" Could use cache here ... !!!")
        # use_cache = False

        do_crop = (c[2] - c[0] != 1) or (c[3] - c[1] != 1)
        h, w  = image_data.shape[:2]
        if do_crop:
            crop_xmin = int(np.round(c[0] * w))
            crop_xmax = int(np.round(c[2] * w))
            crop_ymin = int(np.round(c[1] * h))
            crop_ymax = int(np.round(c[3] * h))
            image_data = image_data[crop_ymin:crop_ymax, crop_xmin:crop_xmax]
        else:
            crop_xmin = crop_ymin = 0
            crop_xmax = w
            crop_ymax = h

        cropped_image_shape = image_data.shape
        self.add_time('crop', time1)

        # time1 = get_time()
        image_height, image_width  = image_data.shape[:2]
        ratio_width = float(label_width) / image_width
        ratio_height = float(label_height) / image_height
        ratio = min(ratio_width, ratio_height)
        display_width = int(round(image_width * ratio))
        display_height = int(round(image_height * ratio))

        if self.show_overlay and self._image_ref is not self._image and self._image_ref and \
            self._image.data.shape == self._image_ref.data.shape:
            # to create the overlay rapidly, we will mix the two images based on the current cursor position
            # 1. convert cursor position to image position
            (height, width) = cropped_image_shape[:2]
            # compute rect
            rect = QtCore.QRect(0, 0, display_width, display_height)
            devRect = QtCore.QRect(0, 0, self.evt_width, self.evt_height)
            rect.moveCenter(devRect.center())
            im_x = int((self.mouse_x - rect.x()) / rect.width() * width)
            im_x = max(0,min(width-1, im_x))
            # im_y = int((self.mouse_y - rect.y()) / rect.height() * height)
            # We need to have a copy here .. slow, better option???
            image_data = np.copy(image_data)
            image_data[:, :im_x] = self._image_ref.data[crop_ymin:crop_ymax, crop_xmin:(crop_xmin+im_x)]

        resize_applied = False
        if not use_cache:
            anti_aliasing = ratio < 1
            #self.print_log("ratio is {:0.2f}".format(ratio))
            use_opencv_resize = anti_aliasing
            # enable this as optional?
            # opencv_downscale_interpolation = opencv_fast_interpolation
            opencv_fast_interpolation = cv2.INTER_NEAREST
            if self.antialiasing:
                opencv_downscale_interpolation = cv2.INTER_AREA
            else:
                opencv_downscale_interpolation = cv2.INTER_NEAREST
            # opencv_upscale_interpolation   = cv2.INTER_LINEAR
            opencv_upscale_interpolation   = opencv_fast_interpolation
            # self.print_time('several settings', time1, start_time)

            # self.print_log("use_opencv_resize {} channels {}".format(use_opencv_resize, current_image.channels))
            # if ratio<1 we want anti aliasing and we want to resize as soon as possible to reduce computation time
            if use_opencv_resize and not resize_applied and channels == ImageFormat.CH_RGB:

                prev_shape = image_data.shape
                initial_type = image_data.dtype
                if image_data.dtype != np.uint8:
                    print(f"image_data type {type(image_data)} {image_data.dtype}")
                    image_data = image_data.astype(np.float32)

                # if ratio is >2, start with integer downsize which is much faster
                # we could add this condition opencv_downscale_interpolation==cv2.INTER_AREA
                if ratio<=0.5:
                    if image_data.shape[0]%2!=0 or image_data.shape[1]%2 !=0:
                        # clip image to multiple of 2 dimension
                        image_data = image_data[:2*(image_data.shape[0]//2),:2*(image_data.shape[1]//2)]
                    start_0 = get_time()
                    resized_image = cv2.resize(image_data, (image_width>>1, image_height>>1),
                                            interpolation=opencv_downscale_interpolation)
                    if self.display_timing:
                        print(f' === qtImageViewer: ratio {ratio:0.2f} paint_image() OpenCV resize from '
                            f'{current_image.data.shape} to '
                            f'{resized_image.shape} --> {int((get_time()-start_0)*1000)} ms')
                    image_data = resized_image
                    if ratio<=0.25:
                        if image_data.shape[0]%2!=0 or image_data.shape[1]%2 !=0:
                            # clip image to multiple of 2 dimension
                            image_data = image_data[:2*(image_data.shape[0]//2),:2*(image_data.shape[1]//2)]
                        start_0 = get_time()
                        resized_image = cv2.resize(image_data, (image_width>>2, image_height>>2),
                                                interpolation=opencv_downscale_interpolation)
                        if self.display_timing:
                            print(f' === qtImageViewer: ratio {ratio:0.2f} paint_image() OpenCV resize from '
                                f'{current_image.data.shape} to '
                                f'{resized_image.shape} --> {int((get_time()-start_0)*1000)} ms')
                        image_data = resized_image

                time1 = get_time()
                start_0 = get_time()
                resized_image = cv2.resize(image_data, (display_width, display_height),
                                        interpolation=opencv_downscale_interpolation)
                if self.display_timing:
                    print(f' === qtImageViewer: paint_image() OpenCV resize from {image_data.shape} to '
                        f'{resized_image.shape} --> {int((get_time()-start_0)*1000)} ms')

                image_data = resized_image.astype(initial_type)
                resize_applied = True
                self.add_time('cv2.resize',time1)

            current_image = ViewerImage(image_data,  precision=precision, downscale=downscale, channels=channels)
            if self.show_stats:
                # Output RGB from input
                ch = self._image.channels
                data_shape = current_image.data.shape
                if len(data_shape)==2:
                    print(f"input average {np.average(current_image.data)}")
                if len(data_shape)==3:
                    for c in range(data_shape[2]):
                        print(f"input average ch {c} {np.average(current_image.data[:,:,c])}")
            current_image = self.apply_filters(current_image)

            # Compute the histogram here, with the smallest image!!!
            if self.show_histogram:
                # previous version only python with its modules
                # histograms  = self.compute_histogram    (current_image, show_timings=self.display_timing)
                # new version with bound C++ code and openMP: much faster
                histograms = self.compute_histogram_Cpp(current_image, show_timings=self.display_timing)
            else:
                histograms = None

            # try to resize anyway with opencv since qt resizing seems too slow
            if not resize_applied and BaseWidget is not QOpenGLWidget:
                time1 = get_time()
                start_0 = get_time()
                prev_shape = current_image.shape
                current_image = cv2.resize(current_image, (display_width, display_height),
                                           interpolation=opencv_upscale_interpolation)
                if self.display_timing:
                    print(f' === qtImageViewer: paint_image() OpenCV resize from {prev_shape} to '
                        f'{(display_height, display_width)} --> {int((get_time()-start_0)*1000)} ms')
                    self.add_time('cv2.resize',time1)

            # no need for more resizing
            resize_applied = True

            # Conversion from numpy array to QImage
            # version 1: goes through PIL image
            # version 2: use QImage constructor directly, faster
            # time1 = get_time()

        else:
            resize_applied = True
            current_image = self.paint_cache['current_image']
            histograms = self.paint_cache['histograms']
            # histograms2 = self.paint_cache['histograms2']

        # if could_use_cache:
        #     print(f" ======= current_image equal ? {np.array_equal(self.paint_cache['current_image'],current_image)}")

        if not use_cache and not self.show_overlay:
            # cache_time = get_time()
            fp = ImageFilterParameters()
            fp.copy_from(self.filter_params)
            self.paint_cache = {
                'imid': self.image_id,
                'imrefid': self.image_ref_id,
                'crop': c, 'labelw': label_width, 'labelh': label_height,
                'filterp': fp, 'showhist': self.show_histogram,
                'histograms': histograms, 
                # 'histograms2': histograms2, 
                'current_image': current_image,
                'show_diff' : show_diff,
                'antialiasing': self.antialiasing
                }
            # print(f"create cache data took {int((get_time() - cache_time) * 1000)} ms")

        if not current_image.flags['C_CONTIGUOUS']:
            current_image = np.require(current_image, np.uint8, 'C')
        qimage = QtGui.QImage(current_image.data, current_image.shape[1], current_image.shape[0],
                                    current_image.strides[0], QtGui.QImage.Format_RGB888)
        # self.add_time('QtGui.QPixmap',time1)

        assert resize_applied, "Image resized should be applied at this point"
        # if not resize_applied:
        #     printf("*** We should never get here ***")
        #     time1 = get_time()
        #     if anti_aliasing:
        #         qimage = qimage.scaled(display_width, display_height, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
        #     else:
        #         qimage = qimage.scaled(display_width, display_height, QtCore.Qt.KeepAspectRatio)
        #     self.add_time('qimage.scaled', time1)
        #     resize_applied = True

        if self.save_image_clipboard:
            self.print_log("exporting to clipboard")
            self.clipboard.setImage(qimage, mode=QtGui.QClipboard.Clipboard)

        painter : QtGui.QPainter = QtGui.QPainter()

        painter.begin(self)
        if BaseWidget is QOpenGLWidget:
            painter.setRenderHint(QtGui.QPainter.Antialiasing)

        # TODO: check that this condition is not needed
        if BaseWidget is QOpenGLWidget:
            rect = QtCore.QRect(0,0, display_width, display_height)
        else:
            rect = QtCore.QRect(qimage.rect())
        devRect = QtCore.QRect(0, 0, self.evt_width, self.evt_height)
        rect.moveCenter(devRect.center())

        time1 = get_time()
        if BaseWidget is QOpenGLWidget:
            painter.drawImage(rect, qimage)
        else:
            painter.drawImage(rect.topLeft(), qimage)
        self.add_time('painter.drawImage',time1)

        if self.show_overlay:
            self.draw_overlay_separation(cropped_image_shape, rect, painter)

        # Draw cursor
        im_pos = None
        if self.show_cursor:
            im_pos = self.draw_cursor(cropped_image_shape, 
                                      crop_xmin, 
                                      crop_ymin, 
                                      rect, 
                                      painter, 
                                      full = self.show_intensity_line,
                                      )

        if self.show_intensity_line:
            (height, width) = cropped_image_shape[:2]
            im_y = int((self.mouse_y -rect.y())/rect.height()*height)
            im_y += crop_ymin
            im_shape = self._image.data.shape
            # Horizontal display
            if im_y>=0 and im_y<im_shape[0] and crop_xmin>=0 and crop_xmin+cropped_image_shape[1]<=im_shape[1]:
                line = self._image.data[im_y, crop_xmin:crop_xmin+cropped_image_shape[1]]
                self.display_intensity_line(
                    painter, 
                    rect, 
                    line,
                    channels = self._image.channels,
                    )

        self.display_text(painter, self.display_message(im_pos, ratio*self.devicePixelRatio()))

        # draw histogram
        if self.show_histogram:
            self.display_histogram(histograms, 1,  painter, rect, show_timings=self.display_timing)
            # self.display_histogram(histograms2, 2, painter, rect, show_timings=self.display_timing)

        painter.end()
        self.print_timing()

        if self.display_timing:
            print(f" paint_image took {int((get_time()-time0)*1000)} ms")

    def show(self):
        if BaseWidget==QOpenGLWidget:
            self.update()
        BaseWidget.show(self)

    def paintEvent(self, event):
        # print(f" qtImageViewer.paintEvent() {self.image_name}")
        if self.trace_calls:
            t = trace_method(self.tab)
        # try:
        if self._image is not None:
            self.paint_image()
        # except Exception as e:
        #     print(f"Failed paint_image() {e}")

    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()
        BaseWidget.resizeEvent(self, event)
        self.print_log(f"resize {event.size()}  self {self.width()} {self.height()}")

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

    def mouseMoveEvent(self, event):
        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 event(self, evt):
        if self.event_recorder is not None:
            self.event_recorder.store_event(self, evt)
        return BaseWidget.event(self, evt)

    def keyPressEvent(self, event):
        self.key_press_event(event, wsize=self.size())

    def keyReleaseEvent(self, evt):
        self.print_log(f"evt {evt.type()}")

Ancestors

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

Class variables

var staticMetaObject

Methods

def apply_filters(self, current_image)
Expand source code
def apply_filters(self, current_image):
    self.print_log(f"current_image.data.shape {current_image.data.shape}")
    # return current_image

    self.start_timing(title='apply_filters()')

    # Output RGB from input
    ch = self._image.channels
    if has_cppbind:
        channels = current_image.channels
        black_level = self.filter_params.black_level.float
        white_level = self.filter_params.white_level.float
        g_r_coeff = self.filter_params.g_r.float
        g_b_coeff = self.filter_params.g_b.float
        saturation = self.filter_params.saturation.float
        max_value = ((1<<current_image.precision)-1)
        max_type = 1  # not used
        gamma = self.filter_params.gamma.float  # not used

        rgb_image = np.empty((current_image.data.shape[0], current_image.data.shape[1], 3), dtype=np.uint8)
        time1 = get_time()
        ok = False
        if ch in ImageFormat.CH_RAWFORMATS() or ch in ImageFormat.CH_RGBFORMATS():
            cases = {
                'uint8':  { 'func': qimview_cpp.apply_filters_u8_u8  , 'name': 'apply_filters_u8_u8'},
                'uint16': { 'func': qimview_cpp.apply_filters_u16_u8, 'name': 'apply_filters_u16_u8'},
                'uint32': { 'func': qimview_cpp.apply_filters_u32_u8, 'name': 'apply_filters_u32_u8'},
                'int16': { 'func': qimview_cpp.apply_filters_s16_u8, 'name': 'apply_filters_s16_u8'},
                'int32': { 'func': qimview_cpp.apply_filters_s32_u8, 'name': 'apply_filters_s32_u8'}
            }
            if current_image.data.dtype.name in cases:
                func = cases[current_image.data.dtype.name]['func']
                name = cases[current_image.data.dtype.name]['name']
                self.print_log(f"qimview_cpp.{name}(current_image, rgb_image, channels, "
                      f"black_level={black_level}, white_level={white_level}, "
                      f"g_r_coeff={g_r_coeff}, g_b_coeff={g_b_coeff}, "
                      f"max_value={max_value}, max_type={max_type}, gamma={gamma})")
                ok = func(current_image.data, rgb_image, channels, black_level, white_level, g_r_coeff,
                            g_b_coeff, max_value, max_type, gamma, saturation)
                self.add_time(f'{name}()',time1, force=True, title='apply_filters()')
            else:
                print(f"apply_filters() not available for {current_image.data.dtype} data type !")
        else:
            cases = {
                'uint8': { 'func': qimview_cpp.apply_filters_scalar_u8_u8, 'name': 'apply_filters_scalar_u8_u8'},
                'uint16': { 'func': qimview_cpp.apply_filters_scalar_u16_u8, 'name': 'apply_filters_scalar_u16_u8'},
                'int16': { 'func': qimview_cpp.apply_filters_scalar_s16_u8, 'name': 'apply_filters_scalar_s16_u8'},
                'uint32': { 'func': qimview_cpp.apply_filters_scalar_u32_u8, 'name': 'apply_filters_scalar_u32_u8'},
                'float64': { 'func': qimview_cpp.apply_filters_scalar_f64_u8, 'name': 'apply_filters_scalar_f64_u8'},
            }
            if current_image.data.dtype.name.startswith('float'):
                max_value = 1.0
            if current_image.data.dtype.name in cases:
                func = cases[current_image.data.dtype.name]['func']
                name = cases[current_image.data.dtype.name]['name']
                self.print_log(f"qimview_cpp.{name}(current_image, rgb_image, "
                      f"black_level={black_level}, white_level={white_level}, "
                      f"max_value={max_value}, max_type={max_type}, gamma={gamma})")
                ok = func(current_image.data, rgb_image, black_level, white_level, max_value, max_type, gamma)
                self.add_time(f'{name}()', time1, force=True, title='apply_filters()')
            else:
                print(f"apply_filters_scalar() not available for {current_image.data.dtype} data type !")
        if not ok:
            self.print_log("Failed running wrap_num.apply_filters_u16_u8 ...", force=True)
    else:
        # self.print_log("current channels {}".format(ch))
        if ch in ImageFormat.CH_RAWFORMATS():
            channel_pos = channel_position[current_image.channels]
            self.print_log("Converting to RGB")
            # convert Bayer to RGB
            rgb_image = np.empty((current_image.data.shape[0], current_image.data.shape[1], 3), 
                                    dtype=current_image.data.dtype)
            rgb_image[:, :, 0] = current_image.data[:, :, channel_pos['r']]
            rgb_image[:, :, 1] = (current_image.data[:, :, channel_pos['gr']]+current_image.data[:, :, channel_pos['gb']])/2
            rgb_image[:, :, 2] = current_image.data[:, :, channel_pos['b']]
        else:
            if ch == ImageFormat.CH_Y:
                # Transform to RGB is it a good idea?
                rgb_image = np.empty((current_image.data.shape[0], current_image.data.shape[1], 3), 
                                        dtype=current_image.data.dtype)
                rgb_image[:, :, 0] = current_image.data
                rgb_image[:, :, 1] = current_image.data
                rgb_image[:, :, 2] = current_image.data
            else:
                rgb_image = current_image.data

        # Use cv2.convertScaleAbs(I,a,b) function for fast processing
        # res = sat(|I*a+b|)
        # if current_image is not in 8 bits, we need to rescale
        min_val = self.filter_params.black_level.float
        max_val = self.filter_params.white_level.float

        if min_val != 0 or max_val != 1 or current_image.precision!=8:
            min_val = self.filter_params.black_level.float
            max_val = self.filter_params.white_level.float
            # adjust levels to precision
            precision = current_image.precision
            min_val = min_val*((1 << precision)-1)
            max_val = max_val*((1 << precision)-1)
            if rgb_image.dtype == np.uint32:
                # Formula a bit complicated, we need to be careful with unsigned processing
                rgb_image =np.clip(((np.clip(rgb_image, min_val, None) - min_val)*(255/(max_val-min_val)))+0.5,
                                   None, 255).astype(np.uint8)
            else:
                # to rescale: add min_val and multiply by (max_val-min_val)/255
                if min_val != 0:
                    rgb_image = cv2.add(rgb_image, (-min_val, -min_val, -min_val, 0))
                rgb_image = cv2.convertScaleAbs(rgb_image, alpha=255. / float(max_val - min_val), beta=0)

        # # if gamma changed
        # if self.filter_params.gamma.value != self.filter_params.gamma.default_value and work_image.dtype == np.uint8:
        #     gamma_coeff = self.filter_params.gamma.float
        #     # self.gamma_label.setText("Gamma  {}".format(gamma_coeff))
        #     invGamma = 1.0 / gamma_coeff
        #     table = np.array([((i / 255.0) ** invGamma) * 255 for i in np.arange(0, 256)]).astype("uint8")
        #     work_image = cv2.LUT(work_image, table)

    self.print_timing(title='apply_filters()')
    return rgb_image
def apply_translation(self, crop)

:param crop: :return: the new crop

Expand source code
def apply_translation(self, crop):
    """
    :param crop:
    :return: the new crop
    """
    # Apply translation
    diff_x, diff_y = self.new_translation()
    diff_y = - diff_y
    # print(" new translation {} {}".format(diff_x, diff_y))
    # apply the maximal allowed translation
    tr_x = float(diff_x) / self.width()
    tr_y = float(diff_y) / self.height()
    tr_x = clip_value(tr_x, crop[2]-1, crop[0])
    tr_y = clip_value(tr_y, crop[3]-1, crop[1])
    # normalized position relative to the full image
    crop[0] -= tr_x
    crop[1] -= tr_y
    crop[2] -= tr_x
    crop[3] -= tr_y
def apply_zoom(self, crop)
Expand source code
def apply_zoom(self, crop):
    (height, width) = self._image.data.shape[:2]
    # print(f"height, width = {height, width}")
    # Apply zoom
    coeff = 1.0/self.new_scale(self.mouse_zy, height)
    # zoom from the center of the image
    center = self.zoom_center
    new_crop = center + (crop - center) * coeff

    # print("new crop zoom 1 {}".format(new_crop))

    # allow crop increase based on the available space
    label_width = self.width()
    # print(f"label_width {label_width}")
    label_height = self.height()

    new_width = width * coeff
    new_height = height * coeff

    ratio_width = float(label_width) / new_width
    ratio_height = float(label_height) / new_height

    # print(f" ratio_width {ratio_width} ratio_height {ratio_height}")
    ratio = min(ratio_width, ratio_height)

    if ratio_width<ratio_height:
        # margin to increase height
        margin_pixels = label_height/ratio - new_height
        margin_height = margin_pixels/height
        new_crop[1] -= margin_height/2
        new_crop[3] += margin_height/2
    else:
        # margin to increase width
        margin_pixels = label_width/ratio - new_width
        margin_width = margin_pixels/width
        new_crop[0] -= margin_width/2
        new_crop[2] += margin_width/2
    # print("new crop zoom 2 {}".format(new_crop))

    return new_crop
def check_translation(self)

This method computes the translation really applied based on the current requested translation :return:

Expand source code
def check_translation(self):
    """
    This method computes the translation really applied based on the current requested translation
    :return:
    """
    # Apply zoom
    crop = self.apply_zoom(self.output_crop)

    # Compute the translation that is really applied after applying the constraints
    diff_x, diff_y = self.new_translation()
    diff_y = - diff_y
    # print(" new translation {} {}".format(diff_x, diff_y))
    # apply the maximal allowed translation
    w, h = self.width(), self.height()
    diff_x = clip_value(diff_x, w*(crop[2]-1), w*(crop[0]))
    diff_y = - clip_value(diff_y, h*(crop[3]-1), h*(crop[1]))
    # normalized position relative to the full image
    return diff_x, diff_y
def draw_cursor(self, cropped_image_shape, crop_xmin, crop_ymin, rect, painter, full=False) ‑> Optional[Tuple[int, int]]

:param cropped_image_shape: dimensions of current crop :param crop_xmin: left pixel of current crop :param crop_ymin: top pixel of current crop :param rect: displayed image area :param painter: :return: tuple: (posx, posy) image pixel position of the cursor, if None cursor is out of image

Expand source code
def draw_cursor(self, cropped_image_shape, crop_xmin, crop_ymin, rect, painter, full=False) -> Optional[Tuple[int, int]]:
    """
    :param cropped_image_shape: dimensions of current crop
    :param crop_xmin: left pixel of current crop
    :param crop_ymin: top pixel of current crop
    :param rect: displayed image area
    :param painter:
    :return:
        tuple: (posx, posy) image pixel position of the cursor, if None cursor is out of image
    """
    # Draw cursor
    if self.display_timing: self.start_timing()
    # get image position
    (height, width) = cropped_image_shape[:2]
    im_x = int((self.mouse_x -rect.x())/rect.width()*width)
    im_y = int((self.mouse_y -rect.y())/rect.height()*height)

    pos_from_im_x = int((im_x+0.5)*rect.width()/width +rect.x())
    pos_from_im_y = int((im_y+0.5)*rect.height()/height+rect.y())

    # ratio = self.screen().devicePixelRatio()
    # print("ratio = {}".format(ratio))
    pos_x = pos_from_im_x  # *ratio
    pos_y = pos_from_im_y  # *ratio
    length_percent = 0.04
    # use percentage of the displayed image dimensions
    length = int(max(self.width(),self.height())*length_percent)
    pen_width = 2 if full else 3
    color = QtGui.QColor(0, 255, 255, 200)
    pen = QtGui.QPen()
    pen.setColor(color)
    pen.setWidth(pen_width)
    painter.setPen(pen)
    if not full:
        painter.drawLine(pos_x-length, pos_y, pos_x+length, pos_y)
        painter.drawLine(pos_x, pos_y-length, pos_x, pos_y+length)
    else:
        painter.drawLine(rect.x(), pos_y, rect.x()+rect.width(), pos_y)
        painter.drawLine(pos_x, rect.y(), pos_x, rect.y()+rect.height())

    # Update text
    if im_x>=0 and im_x<cropped_image_shape[1] and im_y>=0 and im_y<cropped_image_shape[0]:
        # values = cropped_image[im_y, im_x]
        im_x += crop_xmin
        im_y += crop_ymin
        im_pos = (im_x, im_y)
    else:
        im_pos = None
    if self.display_timing: self.print_timing()
    return im_pos
def draw_overlay_separation(self, cropped_image_shape, rect, painter)
Expand source code
def draw_overlay_separation(self, cropped_image_shape, rect, painter):
    (height, width) = cropped_image_shape[:2]
    im_x = int((self.mouse_x - rect.x())/rect.width()*width)
    im_x = max(0, min(width - 1, im_x))
    # im_y = int((self.mouse_y - rect.y())/rect.height()*height)
    # Set position at the beginning of the pixel
    pos_from_im_x = int(im_x*rect.width()/width + rect.x())
    # pos_from_im_y = int((im_y+0.5)*rect.height()/height+ rect.y())
    pen_width = 2
    color = QtGui.QColor(255, 255, 0 , 128)
    pen = QtGui.QPen()
    pen.setColor(color)
    pen.setWidth(pen_width)
    painter.setPen(pen)
    painter.drawLine(pos_from_im_x, rect.y(), pos_from_im_x, rect.y() + rect.height())
def event(self, evt)

event(self, event: PySide6.QtCore.QEvent) -> bool

Expand source code
def event(self, evt):
    if self.event_recorder is not None:
        self.event_recorder.store_event(self, evt)
    return BaseWidget.event(self, evt)
def get_difference_image(self, verbose=True)
Expand source code
def get_difference_image(self, verbose=True):

    factor = self.filter_params.imdiff_factor.float
    if self.paint_diff_cache is not None:
        use_cache = self.paint_diff_cache['imid'] == self.image_id and \
                    self.paint_diff_cache['imrefid'] == self.image_ref_id and \
                    self.paint_diff_cache['factor'] == factor
    else:
        use_cache = False

    if not use_cache:
        im1 = self._image.data
        im2 = self._image_ref.data
        # TODO: get factor from parameters ...
        # factor = int(self.diff_color_slider.value())
        print(f'factor = {factor}')
        print(f' im1.dtype {im1.dtype} im2.dtype {im2.dtype}')
        # Fast OpenCV code
        start = get_time()
        # positive diffs in unsigned 8 bits, OpenCV puts negative values to 0
        try:
            if im1.dtype.name == 'uint8' and im2.dtype.name == 'uint8':
                diff_plus = cv2.subtract(im1, im2)
                diff_minus = cv2.subtract(im2, im1)
                res = cv2.addWeighted(diff_plus, factor, diff_minus, -factor, 127)
                if verbose:
                    print(f" qtImageViewer.difference_image()  took {int((get_time() - start)*1000)} ms")
                    vmin = np.min(res)
                    vmax = np.max(res)
                    print(f"min-max diff = {vmin} - {vmax}")
                    histo,_ = np.histogram(res, bins=int(vmax-vmin+0.5), range=(vmin, vmax))
                    sum = 0
                    for v in range(vmin,vmax):
                        if v!=127:
                            nb = histo[v-vmin]
                            if nb >0:
                                print(f"{v-127}:{nb} ",end='')
                                sum += nb
                    print('')
                    print(f'nb pixel diff  {sum}')
                res = ViewerImage(res,  precision=self._image.precision, 
                                        downscale=self._image.downscale,
                                        channels=self._image.channels)
                self.paint_diff_cache = {  'imid': self.image_id, 'imrefid': self.image_ref_id, 
                    'factor': self.filter_params.imdiff_factor.float
                }
                self.diff_image = res
            else:
                d = (im1.astype(np.float32)-im2.astype(np.float32))*factor
                d[d<-127] = -127
                d[d>128] = 128
                d = (d+127).astype(np.uint8)*255
                res = ViewerImage(d,  precision=8, 
                                        downscale=self._image.downscale,
                                        channels=self._image.channels)
                self.paint_diff_cache = {  'imid': self.image_id, 'imrefid': self.image_ref_id, 
                    'factor': self.filter_params.imdiff_factor.float
                }
                self.diff_image = res
        except Exception as e:
            print(f"Error {e}")
            res = (im1!=im2).astype(np.uint8)*255
            res = ViewerImage(res,  precision=8, 
                                    downscale=self._image.downscale,
                                    channels=ImageFormat.CH_Y)
            self.diff_image = res

    return self.diff_image
def keyPressEvent(self, event)

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

Expand source code
def keyPressEvent(self, event):
    self.key_press_event(event, wsize=self.size())
def keyReleaseEvent(self, evt)

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

Expand source code
def keyReleaseEvent(self, evt):
    self.print_log(f"evt {evt.type()}")
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):
    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 paintEvent(self, event)

paintEvent(self, event: PySide6.QtGui.QPaintEvent) -> None

Expand source code
def paintEvent(self, event):
    # print(f" qtImageViewer.paintEvent() {self.image_name}")
    if self.trace_calls:
        t = trace_method(self.tab)
    # try:
    if self._image is not None:
        self.paint_image()
    # except Exception as e:
    #     print(f"Failed paint_image() {e}")
def paint_image(self)
Expand source code
def paint_image(self):
    # print(f"paint_image display_timing {self.display_timing}")
    if self.trace_calls: t = trace_method(self.tab)
    self.start_timing()
    time0 = time1 = get_time()

    label_width = self.size().width()
    label_height = self.size().height()

    show_diff = self.show_image_differences and self._image is not self._image_ref and \
                self._image_ref is not None and self._image.data.shape == self._image_ref.data.shape

    c = self.update_crop()
    # check paint_cache
    if self.paint_cache is not None:
        use_cache = self.paint_cache['imid'] == self.image_id and \
                    np.array_equal(self.paint_cache['crop'],c) and \
                    self.paint_cache['labelw'] == label_width and \
                    self.paint_cache['labelh'] == label_height and \
                    self.paint_cache['filterp'].is_equal(self.filter_params) and \
                    (self.paint_cache['showhist'] == self.show_histogram or not self.show_histogram) and \
                    self.paint_cache['show_diff'] == show_diff and \
                    self.paint_cache['antialiasing'] == self.antialiasing and \
                    not self.show_overlay
    else:
        use_cache = False

    # if show_diff, compute the image difference (put it in cache??)
    if show_diff:
        # Cache does not work well with differences
        use_cache = False
        # don't save the difference
        current_image = self.get_difference_image()
    else:
        current_image = self._image

    precision  = current_image.precision
    downscale  = current_image.downscale
    channels   = current_image.channels

    # TODO: get data based on the display ratio?
    image_data = current_image.data

    # could_use_cache = use_cache
    # if could_use_cache:
    #     print(" Could use cache here ... !!!")
    # use_cache = False

    do_crop = (c[2] - c[0] != 1) or (c[3] - c[1] != 1)
    h, w  = image_data.shape[:2]
    if do_crop:
        crop_xmin = int(np.round(c[0] * w))
        crop_xmax = int(np.round(c[2] * w))
        crop_ymin = int(np.round(c[1] * h))
        crop_ymax = int(np.round(c[3] * h))
        image_data = image_data[crop_ymin:crop_ymax, crop_xmin:crop_xmax]
    else:
        crop_xmin = crop_ymin = 0
        crop_xmax = w
        crop_ymax = h

    cropped_image_shape = image_data.shape
    self.add_time('crop', time1)

    # time1 = get_time()
    image_height, image_width  = image_data.shape[:2]
    ratio_width = float(label_width) / image_width
    ratio_height = float(label_height) / image_height
    ratio = min(ratio_width, ratio_height)
    display_width = int(round(image_width * ratio))
    display_height = int(round(image_height * ratio))

    if self.show_overlay and self._image_ref is not self._image and self._image_ref and \
        self._image.data.shape == self._image_ref.data.shape:
        # to create the overlay rapidly, we will mix the two images based on the current cursor position
        # 1. convert cursor position to image position
        (height, width) = cropped_image_shape[:2]
        # compute rect
        rect = QtCore.QRect(0, 0, display_width, display_height)
        devRect = QtCore.QRect(0, 0, self.evt_width, self.evt_height)
        rect.moveCenter(devRect.center())
        im_x = int((self.mouse_x - rect.x()) / rect.width() * width)
        im_x = max(0,min(width-1, im_x))
        # im_y = int((self.mouse_y - rect.y()) / rect.height() * height)
        # We need to have a copy here .. slow, better option???
        image_data = np.copy(image_data)
        image_data[:, :im_x] = self._image_ref.data[crop_ymin:crop_ymax, crop_xmin:(crop_xmin+im_x)]

    resize_applied = False
    if not use_cache:
        anti_aliasing = ratio < 1
        #self.print_log("ratio is {:0.2f}".format(ratio))
        use_opencv_resize = anti_aliasing
        # enable this as optional?
        # opencv_downscale_interpolation = opencv_fast_interpolation
        opencv_fast_interpolation = cv2.INTER_NEAREST
        if self.antialiasing:
            opencv_downscale_interpolation = cv2.INTER_AREA
        else:
            opencv_downscale_interpolation = cv2.INTER_NEAREST
        # opencv_upscale_interpolation   = cv2.INTER_LINEAR
        opencv_upscale_interpolation   = opencv_fast_interpolation
        # self.print_time('several settings', time1, start_time)

        # self.print_log("use_opencv_resize {} channels {}".format(use_opencv_resize, current_image.channels))
        # if ratio<1 we want anti aliasing and we want to resize as soon as possible to reduce computation time
        if use_opencv_resize and not resize_applied and channels == ImageFormat.CH_RGB:

            prev_shape = image_data.shape
            initial_type = image_data.dtype
            if image_data.dtype != np.uint8:
                print(f"image_data type {type(image_data)} {image_data.dtype}")
                image_data = image_data.astype(np.float32)

            # if ratio is >2, start with integer downsize which is much faster
            # we could add this condition opencv_downscale_interpolation==cv2.INTER_AREA
            if ratio<=0.5:
                if image_data.shape[0]%2!=0 or image_data.shape[1]%2 !=0:
                    # clip image to multiple of 2 dimension
                    image_data = image_data[:2*(image_data.shape[0]//2),:2*(image_data.shape[1]//2)]
                start_0 = get_time()
                resized_image = cv2.resize(image_data, (image_width>>1, image_height>>1),
                                        interpolation=opencv_downscale_interpolation)
                if self.display_timing:
                    print(f' === qtImageViewer: ratio {ratio:0.2f} paint_image() OpenCV resize from '
                        f'{current_image.data.shape} to '
                        f'{resized_image.shape} --> {int((get_time()-start_0)*1000)} ms')
                image_data = resized_image
                if ratio<=0.25:
                    if image_data.shape[0]%2!=0 or image_data.shape[1]%2 !=0:
                        # clip image to multiple of 2 dimension
                        image_data = image_data[:2*(image_data.shape[0]//2),:2*(image_data.shape[1]//2)]
                    start_0 = get_time()
                    resized_image = cv2.resize(image_data, (image_width>>2, image_height>>2),
                                            interpolation=opencv_downscale_interpolation)
                    if self.display_timing:
                        print(f' === qtImageViewer: ratio {ratio:0.2f} paint_image() OpenCV resize from '
                            f'{current_image.data.shape} to '
                            f'{resized_image.shape} --> {int((get_time()-start_0)*1000)} ms')
                    image_data = resized_image

            time1 = get_time()
            start_0 = get_time()
            resized_image = cv2.resize(image_data, (display_width, display_height),
                                    interpolation=opencv_downscale_interpolation)
            if self.display_timing:
                print(f' === qtImageViewer: paint_image() OpenCV resize from {image_data.shape} to '
                    f'{resized_image.shape} --> {int((get_time()-start_0)*1000)} ms')

            image_data = resized_image.astype(initial_type)
            resize_applied = True
            self.add_time('cv2.resize',time1)

        current_image = ViewerImage(image_data,  precision=precision, downscale=downscale, channels=channels)
        if self.show_stats:
            # Output RGB from input
            ch = self._image.channels
            data_shape = current_image.data.shape
            if len(data_shape)==2:
                print(f"input average {np.average(current_image.data)}")
            if len(data_shape)==3:
                for c in range(data_shape[2]):
                    print(f"input average ch {c} {np.average(current_image.data[:,:,c])}")
        current_image = self.apply_filters(current_image)

        # Compute the histogram here, with the smallest image!!!
        if self.show_histogram:
            # previous version only python with its modules
            # histograms  = self.compute_histogram    (current_image, show_timings=self.display_timing)
            # new version with bound C++ code and openMP: much faster
            histograms = self.compute_histogram_Cpp(current_image, show_timings=self.display_timing)
        else:
            histograms = None

        # try to resize anyway with opencv since qt resizing seems too slow
        if not resize_applied and BaseWidget is not QOpenGLWidget:
            time1 = get_time()
            start_0 = get_time()
            prev_shape = current_image.shape
            current_image = cv2.resize(current_image, (display_width, display_height),
                                       interpolation=opencv_upscale_interpolation)
            if self.display_timing:
                print(f' === qtImageViewer: paint_image() OpenCV resize from {prev_shape} to '
                    f'{(display_height, display_width)} --> {int((get_time()-start_0)*1000)} ms')
                self.add_time('cv2.resize',time1)

        # no need for more resizing
        resize_applied = True

        # Conversion from numpy array to QImage
        # version 1: goes through PIL image
        # version 2: use QImage constructor directly, faster
        # time1 = get_time()

    else:
        resize_applied = True
        current_image = self.paint_cache['current_image']
        histograms = self.paint_cache['histograms']
        # histograms2 = self.paint_cache['histograms2']

    # if could_use_cache:
    #     print(f" ======= current_image equal ? {np.array_equal(self.paint_cache['current_image'],current_image)}")

    if not use_cache and not self.show_overlay:
        # cache_time = get_time()
        fp = ImageFilterParameters()
        fp.copy_from(self.filter_params)
        self.paint_cache = {
            'imid': self.image_id,
            'imrefid': self.image_ref_id,
            'crop': c, 'labelw': label_width, 'labelh': label_height,
            'filterp': fp, 'showhist': self.show_histogram,
            'histograms': histograms, 
            # 'histograms2': histograms2, 
            'current_image': current_image,
            'show_diff' : show_diff,
            'antialiasing': self.antialiasing
            }
        # print(f"create cache data took {int((get_time() - cache_time) * 1000)} ms")

    if not current_image.flags['C_CONTIGUOUS']:
        current_image = np.require(current_image, np.uint8, 'C')
    qimage = QtGui.QImage(current_image.data, current_image.shape[1], current_image.shape[0],
                                current_image.strides[0], QtGui.QImage.Format_RGB888)
    # self.add_time('QtGui.QPixmap',time1)

    assert resize_applied, "Image resized should be applied at this point"
    # if not resize_applied:
    #     printf("*** We should never get here ***")
    #     time1 = get_time()
    #     if anti_aliasing:
    #         qimage = qimage.scaled(display_width, display_height, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
    #     else:
    #         qimage = qimage.scaled(display_width, display_height, QtCore.Qt.KeepAspectRatio)
    #     self.add_time('qimage.scaled', time1)
    #     resize_applied = True

    if self.save_image_clipboard:
        self.print_log("exporting to clipboard")
        self.clipboard.setImage(qimage, mode=QtGui.QClipboard.Clipboard)

    painter : QtGui.QPainter = QtGui.QPainter()

    painter.begin(self)
    if BaseWidget is QOpenGLWidget:
        painter.setRenderHint(QtGui.QPainter.Antialiasing)

    # TODO: check that this condition is not needed
    if BaseWidget is QOpenGLWidget:
        rect = QtCore.QRect(0,0, display_width, display_height)
    else:
        rect = QtCore.QRect(qimage.rect())
    devRect = QtCore.QRect(0, 0, self.evt_width, self.evt_height)
    rect.moveCenter(devRect.center())

    time1 = get_time()
    if BaseWidget is QOpenGLWidget:
        painter.drawImage(rect, qimage)
    else:
        painter.drawImage(rect.topLeft(), qimage)
    self.add_time('painter.drawImage',time1)

    if self.show_overlay:
        self.draw_overlay_separation(cropped_image_shape, rect, painter)

    # Draw cursor
    im_pos = None
    if self.show_cursor:
        im_pos = self.draw_cursor(cropped_image_shape, 
                                  crop_xmin, 
                                  crop_ymin, 
                                  rect, 
                                  painter, 
                                  full = self.show_intensity_line,
                                  )

    if self.show_intensity_line:
        (height, width) = cropped_image_shape[:2]
        im_y = int((self.mouse_y -rect.y())/rect.height()*height)
        im_y += crop_ymin
        im_shape = self._image.data.shape
        # Horizontal display
        if im_y>=0 and im_y<im_shape[0] and crop_xmin>=0 and crop_xmin+cropped_image_shape[1]<=im_shape[1]:
            line = self._image.data[im_y, crop_xmin:crop_xmin+cropped_image_shape[1]]
            self.display_intensity_line(
                painter, 
                rect, 
                line,
                channels = self._image.channels,
                )

    self.display_text(painter, self.display_message(im_pos, ratio*self.devicePixelRatio()))

    # draw histogram
    if self.show_histogram:
        self.display_histogram(histograms, 1,  painter, rect, show_timings=self.display_timing)
        # self.display_histogram(histograms2, 2, painter, rect, show_timings=self.display_timing)

    painter.end()
    self.print_timing()

    if self.display_timing:
        print(f" paint_image took {int((get_time()-time0)*1000)} ms")
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()
    BaseWidget.resizeEvent(self, event)
    self.print_log(f"resize {event.size()}  self {self.width()} {self.height()}")
def set_image(self, image)
Expand source code
def set_image(self, image):
    super().set_image(image)
def show(self)

show(self) -> None

Expand source code
def show(self):
    if BaseWidget==QOpenGLWidget:
        self.update()
    BaseWidget.show(self)
def update_crop(self)
Expand source code
def update_crop(self):
    # Apply zoom
    new_crop = self.apply_zoom(self.output_crop)
    # print(f"update_crop {self.output_crop} --> {new_crop}")
    # Apply translation
    self.apply_translation(new_crop)
    new_crop = np.clip(new_crop, 0, 1)
    # print("move new crop {}".format(new_crop))
    # print(f"output_crop {self.output_crop} new crop {new_crop}")
    return new_crop
def update_crop_new(self)
Expand source code
def update_crop_new(self):
    # 1. transform crop to display coordinates
    
    # Apply zoom
    new_crop = self.apply_zoom(self.output_crop)
    # print(f"update_crop {self.output_crop} --> {new_crop}")
    # Apply translation
    self.apply_translation(new_crop)
    new_crop = np.clip(new_crop, 0, 1)
    # print("move new crop {}".format(new_crop))
    # print(f"output_crop {self.output_crop} new crop {new_crop}")
    return new_crop
def viewer_update(self)
Expand source code
def viewer_update(self):
    if BaseWidget is QOpenGLWidget:
        self.paint_image()
        self.repaint()
    else:
        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

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