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_viewerqimview.image_viewers.gl_image_viewer_baseqimview.image_viewers.gl_image_viewer_shadersqimview.image_viewers.image_filter_parametersqimview.image_viewers.image_filter_parameters_guiqimview.image_viewers.image_viewerqimview.image_viewers.multi_viewqimview.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 scaleAncestors
- GLImageViewerBase
- PySide6.QtOpenGLWidgets.QOpenGLWidget
- PySide6.QtWidgets.QWidget
- PySide6.QtCore.QObject
- PySide6.QtGui.QPaintDevice
- Shiboken.Object
- ImageViewer
Class variables
var fragmentShader_RAWvar fragmentShader_RGBvar staticMetaObjectvar 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 resMethods
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
-
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 -
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() -
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)) -
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] -
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") -
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_VIEWERvar OPENGL_VIEWERvar QT_VIEWER