Related
How to make an Angled arrow-type border in PyQt QFrame? In My code, I Have two QLabels and respective frames. My aim is to make an arrow shape border on right side of every QFrame.For clear-cut idea, attach a sample picture.
import sys
from PyQt5.QtWidgets import *
class Angle_Border(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Angle Border")
self.lbl1 = QLabel("Python")
self.lbl2 = QLabel("PyQt")
self.frame1 = QFrame()
self.frame1.setProperty("type","1")
self.frame1.setFixedSize(200,50)
self.frame1.setStyleSheet("background-color:red;color:white;"
"font-family:Trebuchet MS;font-size: 15pt;text-align: center;"
"border-top-right-radius:25px solid ; border-bottom-right-radius:25px solid ;")
self.frame2 = QFrame()
self.frame2.setFixedSize(200, 50)
self.frame2.setStyleSheet("background-color:blue;color:white;"
"font-family:Trebuchet MS;font-size: 15pt;text-align: center;"
"border-top:1px solid transparent; border-bottom:1px solid transparent;")
self.frame_outer = QFrame()
self.frame_outer.setFixedSize(800, 60)
self.frame_outer.setStyleSheet("background-color:green;color:white;"
"font-family:Trebuchet MS;font-size: 15pt;text-align: center;")
self.frame1_layout = QHBoxLayout(self.frame1)
self.frame2_layout = QHBoxLayout(self.frame2)
self.frame_outer_layout = QHBoxLayout(self.frame_outer)
self.frame_outer_layout.setContentsMargins(5,0,0,0)
self.frame1_layout.addWidget(self.lbl1)
self.frame2_layout.addWidget(self.lbl2)
self.hbox = QHBoxLayout()
self.layout = QHBoxLayout()
self.hbox.addWidget(self.frame1)
self.hbox.addWidget(self.frame2)
self.hbox.addStretch()
self.hbox.setSpacing(0)
# self.layout.addLayout(self.hbox)
self.frame_outer_layout.addLayout(self.hbox)
self.layout.addWidget(self.frame_outer)
self.setLayout(self.layout)
def main():
app = QApplication(sys.argv)
ex = Angle_Border()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Sample Picture
Since the OP didn't ask for user interaction (mouse or keyboard), a possible solution could use the existing features of Qt, specifically QSS (Qt Style Sheets).
While the currently previously accepted solution does follow that approach, it's not very effective, most importantly because it's basically "static", since it always requires knowing the color of the following item in order to define the "arrow" colors.
This not only forces the programmer to always consider the "sibling" items, but also makes extremely (and unnecessarily) complex the dynamic creation of such objects.
The solution is to always (partially) "redo" the layout and update the stylesheets with the necessary values, which consider the current size (which shouldn't be hardcoded), the following item (if any) and carefully using the layout properties and "spacer" stylesheets based on the contents.
The following code uses a more abstract, dynamic approach, with basic functions that allow adding/insertion and removal of items. It still uses a similar QSS method, but, with almost the same "line count", it provides a simpler and much more intuitive approach, allowing item creation, deletion and modification with single function calls that are much easier to use.
A further benefit of this approach is that implementing "reverse" arrows is quite easy, and doesn't break the logic of the item creation.
Considering all the above, you can create an actual class that just needs basic calls such as addItem() or removeItem().
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class ArrowMenu(QWidget):
vMargin = -1
hMargin = -1
def __init__(self, items=None, parent=None):
super().__init__(parent)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addStretch()
self.items = []
if isinstance(items, dict):
self.addItems(items.items())
elif items is not None:
self.addItems(items)
def addItems(self, items):
for item in items:
if isinstance(item, str):
self.addItem(item)
else:
self.addItem(*item)
def addItem(self, text, background=None):
self.insertItem(len(self.items), text, background)
def insertItem(self, index, text, background=None):
label = QLabel(text)
if background is None:
background = self.palette().window().color()
background.setAlpha(0)
else:
background = QColor(background)
# human eyes perceive "brightness" in different ways, let's compute
# that value in order to decide a color that has sufficient contrast
# with the background; see https://photo.stackexchange.com/q/10412
r, g, b, a = background.getRgbF()
brightness = r * .3 + g * .59 + b * .11
foreground = 'black' if brightness >= .5 else 'white'
label.setStyleSheet('color: {}; background: {};'.format(
foreground, background.name(background.HexArgb)))
layout = self.layout()
if index < len(self.items):
i = 0
for _label, _spacer, _ in self.items:
if i == index:
i += 1
layout.insertWidget(i * 2, _label)
layout.insertWidget(i * 2 + 1, _spacer)
i += 1
layout.insertWidget(index * 2, label)
spacer = QWidget(objectName='menuArrow')
layout.insertWidget(index * 2 + 1, spacer)
self.items.insert(index, (label, spacer, background))
self.updateItems()
def removeItem(self, index):
label, spacer, background = self.items.pop(index)
label.deleteLater()
spacer.deleteLater()
layout = self.layout()
for i, (label, spacer, _) in enumerate(self.items):
layout.insertWidget(i * 2, label)
layout.insertWidget(i * 2 + 1, spacer)
self.updateItems()
self.updateGeometry()
def updateItems(self):
if not self.items:
return
size = self.fontMetrics().height()
if self.vMargin < 0:
vSize = size * 2
else:
vSize = size + self.vMargin * 2
spacing = vSize / 2
self.setMinimumHeight(vSize)
if self.hMargin >= 0:
labelMargin = self.hMargin * 2
else:
labelMargin = size // 2
it = iter(self.items)
prevBackground = prevSpacer = None
while True:
try:
label, spacer, background = next(it)
label.setContentsMargins(labelMargin, 0, labelMargin, 0)
spacer.setFixedWidth(spacing)
except StopIteration:
background = QColor()
break
finally:
if prevBackground:
if background.isValid():
cssBackground = background.name(QColor.HexArgb)
else:
cssBackground = 'none'
if prevBackground.alpha():
prevBackground = prevBackground.name(QColor.HexArgb)
else:
mid = QColor(prevBackground)
mid.setAlphaF(.5)
prevBackground = '''
qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 {}, stop:1 {})
'''.format(
prevBackground.name(QColor.HexArgb),
mid.name(QColor.HexArgb),
)
prevSpacer.setStyleSheet('''
ArrowMenu > .QWidget#menuArrow {{
background: transparent;
border-top: {size}px solid {background};
border-bottom: {size}px solid {background};
border-left: {spacing}px solid {prevBackground};
}}
'''.format(
size=self.height() // 2,
spacing=spacing,
prevBackground=prevBackground,
background=cssBackground
))
prevBackground = background
prevSpacer = spacer
def resizeEvent(self, event):
self.updateItems()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
items = (
('Python', 'green'),
('Will delete', 'chocolate'),
('PyQt5', 'red'),
('Java', 'blue'),
('ASP.Net', 'yellow'),
)
ex = ArrowMenu(items)
ex.show()
QTimer.singleShot(2000, lambda: ex.addItem('New item', 'aqua'))
QTimer.singleShot(5000, lambda: ex.removeItem(1))
sys.exit(app.exec_())
And here is the result:
import sys
from PyQt5.QtWidgets import QWidget,QHBoxLayout,QLabel,QFrame,QApplication,QSizePolicy
from PyQt5.QtCore import Qt
class MyFrame(QWidget):
def __init__(self,base_color,top_color,width,edge,text,text_color):
super().__init__()
self.base_color = base_color
self.top_color = top_color
self.width = width
self.edge = edge
self.text = text
self.text_color = text_color
self.lbl = QLabel()
self.lbl.setText(self.text)
self.lbl.setFixedHeight(self.width*2)
self.lbl.setMinimumWidth((QSizePolicy.MinimumExpanding)+100)
self.lbl.setContentsMargins(0,0,0,0)
self.lbl.setAlignment(Qt.AlignCenter)
self.lbl.setStyleSheet(f"QLabel"
f"{{background-color: {self.base_color};"
f"color:{self.text_color};"
f"font-family:Trebuchet MS;"
f"font-size: 15pt;}}")
self.frame_triangle = QFrame()
self.frame_triangle.setFixedSize(self.width, self.width * 2)
self.frame_triangle.setContentsMargins(0,0,0,0)
self.hbox = QHBoxLayout()
self.hbox.setSpacing(0)
self.hbox.setContentsMargins(0,0,0,0)
self.setLayout(self.hbox)
if self.edge == "right":
self.border = "border-left"
self.hbox.addWidget(self.lbl)
self.hbox.addWidget(self.frame_triangle)
elif self.edge == "left":
self.border = "border-right"
self.hbox.addWidget(self.frame_triangle)
self.hbox.addWidget(self.lbl)
elif self.edge == "none":
self.border = "border-right"
self.hbox.addWidget(self.lbl)
self.lbl.setMinimumWidth((QSizePolicy.MinimumExpanding) + 150)
self.frame_triangle.setStyleSheet(f"QFrame"
f"{{background-color: {self.base_color};"
f"border-top:100px solid {self.top_color};"
f"{self.border}:100px solid {self.base_color};"
f"border-bottom:100px solid {self.top_color};"
f"}}")
class Main_Frame(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Angled Frame")
triangle_size = 50
self.frame1 = MyFrame("lightgrey","green",triangle_size,"right","","lightgrey")
self.frame2 = MyFrame("green","red",triangle_size,"right","Python","white")
self.frame3 = MyFrame("red","blue",triangle_size,"right","PyQt5","white")
self.frame4 = MyFrame("blue","yellow",triangle_size,"right","Java","white")
self.frame5 = MyFrame("yellow","lightgrey",triangle_size,"right","ASP.Net","black")
self.frame_overall = QFrame()
self.frame_overall.setStyleSheet("background-color:lightgrey;")
self.frame_overall.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Maximum)
self.frame_overall_layout = QHBoxLayout(self.frame_overall)
self.frame_overall_layout.setSpacing(0)
# self.frame_overall_layout.addWidget(self.frame1)
self.frame_overall_layout.addWidget(self.frame2)
self.frame_overall_layout.addWidget(self.frame3)
self.frame_overall_layout.addWidget(self.frame4)
self.frame_overall_layout.addWidget(self.frame5)
self.vbox = QHBoxLayout()
self.vbox.setContentsMargins(0,0,0,0)
self.vbox.setSpacing(0)
self.vbox.addStretch()
self.vbox.addWidget(self.frame_overall)
self.vbox.addStretch()
self.setLayout(self.vbox)
def main():
app = QApplication(sys.argv)
ex = Main_Frame()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
It seems that this link can anwser your question. However, I adopt a python version for you.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import QColor, QPainter, QPen, QPainterPath, QBrush
class Angle_Border(QWidget):
def __init__(self, firstButtonX, firstButtonY, buttonWidth, buttonHeight, triangleWidth, labels, colors):
super().__init__()
self.firstButtonX = firstButtonX
self.firstButtonY = firstButtonY
self.buttonWidth = buttonWidth
self.buttonHeight = buttonHeight
self.triangleWidth = triangleWidth
self.labels = labels
self.colors = colors
self.button_lists = []
for i, text_i in enumerate(self.labels):
button_i = QPushButton(text_i, self)
self.button_lists.append(button_i)
button_i.setGeometry(self.firstButtonX + (self.buttonWidth+self.triangleWidth)*i, self.firstButtonY,
self.buttonWidth, self.buttonHeight)
button_i.setStyleSheet("background-color: %s;border-style: outset;border-width: 0px;" % (QColor(self.colors[i]).name()))
# button_i.setStyleSheet("border-style: outset;border-width: 0px;")
def paintEvent(self, event):
super().paintEvent(event)
painter = QPainter(self)
for i, button_i in enumerate(self.button_lists):
x = button_i.pos().x()
y = button_i.pos().y()
w = button_i.width()
h = button_i.height()
r = QRect(x+w, y, self.triangleWidth, h)
#
# _____p1
# | \ p3
# |_____ /
# p2
point3X = x + w + self.triangleWidth
point3Y = y + h/2
point1X = x + w
point1Y = y
point2X = x + w
point2Y = y + h
path = QPainterPath()
path.moveTo(point1X, point1Y)
path.lineTo(point2X, point2Y)
path.lineTo(point3X, point3Y)
painter.setPen(QPen(Qt.NoPen))
if i != len(self.button_lists) - 1:
painter.fillRect(r, QBrush(self.colors[i+1]))
painter.fillPath(path, QBrush(self.colors[i]))
def main():
app = QApplication(sys.argv)
firstButtonX = 0
firstButtonY = 0
buttonWidth = 50
buttonHeight = 30
triangleWidth = 30
labels = ["step1", "step2", "step3"]
colors = [Qt.red, Qt.blue, Qt.yellow]
ex = Angle_Border(firstButtonX, firstButtonY, buttonWidth, buttonHeight, triangleWidth, labels, colors)
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Which gives:
You can use QTabBar and override its paint event.
For better display of the last tab, we also override the size hint functions in order to have enough space to show the last arrow without clipping it or drawing over the text.
class ArrowTabBar(QTabBar):
def sizeHint(self):
hint = super().sizeHint()
if self.count():
hint.setWidth(hint.width() + hint.height() * .2)
return hint
def minimumTabSizeHint(self, index):
hint = super().minimumTabSizeHint(index)
if index == self.count() - 1:
hint.setWidth(hint.width() + hint.height() * .2)
return hint
def tabSizeHint(self, index):
hint = super().tabSizeHint(index)
if index == self.count() - 1:
hint.setWidth(hint.width() + hint.height() * .2)
return hint
def paintEvent(self, event):
count = self.count()
if not count:
return
qp = QPainter(self)
qp.setRenderHint(qp.Antialiasing)
bottom = self.height()
midY = bottom // 2
midX = midY / 2.5
bottom -= 1
palette = self.palette()
textColor = palette.windowText().color()
normal = palette.mid()
current = palette.dark()
for i in range(count):
rect = self.tabRect(i)
path = QPainterPath()
x = rect.x()
right = rect.right()
if i:
path.moveTo(x - midX, bottom)
path.lineTo(x + midX, midY)
path.lineTo(x - midX, 0)
else:
path.moveTo(x, bottom)
path.lineTo(x, 0)
path.lineTo(right - midX, 0)
path.lineTo(right + midX, midY)
path.lineTo(right - midX, bottom)
if i == self.currentIndex():
qp.setBrush(current)
else:
qp.setBrush(normal)
qp.setPen(Qt.NoPen)
qp.drawPath(path)
qp.setPen(textColor)
qp.drawText(rect, Qt.AlignCenter|Qt.TextShowMnemonic,
self.tabText(i))
app = QApplication([])
panel = ArrowTabBar()
for i in range(5):
panel.addTab('Item {}'.format(i + 1))
panel.show()
app.exec()
I'd like to plot the horizontal distance between 2 points on an image with PyQtGraph, but I can't draw it.
I think it a way of doing this would be to use 3 instances of LineSegmentROI and make them look connected as one right arc, because they already have many features that would be great for this idea.
Like being draggable, which could be very useful to measure a different distance by simply dragging a side.
The problem are the handles, that can't be removed, or even hidden.
Has anyone done something like this?
# import the necessary packages
from pyqtgraph.graphicsItems.ImageItem import ImageItem
from pyqtgraph.graphicsItems.LinearRegionItem import LinearRegionItem
import requests
import numpy as np
import cv2
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
image = cv2.imread('example.png') # Change if you save the image with a different name
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
app = QtGui.QApplication([])
## Create window with GraphicsView widget
w = pg.GraphicsView()
w.show()
w.resize(image.shape[0], image.shape[1]) # Depending on the picture you may need to resize
w.setWindowTitle('Test')
view = pg.ViewBox()
view.setLimits(xMin=-image.shape[0]*0.05, xMax=image.shape[0]*1.05,
minXRange=100, maxXRange=2000,
yMin=-image.shape[1]*0.05, yMax=image.shape[1]*1.05,
minYRange=100, maxYRange=2000)
w.setCentralItem(view)
## lock the aspect ratio
view.setAspectLocked(True)
## Add image item
item = ImageItem(image)
view.addItem(item)
# Add Line
line = pg.QtGui.QGraphicsLineItem(200, -100, 400, -100, view)
line.setPen(pg.mkPen(color=(255, 0, 0), width=10))
view.addItem(line)
def mouseClicked(evt):
pos = evt[0]
print(pos)
proxyClicked = pg.SignalProxy(w.scene().sigMouseClicked, rateLimit=60, slot=mouseClicked)
## 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 ended up borrowing RectItem from drawing a rectangle in pyqtgraph and using its code for LineItem.
With three LineItems I draw the distance meter between the 2 points.
I still have to add some signals and slots to handle resizing, but I'm working on it.
However the core of the solution is here and I'll follow up with my improvements
# import the necessary packages
from PySide2.QtCore import QLineF, Qt, Signal, Slot, QObject, QPointF, QRectF, QSizeF
from PySide2.QtGui import QRegion
from PySide2.QtWidgets import QGraphicsItem, QLabel, QWidget
from pyqtgraph.graphicsItems.ImageItem import ImageItem
from pyqtgraph.graphicsItems.LinearRegionItem import LinearRegionItem
import numpy as np
import cv2
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui, QtWidgets
from pyqtgraph.graphicsItems.ViewBox.ViewBox import ViewBox
image = cv2.imread('image.jpg')
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
app = QtGui.QApplication([])
## Create window with GraphicsView widget
w = pg.GraphicsView()
w.show()
w.setWindowTitle('Test')
view = pg.ViewBox()
view.setLimits(xMin=0, xMax=image.shape[0],
minXRange=100, maxXRange=2000,
yMin=0, yMax=image.shape[1],
minYRange=100, maxYRange=2000)
w.setCentralItem(view)
## lock the aspect ratio
view.setAspectLocked(True)
## Add image item
item = ImageItem(image)
view.addItem(item)
class LineItem(pg.UIGraphicsItem):
moved = Signal(QPointF)
def __init__(self, line, extend=0, horizontal=False, parent=None):
super().__init__(parent)
self.initialPos = QLineF(line)
self._line = line
self.extend = extend
self.horizontal = horizontal
self._extendLine()
self.picture = QtGui.QPicture()
self._generate_picture()
self.setFlag(QGraphicsItem.ItemIsSelectable)
self.setFlag(QGraphicsItem.ItemIsMovable)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
#property
def line(self):
return self._line
def _extendLine(self):
if (self.extend != 0 and not self.horizontal):
self._line.setP1( QPointF(self._line.x1(), self._line.y1() - abs(self.extend)) )
# if (self.horizontal):
# self.extend = 0
# self._line.setP1( QPointF(self._line.x1(), self._line.y1() - abs(self.extend)) )
def _generate_picture(self):
painter = QtGui.QPainter(self.picture)
painter.setPen(pg.mkPen(color="y", width=2))
painter.drawLine(self.line)
painter.end()
def paint(self, painter, option, widget=None):
painter.drawPicture(0, 0, self.picture)
def boundingRect(self):
lineShape = self.picture.boundingRect()
lineShape.adjust(-10, -10, 10, 10)
return QtCore.QRectF(lineShape)
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange:
# value is the new position.
if self.horizontal:
if value.x() != 0:
value = QPointF(0, value.y())
else:
if value.y() != 0:
value = QPointF(value.x(), 0)
self.moved.emit(value)
return pg.UIGraphicsItem.itemChange(self, change, value)
class Distance(QObject):
def __init__(self, A: QPointF, B: QPointF, view: ViewBox, parent: QWidget=None):
super().__init__(parent)
self.A = A
self.B = B
if A.x() > B.x():
self.A, self.B = B, A
self.distance = abs(B.x() - A.x())
print(self.A)
print(self.B)
extend = 50
top = max(self.A.y(), self.B.y()) + 200
self.left = LineItem(QtCore.QLineF(self.A.x(), self.A.y(), self.A.x(), top), extend)
self.right = LineItem(QtCore.QLineF(self.B.x(), self.B.y(), self.B.x(), top), extend)
self.top = LineItem(QtCore.QLineF(self.A.x(), top, self.B.x(), top), horizontal=True)
self.top.setPos(0, 0)
self.left.moved.connect(self.onLeftSegmentMoved)
self.right.moved.connect(self.onRightSegmentMoved)
self.top.moved.connect(self.onTopSegmentMoved)
self.label = pg.TextItem(str(round(self.distance, 2)), color=(0xFF, 0xFF, 0x00), anchor=(1, 1))
# self.label.setParentItem(self.top)
self.label.setPos(self.A.x()+self.distance/2, top + 5)
view.addItem(self.label)
view.addItem(self.left)
view.addItem(self.top)
view.addItem(self.right)
#Slot(QPointF)
def onLeftSegmentMoved(self, delta: QPointF):
topLeft = self.top.initialPos.p1()
newX = topLeft.x() + delta.x()
newTopLeft = QPointF(newX, topLeft.y())
self.top.line.setP1(newTopLeft)
self.top._generate_picture()
pos = self.label.pos()
self.distance = abs(self.top.line.x2() - self.top.line.x1())
self.label.setPos(newX + (self.top.line.x2() - self.top.line.x1())/2, pos.y())
self.label.setText(str(round(self.distance, 2)))
#Slot(QPointF)
def onTopSegmentMoved(self, delta: QPointF):
leftTop = self.top.initialPos.p1()
newY = leftTop.y() + delta.y()
newLeftTop = QPointF(leftTop.x(), newY)
self.left.line.setP2(newLeftTop)
self.left._generate_picture()
rightTop = self.top.initialPos.p2()
newY = rightTop.y() + delta.y()
newRightTop = QPointF(rightTop.x(), newY)
self.right.line.setP2(newRightTop)
self.right._generate_picture()
pos = self.label.pos()
self.label.setPos(pos.x(), newY)
#Slot(QPointF)
def onRightSegmentMoved(self, delta: QPointF):
topRight = self.top.initialPos.p2()
newX = topRight.x() + delta.x()
newTopRight = QPointF(newX, topRight.y())
self.top.line.setP2(newTopRight)
self.top._generate_picture()
pos = self.label.pos()
self.distance = abs(self.top.line.x2() - self.top.line.x1())
self.label.setPos(newX - (self.top.line.x2() - self.top.line.x1())/2, pos.y())
self.label.setText(str(round(self.distance, 2)))
distance = Distance(QPointF(925, 425), QPointF(138, 500), view)
I'd like to use the wheelEvent to resize an image and place a QGraphicPixmap into a QGraphicsScene.
Before adding the original image, it is resized to around 1/3rd its original size. In the wheelEvent, I'm calling a function that will resize the original image and create a QImage to set the QGraphicsPixmap.
After adding the resized pixmap to the scene, the pixels that were originally under the cursor before the scale have shifted. I'm not sure which positions I need to be mapping to/from the scene to achieve this.
I've tried scaling the graphicsPixmap, scaling and translating the graphicsPixmap, scaling the view and translating the graphicsPixmap/setting an offset.
I clearly don't something about what's happening but I'm not sure what that is..
The WheelEvent below works perfectly until maybe_resize is called.
Depending on the size of the current image in the viewer the maybe_resize method will either resize the current ndarray image, create a new qimage and set a new pixmap in the graphicPixmap, or it exits the method without resizing.
If you run the code as is, the pixmap is in the same place under the cursor, but if you uncomment maybe_resize this is no longer the case.
from PyQt5.QtCore import QRectF, QSize, Qt, pyqtSignal
import cv2
import numpy as np
from PyQt5.QtCore import QRectF, QSize, Qt, pyqtSignal
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import (QApplication,
QFrame,
QGraphicsPixmapItem,
QGraphicsScene,
QGraphicsView,
QMainWindow,
QSizePolicy)
class GraphicsView(QGraphicsView):
def __init__(self, parent):
super(GraphicsView, self).__init__(parent)
self.pixmap = QPixmap()
self._zoom_level = 0
self._scene = Scene(self)
self.setScene(self._scene)
self.gpm = QGraphicsPixmapItem()
self._scene.addItem(self.gpm)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setFrameShape(QFrame.NoFrame)
self.has_image = False
def maybe_resize(self, factor):
self.resize_requested(factor)
def read_image(self, path):
self.base_image = cv2.imread(path, -1)
self._original_res = self.base_image.shape
h, w = self.base_image.shape[0], self.base_image.shape[1]
self.resized_image = cv2.resize(self.base_image, (w // 4, h // 4))
self.has_image = True
self.set_image(self.resized_image)
return self.resized_image
def resize_requested(self, factor):
factor = max(1. * (self._zoom_level * factor), 1)
h = int(self.resized_image.shape[0] * factor)
w = int(self.resized_image.shape[1] * factor)
src = cv2.resize(self.base_image, (w, h))
dst = np.ndarray(src.shape, src.dtype)
dst[:, :, :] = src
self.set_image(dst)
def wheelEvent(self, event):
factor = 1.1
if event.angleDelta().y() < 0:
factor = 0.9
self._zoom_level-=1
else:
self._zoom_level+=1
view_pos = event.pos()
scene_pos = self.mapToScene(view_pos)
self.centerOn(scene_pos)
self.scale(factor, factor)
delta = self.mapToScene(view_pos) - self.mapToScene(self.viewport().rect().center())
self.centerOn(scene_pos - delta)
# self.maybe_resize(factor)
def set_image(self, img):
if not self.has_image:
return
shape = img.shape
w = shape[1]
h = shape[0]
self._image = img
q_img_format = QImage.Format_RGB888
try:
bands = shape[2]
except IndexError:
bands = 1
q_img = QImage(img, w, h, w * bands, q_img_format)
self.pixmap = self.pixmap.fromImage(q_img)
self.setSceneRect(QRectF(self.pixmap.rect()))
self.gpm.setPixmap(self.pixmap)
class Scene(QGraphicsScene):
zoom_changed = pyqtSignal(float)
def __init__(self, parent=None):
super(Scene, self).__init__(parent)
class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.gv = GraphicsView(self)
self.setCentralWidget(self.gv)
def load_image(self, path):
self.gv.read_image(path)
def sizeHint(self):
return QSize(800, 800)
if __name__ == "__main__":
app = QApplication([])
w = Window()
w.load_image('test.jpg')
w.show()
app.exit(app.exec_())
I want to create a grid of buttons with images. The image on each button is a piece from a bigger image which is cutted into pieces - so the button grid can be seen as tiles of the original image (the code i used to put a image on the button is from here).
To cut the image (image=QPixmap(path_to_image)) into pieces i used the methodimage.copy(x, y, width, height) in a for loop.
At first i thought that the code works fine. A more precise check shows that the image pieces have overlapping parts.
Here my code which was tested with an GIF-Image:
import os
import sys
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtWidgets import \
QWidget, QApplication, \
QGridLayout, \
QLabel, QPushButton
def cut_image_into_tiles(image, rows=4, cols=4) -> dict:
if isinstance(image, str) and os.path.exists(image):
image = QPixmap(image)
elif not isinstance(image, QPixmap):
raise ValueError("image must be str or QPixmap object")
# Dim of the tile
tw = int(image.size().width() / cols)
th = int(image.size().height() / rows)
# prepare return value
tiles = {"width": tw, "height": th}
for r in range(rows):
for c in range(cols):
tile = image.copy(c * th, r * tw, tw, th)
# args: x, y, width, height
# https://doc.qt.io/qt-5/qpixmap.html#copy-1
tiles[(r, c)] = tile
return tiles
def create_pixmapbutton(pixmap, width=0, height=0) -> QPushButton:
if isinstance(pixmap, QPixmap):
button = QPushButton()
if width > 0 and height > 0:
button.setIconSize(QSize(width, height))
else:
button.setIconSize(pixmap.size())
button.setIcon(QIcon(pixmap))
return button
def run_viewer(imagepath, rows=4, cols=4):
# ImageCutter.run_viewer(imagepath)
app = QApplication(sys.argv)
image = QPixmap(imagepath)
tiles = cut_image_into_tiles(image=image, rows=rows, cols=cols)
tilewidth = tiles["width"]
tileheight = tiles["height"]
# get dict tiles (keys:=(row, col) or width or height)
viewer = QWidget()
layout = QGridLayout()
viewer.setLayout(layout)
viewer.setWindowTitle("ImageCutter Viewer")
lbl = QLabel()
lbl.setPixmap(image)
layout.addWidget(lbl, 0, 0, rows, cols)
for r in range(rows):
for c in range(cols):
btn = create_pixmapbutton(
tiles[r, c], width=tilewidth, height=tileheight)
btninfo = "buttonsize={}x{}".format(tilewidth, tileheight)
btn.setToolTip(btninfo)
# logger.debug(" create button [{}]".format(btninfo))
layout.addWidget(btn, r, cols + c)
viewer.show()
sys.exit(app.exec_())
I tested the code with run_viewer(path_to_a_gif_image, rows=3, cols=3)
The width of each grid must depend on the width of the image, in your case it depends on th, but th depends on the height which is incorrect, you must change th for tw, the same for the height.
Considering the above, the solution is:
import os
import sys
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtWidgets import QWidget, QApplication, QGridLayout, QLabel, QPushButton
def cut_image_into_tiles(image, rows=4, cols=4) -> dict:
if isinstance(image, str) and os.path.exists(image):
image = QPixmap(image)
elif not isinstance(image, QPixmap):
raise ValueError("image must be str or QPixmap object")
# Dim of the tile
tw = int(image.size().width() / cols)
th = int(image.size().height() / rows)
# prepare return value
tiles = {"width": tw, "height": th}
for r in range(rows):
for c in range(cols):
tile = image.copy(c * tw, r * th, tw, th) # <----
# args: x, y, width, height
# https://doc.qt.io/qt-5/qpixmap.html#copy-1
tiles[(r, c)] = tile
return tiles
def create_pixmapbutton(pixmap, width=0, height=0) -> QPushButton:
if isinstance(pixmap, QPixmap):
button = QPushButton()
if width > 0 and height > 0:
button.setIconSize(QSize(width, height))
else:
button.setIconSize(pixmap.size())
button.setIcon(QIcon(pixmap))
button.setContentsMargins(0, 0, 0, 0)
button.setFixedSize(button.iconSize())
return button
def run_viewer(imagepath, rows=4, cols=4):
# ImageCutter.run_viewer(imagepath)
app = QApplication(sys.argv)
image = QPixmap(imagepath)
tiles = cut_image_into_tiles(image=image, rows=rows, cols=cols)
tilewidth = tiles["width"]
tileheight = tiles["height"]
# get dict tiles (keys:=(row, col) or width or height)
viewer = QWidget()
layout = QGridLayout(viewer)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
viewer.setWindowTitle("ImageCutter Viewer")
lbl = QLabel()
lbl.setPixmap(image)
layout.addWidget(lbl, 0, 0, rows, cols)
for r in range(rows):
for c in range(cols):
btn = create_pixmapbutton(tiles[r, c], width=tilewidth, height=tileheight)
btninfo = "buttonsize={}x{}".format(tilewidth, tileheight)
btn.setToolTip(btninfo)
# logger.debug(" create button [{}]".format(btninfo))
layout.addWidget(btn, r, cols + c)
viewer.show()
viewer.setFixedSize(viewer.sizeHint())
sys.exit(app.exec_())
run_viewer("linux.gif", 3, 3)
Try it:
import sys
from PyQt5.QtCore import QSize, QRect
from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtWidgets import (QWidget, QApplication, QGridLayout,
QLabel, QPushButton, QSizePolicy)
from PIL import Image
def cut_image_into_tiles(image, r=4, c=4):
im = Image.open(image)
x, y = im.size
for i in range(r):
for j in range(c):
if i!=r and j!=c:
im.crop(box=(x/r*i, y/c*j, x/r*(i+1)-1, y/c*(j+1)-1)).\
save('image{}{}.png'.format(str(i+1), str(j+1)))
def run_viewer(imagepath, rows=4, cols=4):
app = QApplication(sys.argv)
image = QPixmap(imagepath)
tiles = cut_image_into_tiles(image=imagepath, r=rows, c=cols)
viewer = QWidget()
layout = QGridLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
viewer.setLayout(layout)
viewer.setWindowTitle("ImageCutter Viewer")
lbl = QLabel()
lbl.setPixmap(image)
layout.addWidget(lbl, 0, 0, rows, cols)
for r in range(1, rows+1):
for c in range(1, cols+1):
button = QPushButton()
button.setStyleSheet('''QPushButton {background-color: yellow; border: 1px solid black;}
QPushButton:pressed {background-color: green;}''')
image = QPixmap(f"image{c}{r}.png")
button.setIconSize(image.size())
button.setIcon(QIcon(image))
layout.addWidget(button, r-1, c+cols)
viewer.show()
sys.exit(app.exec_())
run_viewer("linux.gif", rows=3, cols=3)
Ok, i discovered that for big images and big grids the images on the buttons on the right can be smaller than the others. The same at the bottom buttons.
An offset at the top and on the left will prevent this.
So the function def cut_image_into_tiles(image, rows=4, cols=4) -> dict: must be adapted to:
def cut_image_into_tiles(image, rows=4, cols=4) -> dict:
if isinstance(image, str) and os.path.exists(image):
image = QPixmap(image)
elif not isinstance(image, QPixmap):
raise ValueError("image must be str or QPixmap object")
# Dim of tiled images
width = image.size().width()
height = image.size().height()
tw = int(width / cols)
th = int(height / rows)
offset_w = int((width - tw * cols)/2) # !!!
offset_h = int((height - th * rows)/2) # !!!
# prepare return value
tiles = {"width": tw, "height": th}
for r in range(rows):
for c in range(cols):
x = c * tw
y = r * th
if c == 0:
x += offset_w # !!!
if r == 0:
y += offset_h # !!!
tile = image.copy(x, y, tw, th)
# args: x, y, width, height
# https://doc.qt.io/qt-5/qpixmap.html#copy-1
tiles[(r, c)] = tile
return tiles
The whole code is now:
def cut_image_into_tiles(image, rows=4, cols=4) -> dict:
if isinstance(image, str) and os.path.exists(image):
image = QPixmap(image)
elif not isinstance(image, QPixmap):
raise ValueError("image must be str or QPixmap object")
# Dim of tiled images
width = image.size().width()
height = image.size().height()
tw = int(width / cols)
th = int(height / rows)
offset_w = int((width - tw * cols)/2)
offset_h = int((height - th * rows)/2)
# prepare return value
tiles = {"width": tw, "height": th}
for r in range(rows):
for c in range(cols):
x = c * tw
y = r * th
if c == 0:
x += offset_w
if r == 0:
y += offset_h
tile = image.copy(x, y, tw, th)
# args: x, y, width, height
# https://doc.qt.io/qt-5/qpixmap.html#copy-1
tiles[(r, c)] = tile
return tiles
def create_pixmapbutton(pixmap, width=0, height=0) -> QPushButton:
if isinstance(pixmap, QPixmap):
button = QPushButton()
if width > 0 and height > 0:
button.setIconSize(QSize(width, height))
else:
button.setIconSize(pixmap.size())
button.setIcon(QIcon(pixmap))
return button
def run_viewer(imagepath, rows=4, cols=4):
# ImageCutter.run_viewer(imagepath)
app = QApplication(sys.argv)
image = QPixmap(imagepath)
tiles = cut_image_into_tiles(image=image, rows=rows, cols=cols)
tilewidth = tiles["width"]
tileheight = tiles["height"]
# get dict tiles (keys:=(row, col) or width or height)
viewer = QWidget()
layout = QGridLayout()
viewer.setLayout(layout)
viewer.setWindowTitle("ImageCutter Viewer")
lbl = QLabel()
lbl.setPixmap(image)
layout.addWidget(lbl, 0, 0, rows, cols)
for r in range(rows):
for c in range(cols):
btn = create_pixmapbutton(
tiles[r, c], width=tilewidth, height=tileheight)
btninfo = "buttonsize={}x{}".format(tilewidth, tileheight)
btn.setToolTip(btninfo)
# logger.debug(" create button [{}]".format(btninfo))
layout.addWidget(btn, r, cols + c)
viewer.show()
sys.exit(app.exec_())
I've been trying to create an interactive OpenCV image viewer where I'll be able to view the image immediately after a manipulation. Like say, I'm applying a binary thresholding operation on an image and changing the threshold value from PyQt slider. Now, I want to see each thresholded image in the image viewer.
I've created a very basic program for this purpose using python OpenCV and PyQT5 lib. But, the image is not being updated in the QLabel.
Below is my code:
import sys
import cv2
import numpy as np
import imutils
from PyQt5 import QtCore
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QVBoxLayout, QLCDNumber, QSlider, QLabel, QCheckBox
from PyQt5.QtGui import QPixmap, QImage
class MyWindow(QWidget):
def __init__(self):
super().__init__()
self.imglabel = QLabel(self)
self.imglabel.setFixedSize(1200, 900)
ori_img = cv2.imread("../resources/omr-1-ans-ori.png", cv2.IMREAD_COLOR)
ori_img = imutils.resize(ori_img, height=960)
self.gray_img = cv2.cvtColor(ori_img, cv2.COLOR_BGR2GRAY)
self.gray_img_c = ori_img
self.thresh = False
self.thresh_karnel_size = 11
self.init_ui()
def init_ui(self):
# lcd = QLCDNumber(self)
hbox1 = QHBoxLayout()
cb_thresh = QCheckBox('thresh', self)
cb_thresh.setChecked(False)
cb_thresh.stateChanged.connect(self.changeTitleThresh)
hbox1.addWidget(cb_thresh)
thresh_slider = QSlider(Qt.Horizontal, self)
thresh_slider.setFocusPolicy(Qt.StrongFocus)
thresh_slider.setTickPosition(QSlider.TicksBothSides)
thresh_slider.setTickInterval(1)
thresh_slider.setSingleStep(1)
thresh_slider.setPageStep(1)
thresh_slider.setMinimum(1)
thresh_slider.setMaximum(127)
thresh_slider.valueChanged[int].connect(self.threshSliderChangeValue)
vbox = QVBoxLayout()
vbox.addLayout(hbox1)
vbox.addWidget(thresh_slider)
vbox.addWidget(self.imglabel)
self.setLayout(vbox)
self.setGeometry(50, 50, 1200, 768)
self.setWindowTitle('Learning PyQT5')
self.updateImage()
self.show()
def changeTitleThresh(self, state):
# print("thresh checkbox: ", state, Qt.Checked)
if state == Qt.Checked:
self.thresh = True
else:
self.thresh = False
def threshSliderChangeValue(self, value):
ksize = (value * 2) + 1
print("ksize: ", ksize)
if ksize > 1 and ksize % 2 != 0 and self.thresh:
self.thresh_karnel_size = ksize
self.gray_img = cv2.threshold(self.gray_img, self.thresh_karnel_size, 255, cv2.THRESH_BINARY)[1]
self.gray_img_c = cv2.cvtColor(self.gray_img.copy(), cv2.COLOR_GRAY2BGR)
self.updateImage()
def updateImage(self):
height, width, channel = self.gray_img_c.shape
bytesPerLine = 3 * width
qImg = QImage(self.gray_img_c.data, width, height, bytesPerLine, QImage.Format_RGB888)
pixMap = QPixmap.fromImage(qImg)
pixMap = pixMap.scaled(700, 500, Qt.KeepAspectRatio)
self.imglabel.setPixmap(pixMap)
self.imglabel.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MyWindow()
sys.exit(app.exec_())
I've tried every solution found through google search. But, could not fix it.
Any help or hint will be much appreciated.
The original image must remain intact but you are applying the filter and modifying it every time, in the following example I show the correct way to do it
import sys
import cv2
import imutils
from PyQt5 import QtCore, QtGui, QtWidgets
class MyWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
ori_img = cv2.imread("../resources/omr-1-ans-ori.png", cv2.IMREAD_COLOR)
self.original_image_color = imutils.resize(ori_img, height=960)
self.original_image_gray = cv2.cvtColor(self.original_image_color, cv2.COLOR_BGR2GRAY)
self.thresh = False
self.thresh_karnel_size = 11
self.init_ui()
def init_ui(self):
self.imglabel = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter)
self.imglabel.setFixedSize(1200, 900)
cb_thresh = QtWidgets.QCheckBox('thresh', checked=False)
cb_thresh.stateChanged.connect(self.changeTitleThresh)
self.thresh_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal,
focusPolicy=QtCore.Qt.StrongFocus,
tickPosition=QtWidgets.QSlider.TicksBothSides,
tickInterval=1,
singleStep=1,
pageStep=1,
minimum=1,
maximum=127)
self.thresh_slider.valueChanged[int].connect(self.threshSliderChangeValue)
vbox = QtWidgets.QVBoxLayout(self)
vbox.addWidget(cb_thresh)
vbox.addWidget(self.thresh_slider)
vbox.addWidget(self.imglabel)
self.threshSliderChangeValue(self.thresh_slider.value())
self.setGeometry(50, 50, 1200, 768)
self.setWindowTitle('Learning PyQT5')
self.show()
#QtCore.pyqtSlot(int)
def changeTitleThresh(self, state):
self.thresh = state == QtCore.Qt.Checked
self.threshSliderChangeValue(self.thresh_slider.value())
#QtCore.pyqtSlot(int)
def threshSliderChangeValue(self, value):
ksize = (value * 2) + 1
if ksize > 1 and ksize % 2 != 0 and self.thresh:
self.thresh_karnel_size = ksize
_, gray_img = cv2.threshold(self.original_image_gray, self.thresh_karnel_size, 255, cv2.THRESH_BINARY)
gray_img_c = cv2.cvtColor(gray_img.copy(), cv2.COLOR_GRAY2BGR)
self.updateImage(gray_img_c)
else:
self.updateImage(self.original_image_color)
def updateImage(self, image):
height, width, channel = image.shape
bytesPerLine = 3 * width
qImg = QtGui.QImage(image.data, width, height, bytesPerLine, QtGui.QImage.Format_RGB888)
pixMap = QtGui.QPixmap.fromImage(qImg).scaled(700, 500, QtCore.Qt.KeepAspectRatio)
self.imglabel.setPixmap(pixMap)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ex = MyWindow()
sys.exit(app.exec_())