broken refreshed image when move crosshair on an imageview - python

I'm try to implement a crosshair item on a imageview using pyqtgraph, it seems ok with the functions.
But, when I zoom in the image and move the crosshair, there are some broken points or lines on the boundary of pixel.
Any suggestions will be much appreciated.
completed code as below, could be run and replicate the problem.
# -*- coding: utf-8 -*-
"""
This example demonstrates the use of ImageView, which is a high-level widget for
displaying and analyzing 2D and 3D data. ImageView provides:
1. A zoomable region (ViewBox) for displaying the image
2. A combination histogram and gradient editor (HistogramLUTItem) for
controlling the visual appearance of the image
3. A timeline for selecting the currently displayed frame (for 3D data only).
4. Tools for very basic analysis of image data (see ROI and Norm buttons)
"""
## Add path to library (just for examples; you do not need this)
#import initExample
import numpy as np
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtWidgets import *
# Interpret image data as row-major instead of col-major
pg.setConfigOptions(imageAxisOrder='row-major')
app = QtGui.QApplication([])
## Create window with ImageView widget
win = QtGui.QMainWindow()
win.resize(800,800)
plt=pg.PlotItem()
imv = pg.ImageView(view=plt)
label = pg.LabelItem(justify='right')
plt.addItem(label)
win.setCentralWidget(imv)
win.show()
win.setWindowTitle('pyqtgraph example: ImageView')
## Create random 3D data set with noisy signals
img = pg.gaussianFilter(np.random.normal(size=(200, 200)), (5, 5)) * 20 + 100
img = img[np.newaxis,:,:]
decay = np.exp(-np.linspace(0,0.3,100))[:,np.newaxis,np.newaxis]
data = np.random.normal(size=(100, 200, 200))
data += img * decay
data += 2
## Add time-varying signal
sig = np.zeros(data.shape[0])
sig[30:] += np.exp(-np.linspace(1,10, 70))
sig[40:] += np.exp(-np.linspace(1,10, 60))
sig[70:] += np.exp(-np.linspace(1,10, 30))
sig = sig[:,np.newaxis,np.newaxis] * 3
data[:,50:60,30:40] += sig
## Display the data and assign each frame a time value from 1.0 to 3.0
imv.setImage(data, xvals=np.linspace(1., 3., data.shape[0]))
## Set a custom color map
colors = [
(0, 0, 0),
(45, 5, 61),
(84, 42, 55),
(150, 87, 60),
(208, 171, 141),
(255, 255, 255)
]
cmap = pg.ColorMap(pos=np.linspace(0.0, 1.0, 6), color=colors)
imv.setColorMap(cmap)
#cross hair
vLine = pg.InfiniteLine(angle=90, movable=False)
hLine = pg.InfiniteLine(angle=0, movable=False)
plt.addItem(vLine, ignoreBounds=True)
plt.addItem(hLine, ignoreBounds=True)
vb = plt.vb
def mouseMoved(evt):
pos = evt[0] ## using signal proxy turns original arguments into a tuple
if plt.sceneBoundingRect().contains(pos):
mousePoint = vb.mapSceneToView(pos)
# index = int(mousePoint.x())
# if index > 0 and index < len(data):
# label.setText("<span style='font-size: 12pt'>x=%0.1f, <span style='color: red'>y1=%0.1f</span>" % (mousePoint.x(), data[index, index]))
vLine.setPos(mousePoint.x())
hLine.setPos(mousePoint.y())
proxy = pg.SignalProxy(plt.scene().sigMouseMoved, rateLimit=60, slot=mouseMoved)
## Start Qt event loop unless running in interactive mode.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
I expect the image should be good at anytime, when I move the crosshair.
Thank you.

Related

Qt Custom CheckBox, help about PaintEvent

I am trying to create a custom checkbox. I am using paintEvent function to create my special checkbox.
It's design:
The result on Qt:
First of all, rounded should be added and the junction of the lines should be a smoother transition.
I need a more professional solution. Which is pretty looking.
Thanks!
Code:
import sys, os, time
from PySide6 import QtCore, QtWidgets, QtGui
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *
class ECheckBoxData(object):
Radius = 10
AnimationTime = 600 # ms
FontSize, FontSpacing = 16, 0
Color = {
"CORNER": QColor(239, 239, 239),
"BASE_BACKGROUND": QColor(255, 125, 51),
"BASE_FOREGROUND": QColor(255, 152, 91),
"BASE_HOVER_BACKGROUND" :QColor(255, 152, 91),
"BASE_HOVER_FOREGROUND": QColor(247, 247, 250),
}
TextElide = Qt.ElideMiddle
CheckWidth, CheckHeight = 128, 128
class ECheckBox(QCheckBox):
CheckBoxData = ECheckBoxData()
def __init__(self, CheckBoxData=ECheckBoxData()):
super(ECheckBox, self).__init__(None)
self.CheckBoxData = CheckBoxData
self.myfont = QFont("Times New Roman", 16, weight=QFont.Bold)
self.myfont.setWordSpacing(self.CheckBoxData.FontSpacing)
self.myfont.setStyleHint(QFont.Monospace)
self.myfontMetrics = QFontMetrics(self.myfont)
# font.setStyleHint(QFont.Times, QFont.PreferAntialias)
self.setFont(self.myfont)
self.setFixedHeight(self.CheckBoxData.CheckHeight+2)
self.setMinimumWidth(self.CheckBoxData.CheckWidth+8)
def paintEvent(self, event: QPaintEvent):
pt = QPainter(self)
pt.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing )
border = QPainterPath()
pt.setBrush(self.CheckBoxData.Color["BASE_BACKGROUND"])
pt.setPen(QPen(self.CheckBoxData.Color["CORNER"],5))
border.addRoundedRect(QRectF(2,2,self.CheckBoxData.CheckWidth-2, self.CheckBoxData.CheckHeight-2),self.CheckBoxData.Radius, self.CheckBoxData.Radius)
pt.drawPath(border)
pt.setClipPath(border)
pt.setPen(QPen(Qt.white,self.CheckBoxData.CheckWidth/10))
pt.setBrush(Qt.white)
path2 = QPainterPath()
arrow_width, arrow_height = self.width()/4, self.height()/ (66/8)
center_width, center_height = int(self.width()/2), int(self.height()/2)
#path2.moveTo((self.width() - arrow_width * 2) / 2, (center_height + 2))
#path2.lineTo(QPoint((self.width() - arrow_width) / 2 + 2, (center_height) + arrow_height + 1))
#path2.lineTo(QPoint((self.width()-arrow_width), (center_height)-arrow_height))
path2.addPolygon(QPolygonF([
QPoint((self.width()-arrow_width*2)/2, (center_height+2)), QPoint((self.width()-arrow_width)/2+2, (center_height)+arrow_height+1)
]))
path2.addPolygon(QPolygonF([QPoint((self.width()-arrow_width)/2+2, (center_height)+arrow_height+1), QPoint((self.width()-arrow_width-12), (center_height)-arrow_height)]))
pt.drawPath(path2)
if __name__ == "__main__":
app = QApplication(sys.argv)
wind = QMainWindow()
wind.setStyleSheet("QMainWindow{background-color:rgb(247,247,250)}")
wind.resize(221, 150)
wid = QWidget()
lay = QHBoxLayout(wid)
lay.setAlignment(Qt.AlignCenter)
Data = ECheckBoxData()
e = ECheckBox(Data)
e.setChecked(True)
lay.addWidget(e)
wind.setCentralWidget(wid)
wind.show()
sys.exit(app.exec())
In order to create a smooth and curved outline, you need to properly set the QPen cap and join style.
Using a polygon to draw the outline is obviously not a valid solution, as that outline will be drawn with the pen, but what you need is a path that will be painted with a thick pen and the preferred cap and join styles.
Also, in order to be able to draw a good icon at different sizes, you should not rely on fixed sizes (even if properly computed), but use the current size as a reference instead.
def paintEvent(self, event: QPaintEvent):
pt = QPainter(self)
pt.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing)
size = min(self.width(), self.height())
border = max(1, size / 32)
rect = QRectF(0, 0, size - border, size - border)
# move the square to the *exact* center using a QRectF based on the
# current widget; note: it is very important that you get the center
# using a QRectF, because the center of QRect is always in integer
# values, and will almost always be imprecise at small sizes
rect.moveCenter(QRectF(self.rect()).center())
borderPath = QPainterPath()
# with RelativeSize we can specify the radius as 30% of half the size
borderPath.addRoundedRect(rect, 30, 30, Qt.RelativeSize)
pt.setBrush(self.CheckBoxData.Color["BASE_BACKGROUND"])
pt.setPen(QPen(self.CheckBoxData.Color["CORNER"], border * 2.5))
pt.drawPath(borderPath)
pt.setPen(QPen(Qt.white, size * .125,
cap=Qt.RoundCap, join=Qt.RoundJoin))
arrow_path = QPainterPath()
arrow_path.moveTo(size * .25, size * .5)
arrow_path.lineTo(size * .40, size * .65)
arrow_path.lineTo(size * .7, size * .325)
pt.drawPath(arrow_path.translated(rect.topLeft()))

Set a vispy auto zoom that shows all items on screen like pyqtgraph autoRange() function

Is it possible to implement an auto range function for vispy like we have for pyqtgraph? I have search but can't find any or an alternative way to implement it.
Here is the sample code
from itertools import cycle
import numpy as np
from vispy import app, scene, io
from vispy.color import get_colormaps, BaseColormap
# Read volume
vol2 = np.load(io.load_data_file('brain/mri.npz'))['data']
vol2 = np.flipud(np.rollaxis(vol2, 1))
# Prepare canvas
canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
canvas.measure_fps()
# Set up a viewbox to display the image with interactive pan/zoom
view = canvas.central_widget.add_view()
# Set whether we are emulating a 3D texture
emulate_texture = False
# Create the volume visuals, only one is visible
volume1 = scene.visuals.Volume(vol2, parent=view.scene, threshold=0.225,
emulate_texture=emulate_texture)
# volume1.transform = scene.STTransform(translate=(64, 64, 0))
volume1.transform = scene.STTransform(scale=(3,3,3))
volume2 = scene.visuals.Volume(vol2, parent=view.scene, threshold=0.2,
emulate_texture=emulate_texture)
volume2.visible = False
print(volume1.bounds(0), volume1.bounds(1), volume1.bounds(2))
# Create two cameras (1 for firstperson, 3 for 3d person)
fov = 60.
cam1 = scene.cameras.FlyCamera(parent=view.scene, fov=fov)
cam2 = scene.cameras.TurntableCamera(parent=view.scene, fov=fov)
cam3 = scene.cameras.ArcballCamera(parent=view.scene, fov=fov)
view.camera = cam2 # Select turntable at first
if __name__ == '__main__':
print(__doc__)
app.run()

ScaleBar in pyqtgraph doesn't update when scale is changed

When I change the scale of the axis of my image, my ScaleBar shows the incorrect scale. How do I update the scale bar when I change the axes?
from PyQt5 import QtWidgets
import pyqtgraph as pg
import numpy as np
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
imvOCTTopLeft = pg.ImageView(view=pg.PlotItem())
imvOCTTopLeft.setImage(np.random.normal(size=(100,100)))
imvOCTTopLeft.view.getAxis('left').setScale(0.6)
imvOCTTopLeft.view.getAxis('bottom').setScale(0.4)
scale = pg.ScaleBar(size=10,suffix = "px")
viewbox = imvOCTTopLeft.view
if not isinstance(viewbox, pg.ViewBox): viewbox = viewbox.getViewBox()
scale.setParentItem(viewbox)
scale.anchor((1, 1), (1, 1), offset=(-20, -20))
imvOCTTopLeft.show()
sys.exit(app.exec_())
This image shows that the scale bar is showing approximately 4 pixels but states that it is showing 10 pixels.
I think this is because I changed the axis scale.
This seems to be a bug: link. The viewbox rescales after sigRangeChanged is emitted.
"Hacky" solution is to delay the ScaleBar update:
(You might need to play around with the time, 100 and 10 worked for me. If it doesnt work, increase it.)
from PyQt5 import QtWidgets, QtCore
import pyqtgraph as pg
import numpy as np
def updateDelay(scale, time):
QtCore.QTimer.singleShot(time, scale.updateBar)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
plotItem = pg.PlotItem()
imvOCTTopLeft = pg.ImageView(view=plotItem)
imvOCTTopLeft.setImage(np.random.normal(size=(100, 100)))
imvOCTTopLeft.view.getAxis('left').setScale(0.6)
scale = 0.4 #edit
imvOCTTopLeft.view.getAxis('bottom').setScale(scale) #edit
scale = pg.ScaleBar(size=10*(1/scale), suffix="px") #edit
scale.text.setText('10 px') #edit
plotItem.sigRangeChanged.connect(lambda: updateDelay(scale, 10)) # here: time=10ms
viewbox = imvOCTTopLeft.view
if not isinstance(viewbox, pg.ViewBox): viewbox = viewbox.getViewBox()
scale.setParentItem(viewbox)
scale.anchor((1, 1), (1, 1), offset=(-20, -20))
imvOCTTopLeft.show()
updateDelay(scale, 100) # here time=100ms
sys.exit(app.exec_())
Result:

Returning mouse cursor coordinates in PyQtGraph

I am new to PyQtGraph and want to use it for a speedy visualization of my data acquisition. Previously I was using matplotlib where redrawing the figure was my bottleneck. After transitioning to PyQtGraph, I am currently missing only one functionality of matplotlib. Namely, returning the x-, and y-coordinate of my mouse cursor.
How can I call/mimic the return of the x-, and y-coordinates of my mouse cursor in a plot made using PyQtGraph?
EDIT! - After implementing the tips of leongold, the code is able to return the mousecursor position without losing speed. The code is the following:
import numpy
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore
def gaussian(A, B, x):
return A * numpy.exp(-(x/(2. * B))**2.)
def mouseMoved(evt):
mousePoint = p.vb.mapSceneToView(evt[0])
label.setText("<span style='font-size: 14pt; color: white'> x = %0.2f, <span style='color: white'> y = %0.2f</span>" % (mousePoint.x(), mousePoint.y()))
# Initial data frame
x = numpy.linspace(-5., 5., 10000)
y = gaussian(5., 0.2, x)
# Generate layout
win = pg.GraphicsWindow()
label = pg.LabelItem(justify = "right")
win.addItem(label)
p = win.addPlot(row = 1, col = 0)
plot = p.plot(x, y, pen = "y")
proxy = pg.SignalProxy(p.scene().sigMouseMoved, rateLimit=60, slot=mouseMoved)
# Update layout with new data
i = 0
while i < 500:
noise = numpy.random.normal(0, .2, len(y))
y_new = y + noise
plot.setData(x, y_new, pen = "y", clear = True)
p.enableAutoRange("xy", False)
pg.QtGui.QApplication.processEvents()
i += 1
win.close()
You need to setup a pyqtgraph.SignalProxy and connect it to a callback:
if p is your plot, it'll look like: pyqtgraph.SignalProxy(p.scene().sigMouseMoved, rateLimit=60, slot=callback)
Whenever the mouse is moved over the plot, the callback is called with an event as an argument, i.e. callback(event). event[0] holds a positional argument you pass to p.vb.mapSceneToView(position).x() for x value and p.vb.mapSceneToView(position).y() for y value.

Displaying LaTeX in pyQt/pySide QTableWidget

I would like to add mathematical expressions to the table labels (e.g.: 2^3 should be properly formatted)
Here is a simple example of a table:
http://thomas-cokelaer.info/blog/2012/10/pyqt4-example-of-tablewidget-usage/
setHorizontalHeaderLabels accepts string, only.
I wonder if is it possible to implement somehow this matplotlib approach:
matplotlib - write TeX on Qt form
are there other options?
I've also been trying for some time to display complex labels in the header of a QTableWidget. I was able to do it by reimplementing the paintSection method of a QHeaderView and painting manually the label with a QTextDocument as described in a thread on Qt Centre.
However, this solution was somewhat limited compared to what could be done with LaTex. I thought this could be a good idea to try the approach you suggested in your OP, i.e. using the capability of matplotlib to render LaTex in PySide.
1. Convert matplotlib Figure to QPixmap
First thing that is required in this approach is to be able to convert matplotlib figure in a format that can be easily painted on any QWidget. Below is a function that take a mathTex expression as input and convert it through matplotlib to a QPixmap.
import sys
import matplotlib as mpl
from matplotlib.backends.backend_agg import FigureCanvasAgg
from PySide import QtGui, QtCore
def mathTex_to_QPixmap(mathTex, fs):
#---- set up a mpl figure instance ----
fig = mpl.figure.Figure()
fig.patch.set_facecolor('none')
fig.set_canvas(FigureCanvasAgg(fig))
renderer = fig.canvas.get_renderer()
#---- plot the mathTex expression ----
ax = fig.add_axes([0, 0, 1, 1])
ax.axis('off')
ax.patch.set_facecolor('none')
t = ax.text(0, 0, mathTex, ha='left', va='bottom', fontsize=fs)
#---- fit figure size to text artist ----
fwidth, fheight = fig.get_size_inches()
fig_bbox = fig.get_window_extent(renderer)
text_bbox = t.get_window_extent(renderer)
tight_fwidth = text_bbox.width * fwidth / fig_bbox.width
tight_fheight = text_bbox.height * fheight / fig_bbox.height
fig.set_size_inches(tight_fwidth, tight_fheight)
#---- convert mpl figure to QPixmap ----
buf, size = fig.canvas.print_to_buffer()
qimage = QtGui.QImage.rgbSwapped(QtGui.QImage(buf, size[0], size[1],
QtGui.QImage.Format_ARGB32))
qpixmap = QtGui.QPixmap(qimage)
return qpixmap
2. Paint the QPixmaps to the header of a QTableWidget
The next step is to paint the QPixmap in the header of a QTableWidget. As shown below, I've done it by sub-classing QTableWidget and reimplementing the setHorizontalHeaderLabels method, which is used to convert the mathTex expressions for the labels into QPixmap and to pass it as a list to a subclass of QHeaderView. The QPixmap are then painted within a reimplementation of the paintSection method of QHeaderView and the height of the header is set up to fit the height of the mathTex expression in the reimplementation of the sizeHint methods.
class MyQTableWidget(QtGui.QTableWidget):
def __init__(self, parent=None):
super(MyQTableWidget, self).__init__(parent)
self.setHorizontalHeader(MyHorizHeader(self))
def setHorizontalHeaderLabels(self, headerLabels, fontsize):
qpixmaps = []
indx = 0
for labels in headerLabels:
qpixmaps.append(mathTex_to_QPixmap(labels, fontsize))
self.setColumnWidth(indx, qpixmaps[indx].size().width() + 16)
indx += 1
self.horizontalHeader().qpixmaps = qpixmaps
super(MyQTableWidget, self).setHorizontalHeaderLabels(headerLabels)
class MyHorizHeader(QtGui.QHeaderView):
def __init__(self, parent):
super(MyHorizHeader, self).__init__(QtCore.Qt.Horizontal, parent)
self.setClickable(True)
self.setStretchLastSection(True)
self.qpixmaps = []
def paintSection(self, painter, rect, logicalIndex):
if not rect.isValid():
return
#------------------------------ paint section (without the label) ----
opt = QtGui.QStyleOptionHeader()
self.initStyleOption(opt)
opt.rect = rect
opt.section = logicalIndex
opt.text = ""
#---- mouse over highlight ----
mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos())
if rect.contains(mouse_pos):
opt.state |= QtGui.QStyle.State_MouseOver
#---- paint ----
painter.save()
self.style().drawControl(QtGui.QStyle.CE_Header, opt, painter, self)
painter.restore()
#------------------------------------------- paint mathText label ----
qpixmap = self.qpixmaps[logicalIndex]
#---- centering ----
xpix = (rect.width() - qpixmap.size().width()) / 2. + rect.x()
ypix = (rect.height() - qpixmap.size().height()) / 2.
#---- paint ----
rect = QtCore.QRect(xpix, ypix, qpixmap.size().width(),
qpixmap.size().height())
painter.drawPixmap(rect, qpixmap)
def sizeHint(self):
baseSize = QtGui.QHeaderView.sizeHint(self)
baseHeight = baseSize.height()
if len(self.qpixmaps):
for pixmap in self.qpixmaps:
baseHeight = max(pixmap.height() + 8, baseHeight)
baseSize.setHeight(baseHeight)
self.parentWidget().repaint()
return baseSize
3. Application
Below is an example of a simple application of the above.
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
w = MyQTableWidget()
w.verticalHeader().hide()
headerLabels = [
'$C_{soil}=(1 - n) C_m + \\theta_w C_w$',
'$k_{soil}=\\frac{\\sum f_j k_j \\theta_j}{\\sum f_j \\theta_j}$',
'$\\lambda_{soil}=k_{soil} / C_{soil}$']
w.setColumnCount(len(headerLabels))
w.setHorizontalHeaderLabels(headerLabels, 25)
w.setRowCount(3)
w.setAlternatingRowColors(True)
k = 1
for j in range(3):
for i in range(3):
w.setItem(i, j, QtGui.QTableWidgetItem('Value %i' % (k)))
k += 1
w.show()
w.resize(700, 200)
sys.exit(app.exec_())
which results in:
The solution is not perfect, but it is a good starting point. I'll update it when I will improve it for my own application.

Categories

Resources