New to PyQt and I'm having an issue rotating a QGraphicsEllipseItem. I want the ellipse to rotate around the center of the ellipse instead of the corner of the QRectF used to define the ellipse. My code looks like this (sorry, the computer I am coding it on, doesn't have internet access, so I am copying the relevant parts here by hand):
self.scene = QtGui.QGraphicsScene()
self.ui.graphicsView.setScene(self.scene)
pen = QtGui.QPen(QColor(Qt.yellow))
# Draw first Ellipse
# This code correctly places a yellow ellipse centered at the scene 500,500 point
ellipse1 = QtGui.QGraphicsEllipseItem(0,0,100,10)
ellipse1.setPen(pen)
self.scene.addItem(ellipse1)
ellipse1.setPos(500, 500)
ellipse1.translate(-50, -5)
# Now, try to draw a rotated ellipse
# This code rotates the ellipse about the 0,0 location of the rectangle
# which is the scene 450, 495 point, not the center of the ellipse
ellipse2 = QtGui.QGraphicsEllipseItem(0,0,100,10)
ellipse2.setPen(pen)
self.scene.addItem(ellipse2)
ellipse2.setPos(500, 500)
ellipse2.translate(-50, -5)
ellipse2.rotate(45.0)
OK, that is basically what I expected. Since QGraphicsEllipseItem is derived from QGraphicsItem, I tried to set the transform origin point for ellipse2 before the rotation:
ellipse2 = QtGui.QGraphicsEllipseItem(0,0,100,10)
ellipse2.setPen(pen)
self.scene.addItem(ellipse2)
ellipse2.setPos(500, 500)
ellipse2.translate(-50, -5)
ellipse2.setTransformOriginPoint(450, 495)
ellipse2.rotate(45.0)
This results in the error "AttributeError: 'QGraphicsEllipseItem' object has no attribute 'setTransformOriginPoint'
Obviously, I'm doing something wrong or making an incorrect assumption about QGraphicsEllipseItem. Some sites hint that I may need to use a bounding rectangle in order to do the rotation, but I don't understand how to do that.
If someone could show me the correct way to rotate an ellipse about its center in pyqt, I would greatly appreciate it!!!
Ok, so after many weeks I was able to find my own answer although I don't really really understand why it works. My standard method of programming by brail. Anyway, the code should look like this:
transform = QtGui.QTransform()
ellipse = QtGui.QGraphicsEllipseItem(0,0,100,10)
ellipse.setPen(pen)
ellipse.setPos(500, 500)
transform.rotate(-45.0) # rotate the negative of the angle desired
transform.translate((-50, -5) # center of the ellipse
ellipse.setTansform(transform)
self.scene.addItem(ellipse)
So this successfully places the center of the rotated ellipse at the point 500,500. I'm not sure why you would take the negative of the angle you want to rotate, but it seems to work. If anyone can explain why it works this, I would appreciate it.
I got the same problem and spent two whole days to solve it.This is my solution:
First of all you should define the coordinates(x,y) of the point around which the ellipse should rotate, this way:
ellipse.setTransformOriginPoint(QPointF(?, ?))
then you can use the setRotation() to rotate ellipse
the whole code can be seen below:
__author__ = 'shahryar_slg'
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class MainWindow(QDialog):
def __init__(self):
super(QDialog, self).__init__()
self.view = QGraphicsView()
self.scene = QGraphicsScene()
self.layout = QGridLayout()
self.layout.addWidget(self.view, 0, 0)
self.view.setScene(self.scene)
self.setLayout(self.layout)
self.ellipse = QGraphicsEllipseItem(10, 20, 100, 60)
self.ellipse.setTransformOriginPoint(QPointF(100/2+10, 60/2+20))
self.ellipse.setRotation(-60)
self.scene.addItem(self.ellipse)
# I created another ellipse on the same position to show you
# that the first one is rotated around it's center:
self.ellipse2 = QGraphicsEllipseItem(20, 20, 100, 40)
self.scene.addItem(self.ellipse2)
self.update()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
Pay attention to the way I've calculated the center of the ellipse.
Related
I have a code here that will add an ellipse and line when mouse is clicked.
class Viewer(QtWidgets.QGraphicsView):
def __init__(self, parent):
super(leftImagePhotoViewer, self).__init__(parent)
self._zoom = 0
self._empty = True
self._scene = QtWidgets.QGraphicsScene(self)
self.setGeometry(QtCore.QRect(20, 90, 451, 421))
self.setSceneRect(20, 90, 451, 421)
I have an MouseRelase Event
def mouseReleaseEvent(self,event):
pos = self.mapToScene(event.pos())
point = self._scene.addEllipse(self._size/2, self._size/2, 10, 10, QPen(Qt.black), QBrush(Qt.green))
point.setPos(QPointF(pos.x(),pos.y()))
self._scene.addLine(pos.x(),pos.y(), self.posprev.x(), self.posprev.y(), QPen(Qt.green))
When I clicked the line, its position is similar to the mouse positon, but the ellipse positon has few gap or difference to the exact mouse position.The center of the ellipse should be the endpoitn of the line or where the mouse position is.
See image here:
Can someone help me what is wrong why the ellipse will not add on the exact position to the mouse?
As the documentation of addEllipse() explains:
Note that the item's geometry is provided in item coordinates, and its position is initialized to (0, 0).
This is actually valid for all QGraphicsScene functions that add basic shapes, and the initialized position is always (0, 0) for all QGraphicsItems in general.
Consider the following:
point = scene.addEllipse(5, 5, 10, 10)
The above will create an ellipse enclosed in a rectangle that starts at (5, 5) relative to its position. Since we've not moved it yet, that position is the origin point of the scene.
The ellipse as it as soon as it's created, with the rectangle shown as a reference of its boundaries.
Then, we set its position (assuming the mouse is at 20, 20 of the scene):
point.setPos(QPointF(20, 20))
The result will be an ellipse enclosed in a rectangle that has its top left corner at (25, 25), which is the rectangle position relative to the item position: (5, 5) + (20, 20).
Note that the above shows both the ellipse in the original position and the result of setPos().
If you want an ellipse that will be centered on its position, you must create one with negative x and y coordinates that are half of the width and height of its rectangle.
Considering the case above, the following will properly show the ellipse centered at (20, 20):
point = scene.addEllipse(-5, -5, 10, 10)
point.setPos(QPointF(20, 20))
Notes:
as the documentation shows, mapToScene() already returns a QPointF, there's no point in doing setPos(QPointF(pos.x(), pos.y())): just do setPos(pos);
remember what said above: all items have a starting position at (0, 0); this is valid also for the line you're creating after that point, which will be drawn between pos and self.posprev, but will still be at (0, 0) in scene coordinates;
the view and the scene might need mouse events, especially if you're going to add movable items; you should always call the base implementation (in your case, super().mouseReleaseEvent(event)) when you override functions, unless you really know what you're doing;
as already suggested to you, it is of utmost importance that you read and understand the whole graphics view documentation, especially how its coordinate system works; the graphics view framework is as much powerful as it is complex, and cannot be learnt just by trial and error: being able to use it requires a lot of patience in understanding how it works by carefully studying the documentation of each of its classes and all functions you are going to use;
I'm trying to make a tool for my lab for manual image registration--where the user can select some points on two different images to align them. I made this in matplotlib, but zooming in/out was way too slow (I think because the images we're aligning are pretty high res). Is there a good way to do that in pyqtgraph? I just need to be able to select points on two image plots side by side and display where the point selections were.
Currently I have the images in ImageViews and I tried doing it with imv.scene.sigMouseClicked.connect(mouse_click), but in mouse_click(evt) evt.pos(), evt.scenePos(), and evt.screenPos() all gave coordinates that weren't in the image's coordinates. I also played around with doing the point selection with ROI free handles (since I could get the correct coordinates from those), but it doesn't seem like you could color the handles, which isn't a total deal-breaker I was wondering if there was a better option. Is there a better way to do this?
Edit:
The answer was great, I used it to make this pile of spaghetti:
https://github.com/xkstein/ManualAlign
Figured I'd like it in case someone was looking for something similar and didn't want to hassle with coding a new one from scratch.
Your question is unclear about how you want the program to match the points, here I provide a simple solution to allow you (1) Show an image. (2) Add points to the image.
The basic idea is to use a pg.GraphicsLayoutWidget, then add a pg.ImageItem and a pg.ScatterPlotItem, and each mouse click adds a point to the ScatterPlotItem. Code:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QHBoxLayout
import pyqtgraph as pg
import cv2
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
class ImagePlot(pg.GraphicsLayoutWidget):
def __init__(self):
super(ImagePlot, self).__init__()
self.p1 = pg.PlotItem()
self.addItem(self.p1)
self.p1.vb.invertY(True) # Images need inverted Y axis
# Use ScatterPlotItem to draw points
self.scatterItem = pg.ScatterPlotItem(
size=10,
pen=pg.mkPen(None),
brush=pg.mkBrush(255, 0, 0),
hoverable=True,
hoverBrush=pg.mkBrush(0, 255, 255)
)
self.scatterItem.setZValue(2) # Ensure scatterPlotItem is always at top
self.points = [] # Record Points
self.p1.addItem(self.scatterItem)
def setImage(self, image_path, size):
self.p1.clear()
self.p1.addItem(self.scatterItem)
# pg.ImageItem.__init__ method takes input as an image array
# I use opencv to load image, you can replace with other packages
image = cv2.imread(image_path, 1)
# resize image to some fixed size
image = cv2.resize(image, size)
self.image_item = pg.ImageItem(image)
self.image_item.setOpts(axisOrder='row-major')
self.p1.addItem(self.image_item)
def mousePressEvent(self, event):
point = self.p1.vb.mapSceneToView(event.pos()) # get the point clicked
# Get pixel position of the mouse click
x, y = int(point.x()), int(point.y())
self.points.append([x, y])
self.scatterItem.setPoints(pos=self.points)
super().mousePressEvent(event)
if __name__ == "__main__":
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
app = QApplication([])
win = QMainWindow()
central_win = QWidget()
layout = QHBoxLayout()
central_win.setLayout(layout)
win.setCentralWidget(central_win)
image_plot1 = ImagePlot()
image_plot2 = ImagePlot()
layout.addWidget(image_plot1)
layout.addWidget(image_plot2)
image_plot1.setImage('/home/think/image1.png', (310, 200))
image_plot2.setImage('/home/think/image2.jpeg', (310, 200))
# You can access points by accessing image_plot1.points
win.show()
if (sys.flags.interactive != 1) or not hasattr(Qt.QtCore, "PYQT_VERSION"):
QApplication.instance().exec_()
The result looks like:
Similar questions have been asked a lot for Android, but so far I haven´t been able to find resources related to Windows OS. So basically, as the topic suggests, I would like to draw a rectangle on my camera preview. Some work has been done, but there´s still some problem in my program. Due to some limits, I would like to avoid using opencv as much as possible. Following is my approach:
Open Window´s built-in camera app
Run Python code that draws rectangle on screen, pixel by pixel (see below)
Click on screen with mouse to move rectangle with its upper-left corner
As you can see in the code, I´m not actually drawing on the camera preview but rather drawing on my screen, where the camer preview runs on one layer lower.
Here´s the python code:
import win32gui, win32ui, win32api, win32con
from win32api import GetSystemMetrics
dc = win32gui.GetDC(0)
dcObj = win32ui.CreateDCFromHandle(dc)
hwnd = win32gui.WindowFromPoint((0,0))
monitor = (0, 0, GetSystemMetrics(0), GetSystemMetrics(1))
red = win32api.RGB(255, 0, 0) # Red
past_coordinates = monitor
rec_x = 200 # width of rectangle
rec_y = 100 # height of rectangle
m = (100, 100) # initialize start coordinate
def is_mouse_down():
key_code = win32con.VK_LBUTTON
state = win32api.GetAsyncKeyState(key_code)
return state != 0
while True:
if(is_mouse_down() == True):
m = win32gui.GetCursorPos()
for x in range(rec_x):
win32gui.SetPixel(dc, m[0]+x, m[1], red)
win32gui.SetPixel(dc, m[0]+x, m[1]+rec_y, red)
for y in range(rec_y):
win32gui.SetPixel(dc, m[0], m[1]+y, red)
win32gui.SetPixel(dc, m[0]+rec_x, m[1]+y, red)
As a result, I´m able to draw a red rectangle. However, because the screen is constantly being refreshed, the two horizontal lines of my rectangle (see gif below) are shown as running dots that go from left to right. I can´t find or think of a way to improve this, whilst keeping the possibility to move the rectangle around per ckick.
PS. Ignore white rectangle. It´s a built-in thing of the camera app when you click anywhere on the preview.
Here are the references I used to get to this step:
How to draw an empty rectangle on screen with Python
https://python-forum.io/Thread-How-to-trigger-a-function-by-clicking-the-left-mouse-click
Here is the simple code that draws letters using PyQt5.
setPos is 0, 0 but letters not at the top of the window.
Horizontally letters not at the window edge too.
What is wrong?
Thank you
from PyQt5 import QtWidgets, QtGui, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtGui import QBrush, QColor
import sys
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.initWindow()
def initWindow(self):
self.setGeometry(200,200,1000,400)
self.show()
self.scene = QtWidgets.QGraphicsScene()
self.graphics = QtWidgets.QGraphicsView(self.scene, self)
self.graphics.setGeometry(0, 0, 1000, 400)
self.scene.setSceneRect(0, 0, 1000, 400)
self.graphics.setHorizontalScrollBarPolicy(1)
self.graphics.setVerticalScrollBarPolicy(1)
self.graphics.setFrameStyle(0)
text = QtWidgets.QGraphicsSimpleTextItem()
_font = QtGui.QFont()
_font.setPixelSize(200)
_font.setBold(False)
text.setFont(_font)
text.setText('DDD')
text.setBrush(QBrush(QColor(0,0,0)))
text.setPos(0, 0)
self.scene.addItem(text)
self.graphics.show()
app = QApplication(sys.argv)
win = MainWindow()
sys.exit(app.exec())
As the QGraphicsSimpleTextItem documentation explains, the positioning of the text is based on the font "bounding rectangle":
Each character symbol of a font uses a "baseline" for reference, an "ascent" (how much the font goes from the baseline to the top) and a descent (how much it goes down for letters like lowercase "g" or "q"). Also, most characters do not start exactly on the "left" 0-point, but there is always some margin of "horizontal margin".
If you want to position it exactly at the top-left, you'll need to use QFontMetrics, which provides specific metrics information about a font; also, since we're dealing with a QGraphicsScene, QFontMetricsF is more indicated, as it returns floating point precision. The most important function for this is tightBoundingRect().
If I add the following to your code, just after adding the text item:
outRect = self.scene.addRect(text.boundingRect())
outRect.setPen(QtCore.Qt.red)
fm = QtGui.QFontMetricsF(_font)
boundingRect = fm.tightBoundingRect(text.text())
self.scene.addRect(boundingRect.translated(0, text.boundingRect().bottom() - fm.descent()))
The result is clear (I used a different string to better show the differences):
The red rectangle indicates the actual bounding rectangle of the item (which has the same size of QFontMetrics(_font).boundingRect(QtCore.QRect(), QtCore.Qt.AlignCenter, text.text()) would return.
The black rectangle shows the "tight" bounding rectangle, which is the smallest possible rectangle for that string.
The tight rectangle (as the one provided by QFontMetrics.boundingRect) uses the baseline as the 0-point for the coordinates, so it will always have a negative y position and probably (but it depends on the font and the style) an x greater than 0.
Finally, to get your text item placed with the characters aligned on the top left corner of the scene, you'll need to compute its position based on that tight rectangle:
text.setPos(-boundingRect.left(), -(fm.ascent() + boundingRect.top()))
The left has to be negative to compensate the horizontal text positioning, while the negative vertical position is computed by adding the ascent to the boundingRect top (which is negative in turn).
Keep in mind, though, that the bounding rectangle will still be the bigger red rectangle shown before (obviously translated to the new position):
So, while the text appears aligned on the top left, the item bounding rect top-left corner is actually outside the scene rectangle, and its bottom-left corner exceedes its visual representation.
This is important, as it has to do with item collision detection and mouse button interaction.
To avoid that, you'd need to subclass QGraphicsSimpleTextItem and override boundingRect(), but keep in mind that positioning (expecially vertical) should always be based on the baseline (the ascent), otherwise you'll probably get unexpected or odd behavior if the text changes.
I've solved the problem of pyqtgraph displaying datetime string on the X-axis, but many datetime string will cause overlap.
How to tilt the datetime string displayed on the pyqtgraph X axis to avoid overlapping across the display,I am deeply indebted to you for this help
self.topFiller = QtWidgets.QWidget()
self.topFiller.setMinimumSize(2000, 9000)
graphicsView = QtWidgets.QGraphicsView(self.topFiller)
y1=[100,2,8,5,9,6,1,3,9,11,13,1,1,2,8,5,9,6,1,3,9,11,13,1]
x1=["2019-7-1\r\n11:11:11","2019-7-1\r\n12:11:11","2019-7-1\r\n13:11:11","2019-7-1\r\n14:11:11","2019-7-1\r\n15:11:11","2019-7-1\r\n16:11:11","2019-7-1\r\n11:11:11","2019-7-1\r\n12:11:11","2019-7-1\r\n13:11:11","2019-7-1\r\n14:11:11","2019-7-1\r\n15:11:11","2019-7-1\r\n16:11:11","2019-7-1\r\n11:11:11","2019-7-1\r\n12:11:11","2019-7-1\r\n13:11:11","2019-7-1\r\n14:11:11","2019-7-1\r\n15:11:11","2019-7-1\r\n16:11:11","2019-7-1\r\n11:11:11","2019-7-1\r\n12:11:11","2019-7-1\r\n13:11:11","2019-7-1\r\n14:11:11","2019-7-1\r\n15:11:11","2019-7-1\r\n16:11:11"]
xdict1=dict(enumerate(x1))
stringaxis1 = pg.AxisItem(orientation='bottom')
stringaxis1.setTicks([xdict1.items()])
pw = pg.PlotWidget(graphicsView, left="rate", bottom="time", title="g1/"+str(i)+" in rate",axisItems={'bottom': stringaxis1})
curvein=pw.plot(x=list(xdict1.keys()),y=y1)
pw.getAxis("bottom").setLabel( color='#0000ff')
pw.setXRange(0,10)
You could make your own subclass of AxisItem and reimplement its painting method. I just copied it from the source and modified the part that refers to the painting of the ticks. The trick is to "save" the state of the painter and rotate it before drawing the text, then "restore" it. Since rotation is based on the source position of the painter, I also had to translate to the textRect position and swap its width and height.
It might need some small polishing to ensure that the text is correctly aligned, but it should work, and it also sets the minimum height of the axis item based on the tick text.
class MyAxisItem(pg.AxisItem):
def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
p.setRenderHint(p.Antialiasing, False)
p.setRenderHint(p.TextAntialiasing, True)
## draw long line along axis
pen, p1, p2 = axisSpec
p.setPen(pen)
p.drawLine(p1, p2)
p.translate(0.5,0) ## resolves some damn pixel ambiguity
## draw ticks
for pen, p1, p2 in tickSpecs:
p.setPen(pen)
p.drawLine(p1, p2)
## Draw all text
if self.tickFont is not None:
p.setFont(self.tickFont)
p.setPen(self.pen())
for rect, flags, text in textSpecs:
# this is the important part
p.save()
p.translate(rect.x(), rect.y())
p.rotate(-90)
p.drawText(-rect.width(), rect.height(), rect.width(), rect.height(), flags, text)
# restoring the painter is *required*!!!
p.restore()
stringaxis1 = MyAxisItem(orientation='bottom')
stringaxis1.setTicks([xdict1.items()])
# find the maximum width required by the tick texts and add a small margin
fm = QtGui.QFontMetrics(stringaxis1.font())
minHeight = max(fm.boundingRect(QtCore.QRect(), QtCore.Qt.AlignLeft, t).width() for t in xdict1.values())
stringaxis1.setHeight(minHeight + fm.width(' '))