Pyqt5图像坐标

huangapple go评论83阅读模式
英文:

Pyqt5 image coordinates

问题

我在QLabel上显示图像。我需要图像坐标/像素坐标,但是我使用鼠标单击事件只能得到QLabel坐标。

例如,我的图像是800753,我的QLabel几何位置是(701, 451)。我读取到的坐标是(701, 451),但我需要图像坐标(800753)。

def resimac(self):
    filename = QtWidgets.QFileDialog.getOpenFileName(None, 'Resim Yükle', '.', 'Image Files (*.png *.jpg *.jpeg *.bmp *.tif)')
    self.image = QtGui.QImage(filename[0])
    self.pixmap = QtGui.QPixmap.fromImage(self.image)
    self.resim1.setPixmap(self.pixmap)

    self.resim1.mousePressEvent = self.getPixel

def getPixel(self, event):
    x = event.pos().x()
    y = event.pos().y()
    print("X=", x, " y=", y)

希望这有所帮助。

英文:

I display images with Qlabel.I need image coordinates/pixel coordinates but, I use mouseclickevent its show me only Qlabel coordinates.

for examples my image is 800753 and my Qlabel geometry is (701,451).I reads coordinates in (701,451) but I need image coordinates in (800753)

def resimac(self):
        
   

    filename= QtWidgets.QFileDialog.getOpenFileName(None, 'Resim Yükle', '.', 'Image Files (*.png *.jpg *.jpeg *.bmp *.tif)')
                                              
    self.image=QtGui.QImage(filename[0])
    self.pixmap=QtGui.QPixmap.fromImage(self.image)
    self.resim1.setPixmap(self.pixmap)

    
    self.resim1.mousePressEvent=self.getPixel
    
    
                                         
                                        
def getPixel(self, event):
    
    
    x = event.pos().x()
    y = event.pos().y()
    
    
    print("X=",x," y= ",y)

答案1

得分: 4

由于您没有提供一个最小可重现的示例,我将假设您可能正在设置scaledContents属性,但这也可能不是真的(如果您为标签设置了最大或固定大小)。

您的答案还存在一些其他严重问题,我将在本答案的末尾进行讨论。

必须将点映射到像素图坐标

当将像素图设置为QLabel时,Qt会自动调整标签的大小以适应其内容。
但是,除非标签具有某些大小限制:小于像素图的最大/固定大小,并/或QLabel具有设置为True的scaledContents属性,如上所述。请注意,如果其任何祖先具有某些大小限制(例如,主窗口具有最大大小,或者最大化到小于窗口所需空间的屏幕),也会发生这种情况。
在任何这些情况下,mousePressEvent显然会根据小部件而不是像素图为您提供坐标。

首先,即使它似乎不那么重要,您仍需要考虑每个小部件可能具有一些内容边距:即使事件发生在其实际内容之外的这些边距区域内,小部件仍将接收到这些事件,因此您必须考虑这个方面,并确保事件发生在小部件内容的实际几何形状内(在这种情况下,是像素图)。如果是这样,您将需要将事件位置转换为该矩形以获取其相对于像素图的位置。

然后,如果scaledContents属性为True,图像将缩放到标签的当前可用大小(这也意味着其纵横比将不会保持不变),因此您需要缩放位置。
这只是数学问题:计算图像大小与(标签的内容)之间的比例,然后使用该比例值乘以该值。

    # 单击小部件的水平中心
    mouseX = 100

    pixmapWidth = 400
    widgetWidth = 200
    xRatio = pixmapWidth / widgetWidth
    # xRatio = 2.0

    pixmapX = mouseX * xRatio
    # 结果的“x”是像素图的水平中心
    # pixmapX = 200

另一方面,如果内容未经缩放,您还需要考虑QLabel的alignment属性;通常它是左对齐并且垂直居中的,但这取决于操作系统,当前使用的样式和本地化(考虑从右到左的书写语言)。这意味着如果图像小于可用大小,将在其边距内留下一些空白空间,您必须意识到这一点。

在以下示例中,我试图解决所有这些问题(我必须诚实地说,我不是100%确定,因为由于各种原因,最多可能存在1像素的容忍度,主要涉及基于整数坐标和DPI意识)。请注意,与您的方法不同,我没有覆盖mousePressEvent,而是使用了一个event filter,我将在之后解释原因。

from PyQt5 import QtCore, QtGui, QtWidgets

class Window(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        layout = QtWidgets.QGridLayout(self)

        self.getImageButton = QtWidgets.QPushButton('Select')
        layout.addWidget(self.getImageButton)
        self.getImageButton.clicked.connect(self.resimac)

        self.resim1 = QtWidgets.QLabel()
        layout.addWidget(self.resim1)
        self.resim1.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter)
        # 我假设以下情况...
        self.resim1.setScaledContents(True)
        self.resim1.setFixedSize(701,451)

        # 安装事件过滤器以“捕获”鼠标事件(以及其他事件)
        self.resim1.installEventFilter(self)

    def resimac(self):
        filename, filter = QtWidgets.QFileDialog.getOpenFileName(None, 'Resim Yükle', '.', 'Image Files (*.png *.jpg *.jpeg *.bmp *.tif)')
        if not filename:
            return
        self.resim1.setPixmap(QtGui.QPixmap(filename))

    def eventFilter(self, source, event):
        # 如果源是我们的QLabel,它具有有效的像素图,并且事件是左键单击,请继续尝试获取事件位置
        if (source == self.resim1 and source.pixmap() and not source.pixmap().isNull() and 
            event.type() == QtCore.QEvent.MouseButtonPress and
            event.button() == QtCore.Qt.LeftButton):
                self.getClickedPosition(event.pos())
        return super().eventFilter(source, event)

    def getClickedPosition(self, pos):
        # 考虑小部件内容边距
        contentsRect = QtCore.QRectF(self.resim1.contentsRect())
        if pos not in contentsRect:
            # 在小部件边距之外,忽略!
            return

        # 调整位置以适应内容边距
        pos -= contentsRect.topLeft()

        pixmapRect = self.resim1.pixmap().rect()
        if self.resim1.hasScaledContents():
            x = pos.x() * pixmapRect.width() / contentsRect.width()
            y = pos.y() * pixmapRect.height() / contentsRect.height()
            pos = QtCore.QPoint(x, y)
        else:
            align = self.resim1.alignment()
            # 由于历史原因,QRect(基于整数值)将right()返回为(left+width-1)和bottom()返回为(top+

<details>
<summary>英文:</summary>

Since you didn&#39;t provide a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example), I&#39;m going to assume that you&#39;re probably setting the [`scaledContents`](https://doc.qt.io/qt-5/qlabel.html#scaledContents-prop) property, but that could also be not true (in case you set a maximum or fixed size for the label).

There are some other serious issues about your answer, I&#39;ll address them at the end of this answer.

The point has to be *mapped* to the pixmap coordinates
===

When setting a pixmap to a QLabel, Qt automatically resizes the label to its contents.  
Well, it does it unless the label has some size constrains: a maximum/fixed size that is smaller than the pixmap, and/or the QLabel has the `scaledContents` property set to True as written above. Note that this also happens if any of its ancestors has some size constraints (for example, the main window has a maximum size, or it&#39;s maximized to a screen smaller than the space the window needs).  
In any of those cases, the mousePressEvent will obviously give you the coordinates based on the widget, *not* on the pixmap.

First of all, even if it doesn&#39;t seem to be that important, you&#39;ll have to consider that every widget can have some [contents margins](https://doc.qt.io/qt-5/qwidget.html#contentsMargins): the widget will still receive events that happen *inside the area of those margins*, even if they are outside its actual contents, so you&#39;ll have to consider that aspect, and ensure that the event happens within the real geometry of the widget contents (in this case, the pixmap). If that&#39;s true, you&#39;ll have to translate the event position to that rectangle to get its position according to the pixmap.

Then, if the `scaledContents` property is true, the image will be scaled to the current available size of the label (which also means that its aspect ratio will not be maintained), so you&#39;ll need to *scale* the position.  
This is just a matter of math: compute the proportion between the image size and the (contents of the) label, then multiply the value using that proportion.

# click on the horizontal center of the widget
mouseX = 100

pixmapWidth = 400
widgetWidth = 200
xRatio = pixmapWidth / widgetWidth
# xRatio = 2.0

pixmapX = mouseX * xRatio
# the resulting &quot;x&quot; is the horizontal center of the pixmap
# pixmapX = 200

On the other hand, if the contents are not scaled you&#39;ll have to consider the QLabel [`alignment`](https://doc.qt.io/qt-5/qlabel.html#alignment-prop) property; it is usually aligned on the left and vertically centered, but that depends on the OS, the style currently in use *and the localization* (consider right-to-left writing languages). This means that if the image is smaller than the available size, there will be some empty space within its margins, and you&#39;ll have to be aware of that.


In the following example I&#39;m trying to take care about all of that (I&#39;d have to be honest, I&#39;m not 100% sure, as there *might* be some 1-pixel tolerance due to various reasons, most regarding integer-based coordinates and DPI awareness).
Note that instead of overwriting `mousePressEvent` as you did, I&#39;m using an [event filter](https://doc.qt.io/qt-5/qobject.html#eventFilter), I&#39;ll explain the reason for it afterwards.

from PyQt5 import QtCore, QtGui, QtWidgets

class Window(QtWidgets.QWidget):
def init(self):
QtWidgets.QWidget.init(self)
layout = QtWidgets.QGridLayout(self)

    self.getImageButton = QtWidgets.QPushButton(&#39;Select&#39;)
    layout.addWidget(self.getImageButton)
    self.getImageButton.clicked.connect(self.resimac)

    self.resim1 = QtWidgets.QLabel()
    layout.addWidget(self.resim1)
    self.resim1.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter)
    # I&#39;m assuming the following...
    self.resim1.setScaledContents(True)
    self.resim1.setFixedSize(701,451)

    # install an event filter to &quot;capture&quot; mouse events (amongst others)
    self.resim1.installEventFilter(self)

def resimac(self):
    filename, filter = QtWidgets.QFileDialog.getOpenFileName(None, &#39;Resim Y&#252;kle&#39;, &#39;.&#39;, &#39;Image Files (*.png *.jpg *.jpeg *.bmp *.tif)&#39;)
    if not filename:
        return
    self.resim1.setPixmap(QtGui.QPixmap(filename))

def eventFilter(self, source, event):
    # if the source is our QLabel, it has a valid pixmap, and the event is
    # a left click, proceed in trying to get the event position
    if (source == self.resim1 and source.pixmap() and not source.pixmap().isNull() and 
        event.type() == QtCore.QEvent.MouseButtonPress and
        event.button() == QtCore.Qt.LeftButton):
            self.getClickedPosition(event.pos())
    return super().eventFilter(source, event)

def getClickedPosition(self, pos):
    # consider the widget contents margins
    contentsRect = QtCore.QRectF(self.resim1.contentsRect())
    if pos not in contentsRect:
        # outside widget margins, ignore!
        return

    # adjust the position to the contents margins
    pos -= contentsRect.topLeft()

    pixmapRect = self.resim1.pixmap().rect()
    if self.resim1.hasScaledContents():
        x = pos.x() * pixmapRect.width() / contentsRect.width()
        y = pos.y() * pixmapRect.height() / contentsRect.height()
        pos = QtCore.QPoint(x, y)
    else:
        align = self.resim1.alignment()
        # for historical reasons, QRect (which is based on integer values),
        # returns right() as (left+width-1) and bottom as (top+height-1),
        # and so their opposite functions set/moveRight and set/moveBottom
        # take that into consideration; using a QRectF can prevent that; see:
        # https://doc.qt.io/qt-5/qrect.html#right
        # https://doc.qt.io/qt-5/qrect.html#bottom
        pixmapRect = QtCore.QRectF(pixmapRect)

        # the pixmap is not left aligned, align it correctly
        if align &amp; QtCore.Qt.AlignRight:
            pixmapRect.moveRight(contentsRect.x() + contentsRect.width())
        elif align &amp; QtCore.Qt.AlignHCenter:
            pixmapRect.moveLeft(contentsRect.center().x() - pixmapRect.width() / 2)
        # the pixmap is not top aligned (note that the default for QLabel is
        # Qt.AlignVCenter, the vertical center)
        if align &amp; QtCore.Qt.AlignBottom:
            pixmapRect.moveBottom(contentsRect.y() + contentsRect.height())
        elif align &amp; QtCore.Qt.AlignVCenter:
            pixmapRect.moveTop(contentsRect.center().y() - pixmapRect.height() / 2)

        if not pos in pixmapRect:
            # outside image margins, ignore!
            return
        # translate coordinates to the image position and convert it back to
        # a QPoint, which is integer based
        pos = (pos - pixmapRect.topLeft()).toPoint()

    print(&#39;X={}, Y={}&#39;.format(pos.x(), pos.y()))

if name == 'main':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())


Now. A couple of suggestions.

Don&#39;t overwrite existing child object methods with [other] object&#39;s instance attributes
---
There are various reasons for which this is not a good idea, and, while dealing with Qt, the most important of them is that Qt uses **function caching** for *virtual functions*; this means that as soon as a virtual is called the *first* time, that function will **always** be called in the future. While your approach *could* work in simple cases (especially if the overwriting happens within the parent&#39;s `__init__`), it&#39;s usually prone to unexpected behavior that&#39;s difficult to debug if you&#39;re not very careful.

And that&#39;s exactly your case: I suppose that `resimac` is not called upon parent instantiation and until after some other event (possibly a clicked button) happens. **But** if the user, for some reason, clicks on the label *before* a new pixmap is loaded, your supposedly overwritten method will **never** get called: at that time, you&#39;ve not overwritten it yet, so the user clicks the label, Qt calls the QLabel&#39;s base class `mousePressEvent` implementation, and then that method will *always* be called from that point on, no matter if you try to overwrite it.

To work around that, you have at least 3 options:

1. use an event filter (as the example above); an event filter is something that &quot;captures&quot; events of a widgets and allows you to observe (and interact) with it; you can also decide to propagate that event to the widget&#39;s parent or not (that&#39;s mostly the case of key/mouse events: if a widget isn&#39;t &quot;interested&quot; about one of those events, it &quot;tells&quot; its parent to care about it); this is the simplest method, but it can become hard to implement and debug for complex cases;
2. subclass the widget and manually add it to your GUI within your code;
3. subclass it and &quot;promote&quot; the widget if you&#39;re using Qt&#39;s Designer;

You don&#39;t need to use a QImage for a QLabel.
---
This is not that an issue, it&#39;s just a suggestion: QPixmap already uses (*sort of*) `fromImage` within its C++ code when constructing it with a path as an argument, so there&#39;s no need for that.

Always, *always* provide usable, Minimal Reproducible Example code.
---
See:

- https://stackoverflow.com/help/how-to-ask
- https://stackoverflow.com/help/minimal-reproducible-example

It could take time, even hours to get an &quot;MRE&quot;, but it&#39;s worth it: there&#39;ll always somebody that could answer you, but doesn&#39;t want to or couldn&#39;t dig into your code for various reasons (mostly because it&#39;s incomplete, vague, inusable, lacking context, or even too expanded). If, for *any* reason, there&#39;ll be just *that* one user, you&#39;ll be losing your occasion to solve your problem. Be patient, carefully prepare your questions, and you&#39;ll probably get plenty of interactions and useful insight from it.

</details>



huangapple
  • 本文由 发表于 2020年1月6日 20:01:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/59611751.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定