pyqtgraph image point selection - python

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:

Related

PyQt5 QGraphicsSimpleTextItem Y position

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.

Finding origin pixel of image in OpenCV

In python openCV I am trying to create a GUI where the user has to pick pixels at set y coordinates. I can get the openCV pixel location that I want to set the mouse to, but I have no way of tying that to the overall system pixel which is needed for the win32api.SetCursorPos(). I have tried moving the image window with cv2.moveWindow('label', x, y) and then offsetting the cursor by y+offset, but this is a very inexact solution. Is there any way to find the current system pixel where the image origin pixel resides?
I'm not aware of a way to do this directly with OpenCV (after all, it's meant as convenience for prototyping, rather than a full fledged GUI framework), but since we're on Windows, we can hack it using the WinAPI directly.
N.B. There's a slight complication -- the callback returns image coordinates, so if scaling is enabled, our precision will be limited, and we have to do some extra work to map the coordinates back to client window coordinates.
Let's begin by investigating the window hierarchy create by OpenCV for the image display window. We could investigate the source code, but there's a quicker way, using the Spy++ tool from MSVS.
We can write a simple script to show some random data for this:
import cv2
import numpy as np
WINDOW_NAME = u'image'
img = np.zeros((512, 512), np.uint8)
cv2.randu(img, 0, 256)
cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_NORMAL)
cv2.imshow(WINDOW_NAME, img)
cv2.waitKey()
When we find this window in Spy++, we can see the following info.
There is the top level window, with a caption equal to the window name we specified, of class Main HighGUI class. This window contains a single child window, with no caption, and of class HighGUI class.
The following algorithm comes to mind:
Use FindWindow to find the top level window by caption, and get it's window handle.
Use GetWindow to get the handle of its child window.
Use GetClientRect to get the width and height of the client area (which contains the rendered image).
Transform the x and y image-relative coordinates back to client area space. (We need to know the dimensions of the current image to do this, so we will pass the current image as the user parameter of the callback.)
Transform the coordinates to screen space using ClientToScreen
Sample Script:
import win32gui
from win32con import GW_CHILD
import cv2
import numpy as np
# ============================================================================
def on_mouse(event, x, y, flags, img):
if event != cv2.EVENT_LBUTTONDOWN:
return
window_handle = win32gui.FindWindow(None, WINDOW_NAME)
child_window_handle = win32gui.GetWindow(window_handle, GW_CHILD)
(_, _, client_w, client_h) = win32gui.GetClientRect(child_window_handle)
image_h, image_w = img.shape[:2]
real_x = int(round((float(x) / image_w) * client_w))
real_y = int(round((float(y) / image_h) * client_h))
print win32gui.ClientToScreen(child_window_handle, (real_x, real_y))
# ----------------------------------------------------------------------------
def show_with_callback(name, img):
cv2.namedWindow(name, cv2.WINDOW_NORMAL)
cv2.setMouseCallback(name, on_mouse, img)
cv2.imshow(name, img)
cv2.waitKey()
cv2.destroyWindow(name)
# ============================================================================
WINDOW_NAME = u'image'
# Make some test image
img = np.zeros((512, 512), np.uint8)
cv2.randu(img, 0, 256)
show_with_callback(WINDOW_NAME, img)

How to display clickable RGB image similar to pyqtgraph ImageView?

Despite not being a proficient GUI programmer, I figured out how to use the pyqtgraph module's ImageView function to display an image that I can pan/zoom and click on to get precise pixel coordinates. The complete code is given below. The only problem is that ImageView can apparently only display a single-channel (monochrome) image.
My question: How do I do EXACTLY the same thing as this program (ignoring histogram, norm, and ROI features, which I don't really need), but with the option to display a true color image (e.g., the original JPEG photo)?
import numpy as np
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
import matplotlib.image as mpimg
# Load image from disk and reorient it for viewing
fname = 'R0000187.JPG' # This can be any photo image file
photo=np.array(mpimg.imread(fname))
photo = photo.transpose()
# select for red color and extract as monochrome image
img = photo[0,:,:] # WHAT IF I WANT TO DISPLAY THE ORIGINAL RGB IMAGE?
# Create app
app = QtGui.QApplication([])
## Create window with ImageView widget
win = QtGui.QMainWindow()
win.resize(1200,800)
imv = pg.ImageView()
win.setCentralWidget(imv)
win.show()
win.setWindowTitle(fname)
## Display the data
imv.setImage(img)
def click(event):
event.accept()
pos = event.pos()
print (int(pos.x()),int(pos.y()))
imv.getImageItem().mouseClickEvent = click
## 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_()
pyqtgraph.ImageView does support rgb / rgba images. For example:
import numpy as np
import pyqtgraph as pg
data = np.random.randint(255, size=(100, 100, 3))
pg.image(data)
..and if you want to display the exact image data without automatic level adjustment:
pg.image(data, levels=(0, 255))
As pointed out by Luke, ImageView() does display RGB, provided the correct array shape is passed. In my sample program, I should have used photo.transpose([1,0,2]) to keep the RGB in the last dimension rather than just photo.transpose(). When ImageView is confronted with an array of dimension (3, W, H), it treats the array as a video consisting of 3 monochrome images, with a slider at the bottom to select the frame.
(Corrected to incorporate followup comment by Luke, below)

PyQt QGraphicsEllipseItem rotate offset

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.

Pyqt get pixel position and value when mouse click on the image

I would like to know how i can select a pixel with a mouse click in an image (QImge) and get pixel position and value.
Thanks
self.image = QLabel()
self.image.setPixmap(QPixmap("C:\\myImg.jpg"))
self.image.setObjectName("image")
self.image.mousePressEvent = self.getPos
def getPos(self , event):
x = event.pos().x()
y = event.pos().y()
This question is old but for everybody getting here, like me, this is my solution based on Jareds answer:
self.img = QImage('fname.png')
pixmap = QPixmap(QPixmap.fromImage(self.img))
img_label = QLabel()
img_label.setPixmap(pixmap)
img_label.mousePressEvent = self.getPixel
def self.getPixel(self, event):
x = event.pos().x()
y = event.pos().y()
c = self.img.pixel(x,y) # color code (integer): 3235912
# depending on what kind of value you like (arbitary examples)
c_qobj = QColor(c) # color object
c_rgb = QColor(c).getRgb() # 8bit RGBA: (255, 23, 0, 255)
c_rgbf = QColor(c).getRgbf() # RGBA float: (1.0, 0.3123, 0.0, 1.0)
return x, y, c_rgb
Make sure the size of the label matches the size of the image, otherwise the x and y mouse coords need to be transformed to image coords. And I guess it's also possible to use the .pixel() method directly on a pixmap as well, but the QImage object seems to perform better in my case.
First you have to draw the image. You can do this my making a QLabel widget and call setPixmap. You need to convert your QImage to QPixmap before doing this (you can use QPixmap.fromImage(img)).
You can get mouse clicks by subclassing the QImage and intercepting mousePressEvent. Look up the pixel value with QImage.pixel().

Categories

Resources