Module qimview.image_viewers.image_viewer
Expand source code
#
#
#
from qimview.image_viewers.image_filter_parameters import ImageFilterParameters
from qimview.utils.utils import get_time
from qimview.utils.qt_imports import QtGui, QtCore, QtWidgets
QtKeys = QtCore.Qt.Key
QtMouse = QtCore.Qt.MouseButton
import cv2
import traceback
import abc
import inspect
import numpy as np
from typing import TYPE_CHECKING, Optional, Tuple
# if TYPE_CHECKING:
from qimview.utils.viewer_image import ViewerImage, ImageFormat
from abc import abstractmethod
try:
import qimview_cpp
except Exception as e:
has_cppbind = False
print("Failed to load qimview_cpp: {}".format(e))
else:
has_cppbind = True
print("Do we have cpp binding ? {}".format(has_cppbind))
# copied from https://stackoverflow.com/questions/17065086/how-to-get-the-caller-class-name-inside-a-function-of-another-class-in-python
def get_class_from_frame(fr):
args, _, _, value_dict = inspect.getargvalues(fr)
# we check the first parameter for the frame function is
# named 'self'
if len(args) and args[0] == 'self':
# in that case, 'self' will be referenced in value_dict
instance = value_dict.get('self', None)
if instance:
# return its class name
try:
# return getattr(instance, '__class__', None)
return getattr(instance, '__class__', None).__name__
except:
return None
# return None otherwise
return None
def get_function_name():
return traceback.extract_stack(None, 2)[0][2]
class trace_method():
def __init__(self, tab):
self.tab = tab
method = traceback.extract_stack(None, 2)[0][2]
print(self.tab[0] + method)
self.tab[0] += ' '
def __del__(self):
self.tab[0] = self.tab[0][:-2]
class ImageViewer:
def __init__(self, parent=None):
self.data = None
self._width = 500
self._height = 500
self.lastPos = None # Last mouse position before mouse click
self.mouse_dx = self.mouse_dy = 0
self.mouse_zx = 0
self.mouse_zy = 0
self.mouse_x = 0
self.mouse_y = 0
self.current_dx = self.current_dy = 0
self.current_scale = 1
self._image : Optional[ViewerImage] = None
self._image_ref : Optional[ViewerImage] = None
self.synchronize_viewer = None
self.tab = ["--"]
self.trace_calls = False
self._image_name = ""
self.active_window = False
self.filter_params = ImageFilterParameters()
self.save_image_clipboard = False
self.clipboard = None
self._display_timing = False
self._verbose = False
self.start_time = dict()
self.timings = dict()
self.replacing_widget = None
self.before_max_parent = None
self.show_histogram : bool = True
self.show_cursor : bool = False
self.show_overlay : bool = False
self.show_stats : bool = False
self.show_image_differences : bool = False
self.show_intensity_line : bool = False
self.antialiasing : bool = True
# We track an image counter, changed by set_image, to help reducing same calculations
self.image_id = -1
self.image_ref_id = -1
# Rectangle in which the histogram is displayed
self._histo_rect : Optional[QtCore.QRect] = None
# Histogram displayed scale
self._histo_scale : int = 1
# Widget dimensions to be defined in child resize event
self.evt_width : int
self.evt_height : int
@property
def display_timing(self):
return self._display_timing
@display_timing.setter
def display_timing(self, v):
self._display_timing = v
@property
def verbose(self):
return self._verbose
@verbose.setter
def verbose(self, v):
self._verbose = v
def set_image(self, image : Optional[ViewerImage]):
is_different = (self._image is None) or (self._image is not image)
if image is not None:
self.print_log('set_image({}): is_different = {}'.format(image.data.shape, is_different))
if is_different:
self._image = image
self.image_id += 1
return is_different
def set_image_ref(self, image_ref : Optional[ViewerImage] = None):
is_different = (self._image_ref is None) or (self._image_ref is not image_ref)
if is_different:
self._image_ref = image_ref
self.image_ref_id += 1
def set_clipboard(self, clipboard, save_image):
self.clipboard = clipboard
self.save_image_clipboard = save_image
def print_log(self, mess, force=False):
if self.verbose or force:
caller_name = inspect.stack()[1][3]
print("{}{}: {}".format(self.tab[0], caller_name, mess))
def start_timing(self, title=None):
if not self.display_timing: return
if title is None:
# it seems that inspect is slow
caller_name = inspect.stack()[1][3]
class_name = get_class_from_frame(inspect.stack()[1][0])
if class_name is not None:
caller_name = "{}.{}".format(class_name, caller_name)
else:
caller_name = title
self.start_time[caller_name] = get_time()
self.timings[caller_name] = ''
def add_time(self, mess, current_start, force=False, title=None):
if not self.display_timing: return
if self.display_timing or force:
if title is None:
caller_name = inspect.stack()[1][3]
class_name = get_class_from_frame(inspect.stack()[1][0])
if class_name is not None:
caller_name = "{}.{}".format(class_name, caller_name)
else:
caller_name = title
if caller_name in self.start_time:
total_start = self.start_time[caller_name]
ctime = get_time()
mess = "{} {:0.1f} ms, total {:0.1f} ms".format(mess, (ctime -current_start)*1000, (ctime-total_start)*1000)
self.timings[caller_name] += "{}{}: {}\n".format(self.tab[0], caller_name, mess)
def print_timing(self, add_total=False, force=False, title=None):
if not self.display_timing: return
if title is None:
caller_name = inspect.stack()[1][3]
class_name = get_class_from_frame(inspect.stack()[1][0])
if class_name is not None:
caller_name = "{}.{}".format(class_name, caller_name)
else:
caller_name = title
if add_total:
self.add_time("total", self.start_time[caller_name], force)
if self.timings[caller_name] != '':
print(self.timings[caller_name])
def set_synchronize(self, viewer):
self.synchronize_viewer = viewer
def synchronize_data(self, other_viewer):
other_viewer.current_scale = self.current_scale
other_viewer.current_dx = self.current_dx
other_viewer.current_dy = self.current_dy
other_viewer.mouse_dx = self.mouse_dx
other_viewer.mouse_dy = self.mouse_dy
other_viewer.mouse_zx = self.mouse_zx
other_viewer.mouse_zy = self.mouse_zy
other_viewer.mouse_x = self.mouse_x
other_viewer.mouse_y = self.mouse_y
other_viewer.show_histogram = self.show_histogram
other_viewer.show_cursor = self.show_cursor
other_viewer.show_intensity_line = self.show_intensity_line
other_viewer._histo_scale = self._histo_scale
def synchronize(self, event_viewer):
"""
This method needs to be overloaded with call to self.synchronize_viewer.synchronize()
:param event_viewer: the viewer that started the synchronization
:return:
"""
if self==event_viewer:
if self.display_timing:
start_time = get_time()
if self.display_timing:
print("[ --- Start sync")
if self.synchronize_viewer is not None and self.synchronize_viewer is not event_viewer:
self.synchronize_data(self.synchronize_viewer)
self.synchronize_viewer.viewer_update()
self.synchronize_viewer.synchronize(event_viewer)
if self==event_viewer:
if self.display_timing:
print(' End sync --- {:0.1f} ms'.format((get_time()-start_time)*1000))
def set_active(self, active=True):
self.active_window = active
def is_active(self):
return self.active_window
@property
def image_name(self) -> str:
return self._image_name
@image_name.setter
def image_name(self, v : str):
self._image_name = v
def get_image(self):
return self._image
def new_scale(self, mouse_zy, height):
return max(1, self.current_scale * (1 + mouse_zy * 5.0 / self._height))
# return max(1, self.current_scale + mouse_zy * 5.0 / height)
def new_translation(self):
dx = self.current_dx + self.mouse_dx/self.current_scale
dy = self.current_dy + self.mouse_dy/self.current_scale
return dx, dy
def check_translation(self):
return self.new_translation()
@abstractmethod
def viewer_update(self):
pass
def mouse_press_event(self, event):
self.lastPos = event.pos()
if event.buttons() & QtMouse.RightButton:
event.accept()
def mouse_move_event(self, event):
self.mouse_x = event.x()
self.mouse_y = event.y()
if self.show_overlay:
self.viewer_update()
if event.buttons() & QtMouse.LeftButton:
self.mouse_dx = event.x() - self.lastPos.x()
self.mouse_dy = - (event.y() - self.lastPos.y())
self.viewer_update()
self.synchronize(self)
event.accept()
else:
if event.buttons() & QtMouse.RightButton:
# right button zoom
self.mouse_zx = event.x() - self.lastPos.x()
self.mouse_zy = - (event.y() - self.lastPos.y())
self.viewer_update()
self.synchronize(self)
event.accept()
else:
modifiers = QtWidgets.QApplication.keyboardModifiers()
if self.show_cursor:
self.viewer_update()
self.synchronize(self)
def mouse_release_event(self, event):
if event.button() & QtMouse.LeftButton:
self.current_dx, self.current_dy = self.check_translation()
self.mouse_dy = 0
self.mouse_dx = 0
event.accept()
if event.button() & QtMouse.RightButton:
if self._image is not None:
self.current_scale = self.new_scale(self.mouse_zy, self._image.data.shape[0])
self.mouse_zy = 0
self.mouse_zx = 0
event.accept()
self.synchronize(self)
def mouse_double_click_event(self, event):
self.print_log("double click ")
# Check if double click is on histogram, if so, toggle histogram size
if self._histo_rect and self._histo_rect.contains(event.x(), event.y()):
# scale loops from 1 to 3
self._histo_scale = (self._histo_scale % 3) + 1
self.viewer_update()
event.accept()
return
# Else set current viewer active
self.set_active()
self.viewer_update()
if self.synchronize_viewer is not None:
v = self.synchronize_viewer
while v != self:
v.set_active(False)
v.viewer_update()
if v.synchronize_viewer is not None:
v = v.synchronize_viewer
def mouse_wheel_event(self,event):
# Zoom by applying a factor to the distances to the sides
if hasattr(event, 'delta'):
delta = event.delta()
else:
delta = event.angleDelta().y()
# print("delta = {}".format(delta))
coeff = delta/5
# coeff = 20 if delta > 0 else -20
if self._image:
self.current_scale = self.new_scale(coeff, self._image.data.shape[0])
self.viewer_update()
self.synchronize(self)
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
"""
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
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:
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 mouseDoubleClickEvent(self, event):
def key_press_event(self, event, wsize):
self.print_log(f"ImageViewer: key_press_event {event.key()}")
if type(event) == QtGui.QKeyEvent:
if event.key() == QtKeys.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/3.-Image-Viewers'>Image Viewer</a>")
mb.exec()
event.accept()
return
if event.key() == QtKeys.Key_F11:
self.toggle_fullscreen(event)
return
# allow to switch between images by pressing Alt+'image position' (Alt+0, Alt+1, etc)
key_list = []
# # select upper left crop
# key_list.append(QtCore.Qt.Key_A)
# if event.key() == QtCore.Qt.Key_A:
# self.current_dx = wsize.width()/4
# self.current_dy = -wsize.height()/4
# self.current_scale = 2
# select upper left crop
key_list.append(QtKeys.Key_B)
if event.key() == QtKeys.Key_B:
self.current_dx = -wsize.width() / 4
self.current_dy = -wsize.height() / 4
self.current_scale = 2
# # select lower left crop
# key_list.append(QtCore.Qt.Key_C)
# if event.key() == QtCore.Qt.Key_C:
# self.current_dx = wsize.width() / 4
# self.current_dy = wsize.height() / 4
# self.current_scale = 2
# # select lower right crop
# key_list.append(QtCore.Qt.Key_D)
# if event.key() == QtCore.Qt.Key_D:
# self.current_dx = -wsize.width() / 4
# self.current_dy = wsize.height() / 4
# self.current_scale = 2
# select full crop
key_list.append(QtKeys.Key_F)
if event.key() == QtKeys.Key_F:
self.output_crop = (0., 0., 1., 1.)
self.current_dx = 0
self.current_dy = 0
self.current_scale = 1
# toggle antialiasing
key_list.append(QtKeys.Key_A)
if event.key() == QtKeys.Key_A:
self.antialiasing = not self.antialiasing
print(f"antialiasing {self.antialiasing}")
# toggle histograph
key_list.append(QtKeys.Key_H)
if event.key() == QtKeys.Key_H:
self.show_histogram = not self.show_histogram
# toggle overlay
key_list.append(QtKeys.Key_O)
if event.key() == QtKeys.Key_O:
self.show_overlay = not self.show_overlay
# C: toggle cursor
key_list.append(QtKeys.Key_C)
if event.key() == QtKeys.Key_C:
self.show_cursor = not self.show_cursor
# D: toggle image differences
key_list.append(QtKeys.Key_D)
if event.key() == QtKeys.Key_D:
self.show_image_differences = not self.show_image_differences
# S: display stats on currrent image
key_list.append(QtKeys.Key_S)
if event.key() == QtKeys.Key_S:
self.show_stats = not self.show_stats
# I: display intensity line
key_list.append(QtKeys.Key_I)
if event.key() == QtKeys.Key_I:
self.show_intensity_line = not self.show_intensity_line
if event.key() in key_list:
self.viewer_update()
self.synchronize(self)
event.accept()
return
event.ignore()
else:
event.ignore()
def display_message(self, im_pos: Optional[Tuple[int,int]], scale = None) -> str:
text : str = self.image_name
if self.show_cursor and im_pos:
text += f"\n {self._image.data.shape} {self._image.data.dtype} prec:{self._image.precision}"
if scale is not None:
text += f"\n x{scale:0.2f}"
im_x, im_y = im_pos
values = self._image.data[im_y, im_x]
text += f"\n pos {im_x:4}, {im_y:4} \n rgb {values}"
if self.show_overlay:
text += "\n ref | im "
if self.show_image_differences:
text += "\n im - ref"
return text
def display_text(self, painter: QtGui.QPainter, text: str) -> None:
self.start_timing()
color = QtGui.QColor(255, 50, 50, 255) if self.is_active() else QtGui.QColor(50, 50, 255, 255)
painter.setPen(color)
font = QtGui.QFont('Decorative', 12)
# font.setBold(True)
painter.setFont(font)
painter.setBackground(QtGui.QColor(250, 250, 250, int(0.75*255)))
painter.setBackgroundMode(QtGui.Qt.BGMode.OpaqueMode)
text_options = \
QtCore.Qt.AlignmentFlag.AlignTop | \
QtCore.Qt.AlignmentFlag.AlignLeft | \
QtCore.Qt.TextFlag.TextWordWrap
area_width = 400
area_height = 200
# boundingRect is interesting but slow to be called at each display
# bounding_rect = painter.boundingRect(0, 0, area_width, area_height, text_options, self.display_message)
margin_x = 8
margin_y = 5
painter.drawText(
margin_x,
# self.evt_height-margin_y-bounding_rect.height(), area_width, area_height,
margin_y, area_width, area_height,
text_options,
text
)
self.print_timing()
def compute_histogram(self, current_image, show_timings=False):
# print(f"compute_histogram show_timings {show_timings}")
if show_timings: h_start = get_time()
# Compute steps based on input image resolution
im_w, im_h = current_image.shape[1], current_image.shape[0]
target_w = 800
target_h = 600
hist_x_step = max(1, int(im_w/target_w+0.5))
hist_y_step = max(1, int(im_h/target_h+0.5))
input_image = current_image
# print(f"current_image {current_image.shape} _image {self._image.shape}")
# input_image = self._image
resized_im = input_image[::hist_y_step, ::hist_x_step, :]
resized_im = input_image
if self.verbose:
print(f"qtImageViewer.compute_histograph() steps are {hist_x_step, hist_y_step} "
f"shape {current_image.shape} --> {resized_im.shape}")
if show_timings: resized_time = get_time()-h_start
calc_hist_time = 0
# First compute all histograms
if show_timings: start_hist = get_time()
hist_all = np.empty((3, 256), dtype=np.float32)
# print(f"{resized_im[::100,::100,:]}")
for channel, im_ch in enumerate(cv2.split(resized_im)):
# hist = cv2.calcHist(resized_im[:, :, channel], [0], None, [256], [0, 256])
hist = cv2.calcHist([im_ch], [0], None, [256], [0, 256])
# print(f"max diff {np.max(np.abs(hist-hist2))}")
hist_all[channel, :] = hist[:, 0]
hist_all = hist_all / np.max(hist_all)
if show_timings: end_hist = get_time()
if show_timings: calc_hist_time += end_hist-start_hist
if show_timings: gauss_start = get_time()
hist_all = cv2.GaussianBlur(hist_all, (7, 1), sigmaX=1.5, sigmaY=0.2)
if show_timings: gauss_time = get_time() - gauss_start
if show_timings:
print(f"compute_histogram took {(get_time()-h_start)*1000:0.1f} msec. ", end="")
print(f"from which calchist:{calc_hist_time*1000:0.1f}, "
f"resizing:{resized_time*1000:0.1f}, "
f"gauss:{gauss_time*1000:0.1f}")
return hist_all
def compute_histogram_Cpp(self, current_image, show_timings=False):
# print(f"compute_histogram show_timings {show_timings}")
if show_timings: h_start = get_time()
# Compute steps based on input image resolution
im_w, im_h = current_image.shape[1], current_image.shape[0]
target_w = 800
target_h = 600
hist_x_step = max(1, int(im_w/target_w+0.5))
hist_y_step = max(1, int(im_h/target_h+0.5))
output_histogram = np.empty((3,256), dtype=np.uint32)
qimview_cpp.compute_histogram(current_image, output_histogram, int(hist_x_step), int(hist_y_step))
if show_timings: t1 = get_time()
hist_all = output_histogram.astype(np.float32)
hist_all = hist_all / np.max(hist_all)
hist_all = cv2.GaussianBlur(hist_all, (7, 1), sigmaX=1.5, sigmaY=0.2)
if show_timings: print(f"qimview_cpp.compute_histogram took {(get_time()-h_start)*1000:0.1f} ms, "
f"{(get_time()-t1)*1000:0.1f} ms")
return hist_all
def display_histogram(self, hist_all, id, painter, im_rect, show_timings=False):
"""
:param painter:
:param rect: displayed image area
:return:
"""
if hist_all is None:
return
histo_timings = show_timings
#if histo_timings:
h_start = get_time()
# Histogram: keep constant width/height ratio
display_ratio : float = 2.0
# print(f'im_rect = {im_rect}')
w, h = self.evt_width, self.evt_height
width : int = int( min(w/4*self._histo_scale, h/3*self._histo_scale))
height : int = int( width/display_ratio)
start_x : int = w - width*id - 10
start_y : int = h - 10
margin : int = 3
if histo_timings: rect_start = get_time()
rect = QtCore.QRect(start_x-margin, start_y-margin-height, width+2*margin, height+2*margin)
self._histo_rect = rect
# painter.fillRect(rect, QtGui.QBrush(QtGui.QColor(255, 255, 255, 128+64)))
# Transparent light grey
painter.fillRect(rect, QtGui.QColor(205, 205, 205, 128+32))
if histo_timings: rect_time = get_time()-rect_start
# print(f"current_image {current_image.shape} _image {self._image.shape}")
# input_image = self._image
path_time = 0
pen = QtGui.QPen()
pen.setWidth(2)
qcolors = {
0: QtGui.QColor(255, 50, 50, 255),
1: QtGui.QColor(50, 255, 50, 255),
2: QtGui.QColor(50, 50, 255, 255)
}
step_x = float(width) / 256
step = 2
x_range = np.array(range(0, 256, step))
x_pos = start_x + x_range*step_x
for channel in range(3):
pen.setColor(qcolors[channel])
painter.setPen(pen)
# painter.setBrush(color)
# print(f"histogram painting 1 took {get_time() - h_start} sec.")
# print(f"histogram painting 2 took {get_time() - h_start} sec.")
if histo_timings: start_path = get_time()
# apply a small Gaussian filtering to histogram curve
path = QtGui.QPainterPath()
y_pos = start_y - hist_all[channel, x_range]*height
# polygon = QtGui.QPolygonF([QtCore.QPointF(x_pos[n], y_pos[n]) for n in range(len(x_range))])
# path.addPolygon(polygon)
path.moveTo(x_pos[0], y_pos[0])
for n in range(1,len(x_range)):
path.lineTo(x_pos[n], y_pos[n])
painter.drawPath(path)
if histo_timings: path_time += get_time()-start_path
if histo_timings:
print(f"display_histogram took {(get_time()-h_start)*1000:0.1f} msec. ", end='')
print(f"from which path:{int(path_time*1000)}, rect:{int(rect_time*1000)}")
def display_intensity_line(self,
painter: QtGui.QPainter,
im_rect: QtCore.QRect,
line: np.ndarray,
channels : ImageFormat,
) -> None:
#if histo_timings:
h_start = get_time()
# print(f'im_rect = {im_rect}')
w, h = self.evt_width, self.evt_height
width : int = im_rect.width()
height : int = int( h/5)
start_x : int = im_rect.x()
margin_y : int = 2
start_y : int = h-margin_y
rect = QtCore.QRect(start_x, start_y-height, width, height)
self._line_rect = rect
painter.fillRect(rect, QtGui.QColor(205, 205, 205, 128+32))
pen = QtGui.QPen()
pen.setWidth(1)
# Adapt for Bayer, Y, etc ...
qcolors = {
'R' : QtGui.QColor(240, 30, 30, 255),
'G' : QtGui.QColor( 30, 240, 30, 255),
'Gr': QtGui.QColor(130, 240, 30, 255),
'Gb': QtGui.QColor( 30, 240, 130, 255),
'B' : QtGui.QColor( 30, 30, 240, 255),
'Y' : QtGui.QColor( 30, 30, 30, 255),
}
colors = {
ImageFormat.CH_RGB : ['R','G','B'],
ImageFormat.CH_BGR : ['B','G','R'],
ImageFormat.CH_RGGB : ['R','Gr','Gb','B'],
ImageFormat.CH_GRBG : ['Gr','R','B','Gb'],
ImageFormat.CH_GBRG : ['Gb','B','R','Gr'],
ImageFormat.CH_BGGR : ['B','Gb','Gr','R'],
}[channels]
assert line.shape[1] == len(colors), f"Error: Mismatch between imageformat and number of channels"
nb_values = line.shape[0]
step_x = float(width) / nb_values
x_range = np.array(range(0, nb_values))
x_pos = start_x + (x_range+0.5)*step_x
max_val = np.max(line)
line = line.astype(np.float32)
in_margin = 2
in_start_y = start_y - in_margin
in_height = height - 2*in_margin
for channel in range(len(colors)):
pen.setColor(qcolors[colors[channel]])
painter.setPen(pen)
# apply a small Gaussian filtering to histogram curve
path = QtGui.QPainterPath()
y_pos = (in_start_y - line[:,channel]*(in_height/max_val)+0.5).astype(np.uint32)
path.moveTo(x_pos[0], y_pos[0])
for n in range(1,len(x_range)):
path.lineTo(x_pos[n], y_pos[n])
painter.drawPath(path)
Functions
def get_class_from_frame(fr)-
Expand source code
def get_class_from_frame(fr): args, _, _, value_dict = inspect.getargvalues(fr) # we check the first parameter for the frame function is # named 'self' if len(args) and args[0] == 'self': # in that case, 'self' will be referenced in value_dict instance = value_dict.get('self', None) if instance: # return its class name try: # return getattr(instance, '__class__', None) return getattr(instance, '__class__', None).__name__ except: return None # return None otherwise return None def get_function_name()-
Expand source code
def get_function_name(): return traceback.extract_stack(None, 2)[0][2]
Classes
class ImageViewer (parent=None)-
Expand source code
class ImageViewer: def __init__(self, parent=None): self.data = None self._width = 500 self._height = 500 self.lastPos = None # Last mouse position before mouse click self.mouse_dx = self.mouse_dy = 0 self.mouse_zx = 0 self.mouse_zy = 0 self.mouse_x = 0 self.mouse_y = 0 self.current_dx = self.current_dy = 0 self.current_scale = 1 self._image : Optional[ViewerImage] = None self._image_ref : Optional[ViewerImage] = None self.synchronize_viewer = None self.tab = ["--"] self.trace_calls = False self._image_name = "" self.active_window = False self.filter_params = ImageFilterParameters() self.save_image_clipboard = False self.clipboard = None self._display_timing = False self._verbose = False self.start_time = dict() self.timings = dict() self.replacing_widget = None self.before_max_parent = None self.show_histogram : bool = True self.show_cursor : bool = False self.show_overlay : bool = False self.show_stats : bool = False self.show_image_differences : bool = False self.show_intensity_line : bool = False self.antialiasing : bool = True # We track an image counter, changed by set_image, to help reducing same calculations self.image_id = -1 self.image_ref_id = -1 # Rectangle in which the histogram is displayed self._histo_rect : Optional[QtCore.QRect] = None # Histogram displayed scale self._histo_scale : int = 1 # Widget dimensions to be defined in child resize event self.evt_width : int self.evt_height : int @property def display_timing(self): return self._display_timing @display_timing.setter def display_timing(self, v): self._display_timing = v @property def verbose(self): return self._verbose @verbose.setter def verbose(self, v): self._verbose = v def set_image(self, image : Optional[ViewerImage]): is_different = (self._image is None) or (self._image is not image) if image is not None: self.print_log('set_image({}): is_different = {}'.format(image.data.shape, is_different)) if is_different: self._image = image self.image_id += 1 return is_different def set_image_ref(self, image_ref : Optional[ViewerImage] = None): is_different = (self._image_ref is None) or (self._image_ref is not image_ref) if is_different: self._image_ref = image_ref self.image_ref_id += 1 def set_clipboard(self, clipboard, save_image): self.clipboard = clipboard self.save_image_clipboard = save_image def print_log(self, mess, force=False): if self.verbose or force: caller_name = inspect.stack()[1][3] print("{}{}: {}".format(self.tab[0], caller_name, mess)) def start_timing(self, title=None): if not self.display_timing: return if title is None: # it seems that inspect is slow caller_name = inspect.stack()[1][3] class_name = get_class_from_frame(inspect.stack()[1][0]) if class_name is not None: caller_name = "{}.{}".format(class_name, caller_name) else: caller_name = title self.start_time[caller_name] = get_time() self.timings[caller_name] = '' def add_time(self, mess, current_start, force=False, title=None): if not self.display_timing: return if self.display_timing or force: if title is None: caller_name = inspect.stack()[1][3] class_name = get_class_from_frame(inspect.stack()[1][0]) if class_name is not None: caller_name = "{}.{}".format(class_name, caller_name) else: caller_name = title if caller_name in self.start_time: total_start = self.start_time[caller_name] ctime = get_time() mess = "{} {:0.1f} ms, total {:0.1f} ms".format(mess, (ctime -current_start)*1000, (ctime-total_start)*1000) self.timings[caller_name] += "{}{}: {}\n".format(self.tab[0], caller_name, mess) def print_timing(self, add_total=False, force=False, title=None): if not self.display_timing: return if title is None: caller_name = inspect.stack()[1][3] class_name = get_class_from_frame(inspect.stack()[1][0]) if class_name is not None: caller_name = "{}.{}".format(class_name, caller_name) else: caller_name = title if add_total: self.add_time("total", self.start_time[caller_name], force) if self.timings[caller_name] != '': print(self.timings[caller_name]) def set_synchronize(self, viewer): self.synchronize_viewer = viewer def synchronize_data(self, other_viewer): other_viewer.current_scale = self.current_scale other_viewer.current_dx = self.current_dx other_viewer.current_dy = self.current_dy other_viewer.mouse_dx = self.mouse_dx other_viewer.mouse_dy = self.mouse_dy other_viewer.mouse_zx = self.mouse_zx other_viewer.mouse_zy = self.mouse_zy other_viewer.mouse_x = self.mouse_x other_viewer.mouse_y = self.mouse_y other_viewer.show_histogram = self.show_histogram other_viewer.show_cursor = self.show_cursor other_viewer.show_intensity_line = self.show_intensity_line other_viewer._histo_scale = self._histo_scale def synchronize(self, event_viewer): """ This method needs to be overloaded with call to self.synchronize_viewer.synchronize() :param event_viewer: the viewer that started the synchronization :return: """ if self==event_viewer: if self.display_timing: start_time = get_time() if self.display_timing: print("[ --- Start sync") if self.synchronize_viewer is not None and self.synchronize_viewer is not event_viewer: self.synchronize_data(self.synchronize_viewer) self.synchronize_viewer.viewer_update() self.synchronize_viewer.synchronize(event_viewer) if self==event_viewer: if self.display_timing: print(' End sync --- {:0.1f} ms'.format((get_time()-start_time)*1000)) def set_active(self, active=True): self.active_window = active def is_active(self): return self.active_window @property def image_name(self) -> str: return self._image_name @image_name.setter def image_name(self, v : str): self._image_name = v def get_image(self): return self._image def new_scale(self, mouse_zy, height): return max(1, self.current_scale * (1 + mouse_zy * 5.0 / self._height)) # return max(1, self.current_scale + mouse_zy * 5.0 / height) def new_translation(self): dx = self.current_dx + self.mouse_dx/self.current_scale dy = self.current_dy + self.mouse_dy/self.current_scale return dx, dy def check_translation(self): return self.new_translation() @abstractmethod def viewer_update(self): pass def mouse_press_event(self, event): self.lastPos = event.pos() if event.buttons() & QtMouse.RightButton: event.accept() def mouse_move_event(self, event): self.mouse_x = event.x() self.mouse_y = event.y() if self.show_overlay: self.viewer_update() if event.buttons() & QtMouse.LeftButton: self.mouse_dx = event.x() - self.lastPos.x() self.mouse_dy = - (event.y() - self.lastPos.y()) self.viewer_update() self.synchronize(self) event.accept() else: if event.buttons() & QtMouse.RightButton: # right button zoom self.mouse_zx = event.x() - self.lastPos.x() self.mouse_zy = - (event.y() - self.lastPos.y()) self.viewer_update() self.synchronize(self) event.accept() else: modifiers = QtWidgets.QApplication.keyboardModifiers() if self.show_cursor: self.viewer_update() self.synchronize(self) def mouse_release_event(self, event): if event.button() & QtMouse.LeftButton: self.current_dx, self.current_dy = self.check_translation() self.mouse_dy = 0 self.mouse_dx = 0 event.accept() if event.button() & QtMouse.RightButton: if self._image is not None: self.current_scale = self.new_scale(self.mouse_zy, self._image.data.shape[0]) self.mouse_zy = 0 self.mouse_zx = 0 event.accept() self.synchronize(self) def mouse_double_click_event(self, event): self.print_log("double click ") # Check if double click is on histogram, if so, toggle histogram size if self._histo_rect and self._histo_rect.contains(event.x(), event.y()): # scale loops from 1 to 3 self._histo_scale = (self._histo_scale % 3) + 1 self.viewer_update() event.accept() return # Else set current viewer active self.set_active() self.viewer_update() if self.synchronize_viewer is not None: v = self.synchronize_viewer while v != self: v.set_active(False) v.viewer_update() if v.synchronize_viewer is not None: v = v.synchronize_viewer def mouse_wheel_event(self,event): # Zoom by applying a factor to the distances to the sides if hasattr(event, 'delta'): delta = event.delta() else: delta = event.angleDelta().y() # print("delta = {}".format(delta)) coeff = delta/5 # coeff = 20 if delta > 0 else -20 if self._image: self.current_scale = self.new_scale(coeff, self._image.data.shape[0]) self.viewer_update() self.synchronize(self) 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 """ 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 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: 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 mouseDoubleClickEvent(self, event): def key_press_event(self, event, wsize): self.print_log(f"ImageViewer: key_press_event {event.key()}") if type(event) == QtGui.QKeyEvent: if event.key() == QtKeys.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/3.-Image-Viewers'>Image Viewer</a>") mb.exec() event.accept() return if event.key() == QtKeys.Key_F11: self.toggle_fullscreen(event) return # allow to switch between images by pressing Alt+'image position' (Alt+0, Alt+1, etc) key_list = [] # # select upper left crop # key_list.append(QtCore.Qt.Key_A) # if event.key() == QtCore.Qt.Key_A: # self.current_dx = wsize.width()/4 # self.current_dy = -wsize.height()/4 # self.current_scale = 2 # select upper left crop key_list.append(QtKeys.Key_B) if event.key() == QtKeys.Key_B: self.current_dx = -wsize.width() / 4 self.current_dy = -wsize.height() / 4 self.current_scale = 2 # # select lower left crop # key_list.append(QtCore.Qt.Key_C) # if event.key() == QtCore.Qt.Key_C: # self.current_dx = wsize.width() / 4 # self.current_dy = wsize.height() / 4 # self.current_scale = 2 # # select lower right crop # key_list.append(QtCore.Qt.Key_D) # if event.key() == QtCore.Qt.Key_D: # self.current_dx = -wsize.width() / 4 # self.current_dy = wsize.height() / 4 # self.current_scale = 2 # select full crop key_list.append(QtKeys.Key_F) if event.key() == QtKeys.Key_F: self.output_crop = (0., 0., 1., 1.) self.current_dx = 0 self.current_dy = 0 self.current_scale = 1 # toggle antialiasing key_list.append(QtKeys.Key_A) if event.key() == QtKeys.Key_A: self.antialiasing = not self.antialiasing print(f"antialiasing {self.antialiasing}") # toggle histograph key_list.append(QtKeys.Key_H) if event.key() == QtKeys.Key_H: self.show_histogram = not self.show_histogram # toggle overlay key_list.append(QtKeys.Key_O) if event.key() == QtKeys.Key_O: self.show_overlay = not self.show_overlay # C: toggle cursor key_list.append(QtKeys.Key_C) if event.key() == QtKeys.Key_C: self.show_cursor = not self.show_cursor # D: toggle image differences key_list.append(QtKeys.Key_D) if event.key() == QtKeys.Key_D: self.show_image_differences = not self.show_image_differences # S: display stats on currrent image key_list.append(QtKeys.Key_S) if event.key() == QtKeys.Key_S: self.show_stats = not self.show_stats # I: display intensity line key_list.append(QtKeys.Key_I) if event.key() == QtKeys.Key_I: self.show_intensity_line = not self.show_intensity_line if event.key() in key_list: self.viewer_update() self.synchronize(self) event.accept() return event.ignore() else: event.ignore() def display_message(self, im_pos: Optional[Tuple[int,int]], scale = None) -> str: text : str = self.image_name if self.show_cursor and im_pos: text += f"\n {self._image.data.shape} {self._image.data.dtype} prec:{self._image.precision}" if scale is not None: text += f"\n x{scale:0.2f}" im_x, im_y = im_pos values = self._image.data[im_y, im_x] text += f"\n pos {im_x:4}, {im_y:4} \n rgb {values}" if self.show_overlay: text += "\n ref | im " if self.show_image_differences: text += "\n im - ref" return text def display_text(self, painter: QtGui.QPainter, text: str) -> None: self.start_timing() color = QtGui.QColor(255, 50, 50, 255) if self.is_active() else QtGui.QColor(50, 50, 255, 255) painter.setPen(color) font = QtGui.QFont('Decorative', 12) # font.setBold(True) painter.setFont(font) painter.setBackground(QtGui.QColor(250, 250, 250, int(0.75*255))) painter.setBackgroundMode(QtGui.Qt.BGMode.OpaqueMode) text_options = \ QtCore.Qt.AlignmentFlag.AlignTop | \ QtCore.Qt.AlignmentFlag.AlignLeft | \ QtCore.Qt.TextFlag.TextWordWrap area_width = 400 area_height = 200 # boundingRect is interesting but slow to be called at each display # bounding_rect = painter.boundingRect(0, 0, area_width, area_height, text_options, self.display_message) margin_x = 8 margin_y = 5 painter.drawText( margin_x, # self.evt_height-margin_y-bounding_rect.height(), area_width, area_height, margin_y, area_width, area_height, text_options, text ) self.print_timing() def compute_histogram(self, current_image, show_timings=False): # print(f"compute_histogram show_timings {show_timings}") if show_timings: h_start = get_time() # Compute steps based on input image resolution im_w, im_h = current_image.shape[1], current_image.shape[0] target_w = 800 target_h = 600 hist_x_step = max(1, int(im_w/target_w+0.5)) hist_y_step = max(1, int(im_h/target_h+0.5)) input_image = current_image # print(f"current_image {current_image.shape} _image {self._image.shape}") # input_image = self._image resized_im = input_image[::hist_y_step, ::hist_x_step, :] resized_im = input_image if self.verbose: print(f"qtImageViewer.compute_histograph() steps are {hist_x_step, hist_y_step} " f"shape {current_image.shape} --> {resized_im.shape}") if show_timings: resized_time = get_time()-h_start calc_hist_time = 0 # First compute all histograms if show_timings: start_hist = get_time() hist_all = np.empty((3, 256), dtype=np.float32) # print(f"{resized_im[::100,::100,:]}") for channel, im_ch in enumerate(cv2.split(resized_im)): # hist = cv2.calcHist(resized_im[:, :, channel], [0], None, [256], [0, 256]) hist = cv2.calcHist([im_ch], [0], None, [256], [0, 256]) # print(f"max diff {np.max(np.abs(hist-hist2))}") hist_all[channel, :] = hist[:, 0] hist_all = hist_all / np.max(hist_all) if show_timings: end_hist = get_time() if show_timings: calc_hist_time += end_hist-start_hist if show_timings: gauss_start = get_time() hist_all = cv2.GaussianBlur(hist_all, (7, 1), sigmaX=1.5, sigmaY=0.2) if show_timings: gauss_time = get_time() - gauss_start if show_timings: print(f"compute_histogram took {(get_time()-h_start)*1000:0.1f} msec. ", end="") print(f"from which calchist:{calc_hist_time*1000:0.1f}, " f"resizing:{resized_time*1000:0.1f}, " f"gauss:{gauss_time*1000:0.1f}") return hist_all def compute_histogram_Cpp(self, current_image, show_timings=False): # print(f"compute_histogram show_timings {show_timings}") if show_timings: h_start = get_time() # Compute steps based on input image resolution im_w, im_h = current_image.shape[1], current_image.shape[0] target_w = 800 target_h = 600 hist_x_step = max(1, int(im_w/target_w+0.5)) hist_y_step = max(1, int(im_h/target_h+0.5)) output_histogram = np.empty((3,256), dtype=np.uint32) qimview_cpp.compute_histogram(current_image, output_histogram, int(hist_x_step), int(hist_y_step)) if show_timings: t1 = get_time() hist_all = output_histogram.astype(np.float32) hist_all = hist_all / np.max(hist_all) hist_all = cv2.GaussianBlur(hist_all, (7, 1), sigmaX=1.5, sigmaY=0.2) if show_timings: print(f"qimview_cpp.compute_histogram took {(get_time()-h_start)*1000:0.1f} ms, " f"{(get_time()-t1)*1000:0.1f} ms") return hist_all def display_histogram(self, hist_all, id, painter, im_rect, show_timings=False): """ :param painter: :param rect: displayed image area :return: """ if hist_all is None: return histo_timings = show_timings #if histo_timings: h_start = get_time() # Histogram: keep constant width/height ratio display_ratio : float = 2.0 # print(f'im_rect = {im_rect}') w, h = self.evt_width, self.evt_height width : int = int( min(w/4*self._histo_scale, h/3*self._histo_scale)) height : int = int( width/display_ratio) start_x : int = w - width*id - 10 start_y : int = h - 10 margin : int = 3 if histo_timings: rect_start = get_time() rect = QtCore.QRect(start_x-margin, start_y-margin-height, width+2*margin, height+2*margin) self._histo_rect = rect # painter.fillRect(rect, QtGui.QBrush(QtGui.QColor(255, 255, 255, 128+64))) # Transparent light grey painter.fillRect(rect, QtGui.QColor(205, 205, 205, 128+32)) if histo_timings: rect_time = get_time()-rect_start # print(f"current_image {current_image.shape} _image {self._image.shape}") # input_image = self._image path_time = 0 pen = QtGui.QPen() pen.setWidth(2) qcolors = { 0: QtGui.QColor(255, 50, 50, 255), 1: QtGui.QColor(50, 255, 50, 255), 2: QtGui.QColor(50, 50, 255, 255) } step_x = float(width) / 256 step = 2 x_range = np.array(range(0, 256, step)) x_pos = start_x + x_range*step_x for channel in range(3): pen.setColor(qcolors[channel]) painter.setPen(pen) # painter.setBrush(color) # print(f"histogram painting 1 took {get_time() - h_start} sec.") # print(f"histogram painting 2 took {get_time() - h_start} sec.") if histo_timings: start_path = get_time() # apply a small Gaussian filtering to histogram curve path = QtGui.QPainterPath() y_pos = start_y - hist_all[channel, x_range]*height # polygon = QtGui.QPolygonF([QtCore.QPointF(x_pos[n], y_pos[n]) for n in range(len(x_range))]) # path.addPolygon(polygon) path.moveTo(x_pos[0], y_pos[0]) for n in range(1,len(x_range)): path.lineTo(x_pos[n], y_pos[n]) painter.drawPath(path) if histo_timings: path_time += get_time()-start_path if histo_timings: print(f"display_histogram took {(get_time()-h_start)*1000:0.1f} msec. ", end='') print(f"from which path:{int(path_time*1000)}, rect:{int(rect_time*1000)}") def display_intensity_line(self, painter: QtGui.QPainter, im_rect: QtCore.QRect, line: np.ndarray, channels : ImageFormat, ) -> None: #if histo_timings: h_start = get_time() # print(f'im_rect = {im_rect}') w, h = self.evt_width, self.evt_height width : int = im_rect.width() height : int = int( h/5) start_x : int = im_rect.x() margin_y : int = 2 start_y : int = h-margin_y rect = QtCore.QRect(start_x, start_y-height, width, height) self._line_rect = rect painter.fillRect(rect, QtGui.QColor(205, 205, 205, 128+32)) pen = QtGui.QPen() pen.setWidth(1) # Adapt for Bayer, Y, etc ... qcolors = { 'R' : QtGui.QColor(240, 30, 30, 255), 'G' : QtGui.QColor( 30, 240, 30, 255), 'Gr': QtGui.QColor(130, 240, 30, 255), 'Gb': QtGui.QColor( 30, 240, 130, 255), 'B' : QtGui.QColor( 30, 30, 240, 255), 'Y' : QtGui.QColor( 30, 30, 30, 255), } colors = { ImageFormat.CH_RGB : ['R','G','B'], ImageFormat.CH_BGR : ['B','G','R'], ImageFormat.CH_RGGB : ['R','Gr','Gb','B'], ImageFormat.CH_GRBG : ['Gr','R','B','Gb'], ImageFormat.CH_GBRG : ['Gb','B','R','Gr'], ImageFormat.CH_BGGR : ['B','Gb','Gr','R'], }[channels] assert line.shape[1] == len(colors), f"Error: Mismatch between imageformat and number of channels" nb_values = line.shape[0] step_x = float(width) / nb_values x_range = np.array(range(0, nb_values)) x_pos = start_x + (x_range+0.5)*step_x max_val = np.max(line) line = line.astype(np.float32) in_margin = 2 in_start_y = start_y - in_margin in_height = height - 2*in_margin for channel in range(len(colors)): pen.setColor(qcolors[colors[channel]]) painter.setPen(pen) # apply a small Gaussian filtering to histogram curve path = QtGui.QPainterPath() y_pos = (in_start_y - line[:,channel]*(in_height/max_val)+0.5).astype(np.uint32) path.moveTo(x_pos[0], y_pos[0]) for n in range(1,len(x_range)): path.lineTo(x_pos[n], y_pos[n]) painter.drawPath(path)Subclasses
Instance variables
var display_timing-
Expand source code
@property def display_timing(self): return self._display_timing var image_name : str-
Expand source code
@property def image_name(self) -> str: return self._image_name var verbose-
Expand source code
@property def verbose(self): return self._verbose
Methods
def add_time(self, mess, current_start, force=False, title=None)-
Expand source code
def add_time(self, mess, current_start, force=False, title=None): if not self.display_timing: return if self.display_timing or force: if title is None: caller_name = inspect.stack()[1][3] class_name = get_class_from_frame(inspect.stack()[1][0]) if class_name is not None: caller_name = "{}.{}".format(class_name, caller_name) else: caller_name = title if caller_name in self.start_time: total_start = self.start_time[caller_name] ctime = get_time() mess = "{} {:0.1f} ms, total {:0.1f} ms".format(mess, (ctime -current_start)*1000, (ctime-total_start)*1000) self.timings[caller_name] += "{}{}: {}\n".format(self.tab[0], caller_name, mess) def check_translation(self)-
Expand source code
def check_translation(self): return self.new_translation() def compute_histogram(self, current_image, show_timings=False)-
Expand source code
def compute_histogram(self, current_image, show_timings=False): # print(f"compute_histogram show_timings {show_timings}") if show_timings: h_start = get_time() # Compute steps based on input image resolution im_w, im_h = current_image.shape[1], current_image.shape[0] target_w = 800 target_h = 600 hist_x_step = max(1, int(im_w/target_w+0.5)) hist_y_step = max(1, int(im_h/target_h+0.5)) input_image = current_image # print(f"current_image {current_image.shape} _image {self._image.shape}") # input_image = self._image resized_im = input_image[::hist_y_step, ::hist_x_step, :] resized_im = input_image if self.verbose: print(f"qtImageViewer.compute_histograph() steps are {hist_x_step, hist_y_step} " f"shape {current_image.shape} --> {resized_im.shape}") if show_timings: resized_time = get_time()-h_start calc_hist_time = 0 # First compute all histograms if show_timings: start_hist = get_time() hist_all = np.empty((3, 256), dtype=np.float32) # print(f"{resized_im[::100,::100,:]}") for channel, im_ch in enumerate(cv2.split(resized_im)): # hist = cv2.calcHist(resized_im[:, :, channel], [0], None, [256], [0, 256]) hist = cv2.calcHist([im_ch], [0], None, [256], [0, 256]) # print(f"max diff {np.max(np.abs(hist-hist2))}") hist_all[channel, :] = hist[:, 0] hist_all = hist_all / np.max(hist_all) if show_timings: end_hist = get_time() if show_timings: calc_hist_time += end_hist-start_hist if show_timings: gauss_start = get_time() hist_all = cv2.GaussianBlur(hist_all, (7, 1), sigmaX=1.5, sigmaY=0.2) if show_timings: gauss_time = get_time() - gauss_start if show_timings: print(f"compute_histogram took {(get_time()-h_start)*1000:0.1f} msec. ", end="") print(f"from which calchist:{calc_hist_time*1000:0.1f}, " f"resizing:{resized_time*1000:0.1f}, " f"gauss:{gauss_time*1000:0.1f}") return hist_all def compute_histogram_Cpp(self, current_image, show_timings=False)-
Expand source code
def compute_histogram_Cpp(self, current_image, show_timings=False): # print(f"compute_histogram show_timings {show_timings}") if show_timings: h_start = get_time() # Compute steps based on input image resolution im_w, im_h = current_image.shape[1], current_image.shape[0] target_w = 800 target_h = 600 hist_x_step = max(1, int(im_w/target_w+0.5)) hist_y_step = max(1, int(im_h/target_h+0.5)) output_histogram = np.empty((3,256), dtype=np.uint32) qimview_cpp.compute_histogram(current_image, output_histogram, int(hist_x_step), int(hist_y_step)) if show_timings: t1 = get_time() hist_all = output_histogram.astype(np.float32) hist_all = hist_all / np.max(hist_all) hist_all = cv2.GaussianBlur(hist_all, (7, 1), sigmaX=1.5, sigmaY=0.2) if show_timings: print(f"qimview_cpp.compute_histogram took {(get_time()-h_start)*1000:0.1f} ms, " f"{(get_time()-t1)*1000:0.1f} ms") return hist_all def display_histogram(self, hist_all, id, painter, im_rect, show_timings=False)-
:param painter: :param rect: displayed image area :return:
Expand source code
def display_histogram(self, hist_all, id, painter, im_rect, show_timings=False): """ :param painter: :param rect: displayed image area :return: """ if hist_all is None: return histo_timings = show_timings #if histo_timings: h_start = get_time() # Histogram: keep constant width/height ratio display_ratio : float = 2.0 # print(f'im_rect = {im_rect}') w, h = self.evt_width, self.evt_height width : int = int( min(w/4*self._histo_scale, h/3*self._histo_scale)) height : int = int( width/display_ratio) start_x : int = w - width*id - 10 start_y : int = h - 10 margin : int = 3 if histo_timings: rect_start = get_time() rect = QtCore.QRect(start_x-margin, start_y-margin-height, width+2*margin, height+2*margin) self._histo_rect = rect # painter.fillRect(rect, QtGui.QBrush(QtGui.QColor(255, 255, 255, 128+64))) # Transparent light grey painter.fillRect(rect, QtGui.QColor(205, 205, 205, 128+32)) if histo_timings: rect_time = get_time()-rect_start # print(f"current_image {current_image.shape} _image {self._image.shape}") # input_image = self._image path_time = 0 pen = QtGui.QPen() pen.setWidth(2) qcolors = { 0: QtGui.QColor(255, 50, 50, 255), 1: QtGui.QColor(50, 255, 50, 255), 2: QtGui.QColor(50, 50, 255, 255) } step_x = float(width) / 256 step = 2 x_range = np.array(range(0, 256, step)) x_pos = start_x + x_range*step_x for channel in range(3): pen.setColor(qcolors[channel]) painter.setPen(pen) # painter.setBrush(color) # print(f"histogram painting 1 took {get_time() - h_start} sec.") # print(f"histogram painting 2 took {get_time() - h_start} sec.") if histo_timings: start_path = get_time() # apply a small Gaussian filtering to histogram curve path = QtGui.QPainterPath() y_pos = start_y - hist_all[channel, x_range]*height # polygon = QtGui.QPolygonF([QtCore.QPointF(x_pos[n], y_pos[n]) for n in range(len(x_range))]) # path.addPolygon(polygon) path.moveTo(x_pos[0], y_pos[0]) for n in range(1,len(x_range)): path.lineTo(x_pos[n], y_pos[n]) painter.drawPath(path) if histo_timings: path_time += get_time()-start_path if histo_timings: print(f"display_histogram took {(get_time()-h_start)*1000:0.1f} msec. ", end='') print(f"from which path:{int(path_time*1000)}, rect:{int(rect_time*1000)}") def display_intensity_line(self, painter: PySide6.QtGui.QPainter, im_rect: PySide6.QtCore.QRect, line: numpy.ndarray, channels: ImageFormat) ‑> None-
Expand source code
def display_intensity_line(self, painter: QtGui.QPainter, im_rect: QtCore.QRect, line: np.ndarray, channels : ImageFormat, ) -> None: #if histo_timings: h_start = get_time() # print(f'im_rect = {im_rect}') w, h = self.evt_width, self.evt_height width : int = im_rect.width() height : int = int( h/5) start_x : int = im_rect.x() margin_y : int = 2 start_y : int = h-margin_y rect = QtCore.QRect(start_x, start_y-height, width, height) self._line_rect = rect painter.fillRect(rect, QtGui.QColor(205, 205, 205, 128+32)) pen = QtGui.QPen() pen.setWidth(1) # Adapt for Bayer, Y, etc ... qcolors = { 'R' : QtGui.QColor(240, 30, 30, 255), 'G' : QtGui.QColor( 30, 240, 30, 255), 'Gr': QtGui.QColor(130, 240, 30, 255), 'Gb': QtGui.QColor( 30, 240, 130, 255), 'B' : QtGui.QColor( 30, 30, 240, 255), 'Y' : QtGui.QColor( 30, 30, 30, 255), } colors = { ImageFormat.CH_RGB : ['R','G','B'], ImageFormat.CH_BGR : ['B','G','R'], ImageFormat.CH_RGGB : ['R','Gr','Gb','B'], ImageFormat.CH_GRBG : ['Gr','R','B','Gb'], ImageFormat.CH_GBRG : ['Gb','B','R','Gr'], ImageFormat.CH_BGGR : ['B','Gb','Gr','R'], }[channels] assert line.shape[1] == len(colors), f"Error: Mismatch between imageformat and number of channels" nb_values = line.shape[0] step_x = float(width) / nb_values x_range = np.array(range(0, nb_values)) x_pos = start_x + (x_range+0.5)*step_x max_val = np.max(line) line = line.astype(np.float32) in_margin = 2 in_start_y = start_y - in_margin in_height = height - 2*in_margin for channel in range(len(colors)): pen.setColor(qcolors[colors[channel]]) painter.setPen(pen) # apply a small Gaussian filtering to histogram curve path = QtGui.QPainterPath() y_pos = (in_start_y - line[:,channel]*(in_height/max_val)+0.5).astype(np.uint32) path.moveTo(x_pos[0], y_pos[0]) for n in range(1,len(x_range)): path.lineTo(x_pos[n], y_pos[n]) painter.drawPath(path) def display_message(self, im_pos: Optional[Tuple[int, int]], scale=None) ‑> str-
Expand source code
def display_message(self, im_pos: Optional[Tuple[int,int]], scale = None) -> str: text : str = self.image_name if self.show_cursor and im_pos: text += f"\n {self._image.data.shape} {self._image.data.dtype} prec:{self._image.precision}" if scale is not None: text += f"\n x{scale:0.2f}" im_x, im_y = im_pos values = self._image.data[im_y, im_x] text += f"\n pos {im_x:4}, {im_y:4} \n rgb {values}" if self.show_overlay: text += "\n ref | im " if self.show_image_differences: text += "\n im - ref" return text def display_text(self, painter: PySide6.QtGui.QPainter, text: str) ‑> None-
Expand source code
def display_text(self, painter: QtGui.QPainter, text: str) -> None: self.start_timing() color = QtGui.QColor(255, 50, 50, 255) if self.is_active() else QtGui.QColor(50, 50, 255, 255) painter.setPen(color) font = QtGui.QFont('Decorative', 12) # font.setBold(True) painter.setFont(font) painter.setBackground(QtGui.QColor(250, 250, 250, int(0.75*255))) painter.setBackgroundMode(QtGui.Qt.BGMode.OpaqueMode) text_options = \ QtCore.Qt.AlignmentFlag.AlignTop | \ QtCore.Qt.AlignmentFlag.AlignLeft | \ QtCore.Qt.TextFlag.TextWordWrap area_width = 400 area_height = 200 # boundingRect is interesting but slow to be called at each display # bounding_rect = painter.boundingRect(0, 0, area_width, area_height, text_options, self.display_message) margin_x = 8 margin_y = 5 painter.drawText( margin_x, # self.evt_height-margin_y-bounding_rect.height(), area_width, area_height, margin_y, area_width, area_height, text_options, text ) self.print_timing() 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 """ 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 return None def get_image(self)-
Expand source code
def get_image(self): return self._image def is_active(self)-
Expand source code
def is_active(self): return self.active_window def key_press_event(self, event, wsize)-
Expand source code
def key_press_event(self, event, wsize): self.print_log(f"ImageViewer: key_press_event {event.key()}") if type(event) == QtGui.QKeyEvent: if event.key() == QtKeys.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/3.-Image-Viewers'>Image Viewer</a>") mb.exec() event.accept() return if event.key() == QtKeys.Key_F11: self.toggle_fullscreen(event) return # allow to switch between images by pressing Alt+'image position' (Alt+0, Alt+1, etc) key_list = [] # # select upper left crop # key_list.append(QtCore.Qt.Key_A) # if event.key() == QtCore.Qt.Key_A: # self.current_dx = wsize.width()/4 # self.current_dy = -wsize.height()/4 # self.current_scale = 2 # select upper left crop key_list.append(QtKeys.Key_B) if event.key() == QtKeys.Key_B: self.current_dx = -wsize.width() / 4 self.current_dy = -wsize.height() / 4 self.current_scale = 2 # # select lower left crop # key_list.append(QtCore.Qt.Key_C) # if event.key() == QtCore.Qt.Key_C: # self.current_dx = wsize.width() / 4 # self.current_dy = wsize.height() / 4 # self.current_scale = 2 # # select lower right crop # key_list.append(QtCore.Qt.Key_D) # if event.key() == QtCore.Qt.Key_D: # self.current_dx = -wsize.width() / 4 # self.current_dy = wsize.height() / 4 # self.current_scale = 2 # select full crop key_list.append(QtKeys.Key_F) if event.key() == QtKeys.Key_F: self.output_crop = (0., 0., 1., 1.) self.current_dx = 0 self.current_dy = 0 self.current_scale = 1 # toggle antialiasing key_list.append(QtKeys.Key_A) if event.key() == QtKeys.Key_A: self.antialiasing = not self.antialiasing print(f"antialiasing {self.antialiasing}") # toggle histograph key_list.append(QtKeys.Key_H) if event.key() == QtKeys.Key_H: self.show_histogram = not self.show_histogram # toggle overlay key_list.append(QtKeys.Key_O) if event.key() == QtKeys.Key_O: self.show_overlay = not self.show_overlay # C: toggle cursor key_list.append(QtKeys.Key_C) if event.key() == QtKeys.Key_C: self.show_cursor = not self.show_cursor # D: toggle image differences key_list.append(QtKeys.Key_D) if event.key() == QtKeys.Key_D: self.show_image_differences = not self.show_image_differences # S: display stats on currrent image key_list.append(QtKeys.Key_S) if event.key() == QtKeys.Key_S: self.show_stats = not self.show_stats # I: display intensity line key_list.append(QtKeys.Key_I) if event.key() == QtKeys.Key_I: self.show_intensity_line = not self.show_intensity_line if event.key() in key_list: self.viewer_update() self.synchronize(self) event.accept() return event.ignore() else: event.ignore() def mouse_double_click_event(self, event)-
Expand source code
def mouse_double_click_event(self, event): self.print_log("double click ") # Check if double click is on histogram, if so, toggle histogram size if self._histo_rect and self._histo_rect.contains(event.x(), event.y()): # scale loops from 1 to 3 self._histo_scale = (self._histo_scale % 3) + 1 self.viewer_update() event.accept() return # Else set current viewer active self.set_active() self.viewer_update() if self.synchronize_viewer is not None: v = self.synchronize_viewer while v != self: v.set_active(False) v.viewer_update() if v.synchronize_viewer is not None: v = v.synchronize_viewer def mouse_move_event(self, event)-
Expand source code
def mouse_move_event(self, event): self.mouse_x = event.x() self.mouse_y = event.y() if self.show_overlay: self.viewer_update() if event.buttons() & QtMouse.LeftButton: self.mouse_dx = event.x() - self.lastPos.x() self.mouse_dy = - (event.y() - self.lastPos.y()) self.viewer_update() self.synchronize(self) event.accept() else: if event.buttons() & QtMouse.RightButton: # right button zoom self.mouse_zx = event.x() - self.lastPos.x() self.mouse_zy = - (event.y() - self.lastPos.y()) self.viewer_update() self.synchronize(self) event.accept() else: modifiers = QtWidgets.QApplication.keyboardModifiers() if self.show_cursor: self.viewer_update() self.synchronize(self) def mouse_press_event(self, event)-
Expand source code
def mouse_press_event(self, event): self.lastPos = event.pos() if event.buttons() & QtMouse.RightButton: event.accept() def mouse_release_event(self, event)-
Expand source code
def mouse_release_event(self, event): if event.button() & QtMouse.LeftButton: self.current_dx, self.current_dy = self.check_translation() self.mouse_dy = 0 self.mouse_dx = 0 event.accept() if event.button() & QtMouse.RightButton: if self._image is not None: self.current_scale = self.new_scale(self.mouse_zy, self._image.data.shape[0]) self.mouse_zy = 0 self.mouse_zx = 0 event.accept() self.synchronize(self) def mouse_wheel_event(self, event)-
Expand source code
def mouse_wheel_event(self,event): # Zoom by applying a factor to the distances to the sides if hasattr(event, 'delta'): delta = event.delta() else: delta = event.angleDelta().y() # print("delta = {}".format(delta)) coeff = delta/5 # coeff = 20 if delta > 0 else -20 if self._image: self.current_scale = self.new_scale(coeff, self._image.data.shape[0]) self.viewer_update() self.synchronize(self) def new_scale(self, mouse_zy, height)-
Expand source code
def new_scale(self, mouse_zy, height): return max(1, self.current_scale * (1 + mouse_zy * 5.0 / self._height)) # return max(1, self.current_scale + mouse_zy * 5.0 / height) def new_translation(self)-
Expand source code
def new_translation(self): dx = self.current_dx + self.mouse_dx/self.current_scale dy = self.current_dy + self.mouse_dy/self.current_scale return dx, dy def print_log(self, mess, force=False)-
Expand source code
def print_log(self, mess, force=False): if self.verbose or force: caller_name = inspect.stack()[1][3] print("{}{}: {}".format(self.tab[0], caller_name, mess)) def print_timing(self, add_total=False, force=False, title=None)-
Expand source code
def print_timing(self, add_total=False, force=False, title=None): if not self.display_timing: return if title is None: caller_name = inspect.stack()[1][3] class_name = get_class_from_frame(inspect.stack()[1][0]) if class_name is not None: caller_name = "{}.{}".format(class_name, caller_name) else: caller_name = title if add_total: self.add_time("total", self.start_time[caller_name], force) if self.timings[caller_name] != '': print(self.timings[caller_name]) def set_active(self, active=True)-
Expand source code
def set_active(self, active=True): self.active_window = active def set_clipboard(self, clipboard, save_image)-
Expand source code
def set_clipboard(self, clipboard, save_image): self.clipboard = clipboard self.save_image_clipboard = save_image def set_image(self, image: Optional[ViewerImage])-
Expand source code
def set_image(self, image : Optional[ViewerImage]): is_different = (self._image is None) or (self._image is not image) if image is not None: self.print_log('set_image({}): is_different = {}'.format(image.data.shape, is_different)) if is_different: self._image = image self.image_id += 1 return is_different def set_image_ref(self, image_ref: Optional[ViewerImage] = None)-
Expand source code
def set_image_ref(self, image_ref : Optional[ViewerImage] = None): is_different = (self._image_ref is None) or (self._image_ref is not image_ref) if is_different: self._image_ref = image_ref self.image_ref_id += 1 def set_synchronize(self, viewer)-
Expand source code
def set_synchronize(self, viewer): self.synchronize_viewer = viewer def start_timing(self, title=None)-
Expand source code
def start_timing(self, title=None): if not self.display_timing: return if title is None: # it seems that inspect is slow caller_name = inspect.stack()[1][3] class_name = get_class_from_frame(inspect.stack()[1][0]) if class_name is not None: caller_name = "{}.{}".format(class_name, caller_name) else: caller_name = title self.start_time[caller_name] = get_time() self.timings[caller_name] = '' def synchronize(self, event_viewer)-
This method needs to be overloaded with call to self.synchronize_viewer.synchronize() :param event_viewer: the viewer that started the synchronization :return:
Expand source code
def synchronize(self, event_viewer): """ This method needs to be overloaded with call to self.synchronize_viewer.synchronize() :param event_viewer: the viewer that started the synchronization :return: """ if self==event_viewer: if self.display_timing: start_time = get_time() if self.display_timing: print("[ --- Start sync") if self.synchronize_viewer is not None and self.synchronize_viewer is not event_viewer: self.synchronize_data(self.synchronize_viewer) self.synchronize_viewer.viewer_update() self.synchronize_viewer.synchronize(event_viewer) if self==event_viewer: if self.display_timing: print(' End sync --- {:0.1f} ms'.format((get_time()-start_time)*1000)) def synchronize_data(self, other_viewer)-
Expand source code
def synchronize_data(self, other_viewer): other_viewer.current_scale = self.current_scale other_viewer.current_dx = self.current_dx other_viewer.current_dy = self.current_dy other_viewer.mouse_dx = self.mouse_dx other_viewer.mouse_dy = self.mouse_dy other_viewer.mouse_zx = self.mouse_zx other_viewer.mouse_zy = self.mouse_zy other_viewer.mouse_x = self.mouse_x other_viewer.mouse_y = self.mouse_y other_viewer.show_histogram = self.show_histogram other_viewer.show_cursor = self.show_cursor other_viewer.show_intensity_line = self.show_intensity_line other_viewer._histo_scale = self._histo_scale 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: 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 viewer_update(self)-
Expand source code
@abstractmethod def viewer_update(self): pass
class trace_method (tab)-
Expand source code
class trace_method(): def __init__(self, tab): self.tab = tab method = traceback.extract_stack(None, 2)[0][2] print(self.tab[0] + method) self.tab[0] += ' ' def __del__(self): self.tab[0] = self.tab[0][:-2]