marcomusy / vedo

A python module for scientific analysis of 3D data based on VTK and Numpy
https://vedo.embl.es
MIT License
2.03k stars 264 forks source link

Plotter in Qt - Mouse Click Event not registering at the correct position #606

Closed danieljiang520 closed 2 years ago

danieljiang520 commented 2 years ago

I have tried several different things about the mouse clicks not registering correctly in my Qt mainwindow. When using only my QHD monitor, the program worked just fine (video). However, when using my laptop (zoomed in at 1792 x 1120) as the only display, the mouse clicks seemed to have a varying up-right offset and register more accurately near the bottom left corner of the widget (video). I am suspicious that the screen resolution of the display might cause a problem for vedo.

The mouse event is a vedo plotter event. Changing the "screensize", "size", "pos" attribute of the plotter did not fix the issue.

I looked up some examples provided by vedo, specifically mousehover.py and qt_window1.py. The mousehover example worked fine on my laptop. However, adding a clicking event in qt_window1.py also created the same issue. Therefore, the problem most likely was caused by the qt widget.

    def __init__(self,size):
        super(MainWindow, self).__init__()

        # load the components defined in th xml file
        loadUi("viewer_gui.ui", self)
        self.screenSize = size

        # Connections for all elements in Mainwindow
        self.pushButton_inputfile.clicked.connect(self.getFilePath)
        self.pushButton_clearSelection.clicked.connect(self.clearScreen)
        self.action_selectVertex.toggled.connect(self.actionSelection_state_changed)
        self.action_selectActor.toggled.connect(self.actionSelection_state_changed)

        # Set up VTK widget
        self.vtkWidget = QVTKRenderWindowInteractor()
        self.splitter_viewer.addWidget(self.vtkWidget)

        # ipy console
        self.ipyConsole = QIPythonWidget(customBanner="Welcome to the embedded ipython console\n")
        self.splitter_viewer.addWidget(self.ipyConsole)
        self.ipyConsole.pushVariables({"foo":43, "print_process_id":print_process_id, "ipy":self.ipyConsole, "self":self})
        self.ipyConsole.printText("The variable 'foo' and the method 'print_process_id()' are available.\
            Use the 'whos' command for information.\n\nTo push variables run this before starting the UI:\
                \n ipyConsole.pushVariables({\"foo\":43,\"print_process_id\":print_process_id})")

        # Create renderer and add the vedo objects and callbacks
        self.plt = Plotter(qtWidget=self.vtkWidget,bg='DarkSlateBlue',bg2='MidnightBlue',screensize=(1792,1120))
        self.id1 = self.plt.addCallback("mouse click", self.onMouseClick)
        self.id2 = self.plt.addCallback("key press",   self.onKeypress)
        self.plt.show()                  # <--- show the vedo rendering

    def onMouseClick(self, event):
        if(self.action_selectActor.isChecked()):
            self.selectActor(event)
        elif(self.action_selectVertex.isChecked()):
            self.selectVertex(event)

    def selectActor(self,event):
        if(not event.actor):
            return
        printc("You have clicked your mouse button. Event info:\n", event, c='y')
        printc("Left button pressed on", [event.picked3d])
        # adding a silhouette might cause some lags
        # self.plt += event.actor.silhouette().lineWidth(2).c('red')
        #an alternative solution
        self.actorSelection = event.actor.clone()
        self.actorSelection.c('red')
        self.plt += self.actorSelection

    def selectVertex(self,event):
        if(not event.isPoints):
            return
        # print(arr[event.actor.closestPoint(event.picked3d, returnPointId=True)])
        printc("You have clicked your mouse button. Event info:\n", event, c='y')
        printc("Left button pressed on 3d: ", [event.picked3d])
        printc("Left button pressed on 2d: ", [event.picked2d])
        p = pointcloud.Point(pos=(event.picked3d[0],event.picked3d[1],event.picked3d[2]),r=12,c='red',alpha=0.5)
        self.vertexSelections.append(p)        
        self.plt += p

Running the following lines:

app = QApplication(sys.argv)
screen = app.primaryScreen()
print('Screen: %s' % screen.name())
size = screen.size()
print('Size: %d x %d' % (size.width(), size.height()))

outputted

Screen: Color LCD
Size: 1792 x 1120
marcomusy commented 2 years ago

Sorry I cannot reproduce the issue. This seems to work as expected:

import sys
from PyQt5 import Qt
from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
from vedo import Plotter, Cone, printc, Point

class MainWindow(Qt.QMainWindow):

    def __init__(self, size, parent=None):

        self.actorSelection = None
        self.vertexSelections = []
        self.silu = None

        Qt.QMainWindow.__init__(self, parent)
        self.frame = Qt.QFrame()
        self.layout = Qt.QVBoxLayout()
        self.vtkWidget = QVTKRenderWindowInteractor(self.frame)

        # Create renderer and add the vedo objects and callbacks
        self.plt = Plotter(qtWidget=self.vtkWidget)
        self.id1 = self.plt.addCallback("mouse click", self.onMouseClick)
        self.id2 = self.plt.addCallback("key press",   self.onKeypress)
        self.plt += Cone().rotateX(20)
        self.plt.show()                  # <--- show the vedo rendering

        # Set-up the rest of the Qt window
        button = Qt.QPushButton("My Button makes the cone red")
        button.setToolTip('This is an example button')
        button.clicked.connect(self.onClick)
        self.layout.addWidget(self.vtkWidget)
        self.layout.addWidget(button)
        self.frame.setLayout(self.layout)
        self.setCentralWidget(self.frame)
        self.show()                     # <--- show the Qt Window

    def onMouseClick(self, event):
        printc("..vedo calling onMouseClick")
        if not event.actor:
            return
        #printc("You have clicked your mouse button. Event info:\n", event, c='y')
        printc("Left button pressed on", event.picked3d)
        self.silu = event.actor.silhouette().lineWidth(5).c('purple5')
        pt = Point(event.picked3d).c('black').ps(12)
        self.vertexSelections.append(pt)
        self.plt.remove(self.silu).add([self.silu, pt])

    def onKeypress(self, evt):
        printc("You have pressed key:", evt.keyPressed, c='b')

    @Qt.pyqtSlot()
    def onClick(self):
        printc(".. Qt calling onClick")
        self.plt.actors[0].color('red').rotateZ(40)
        self.plt.interactor.Render()

    def onClose(self):
        #Disable the interactor before closing to prevent it
        #from trying to act on already deleted items
        printc("..calling onClose")
        self.vtkWidget.close()

if __name__ == "__main__":

    app = Qt.QApplication(sys.argv)
    screen = app.primaryScreen()
    size = screen.size()
    print('Screen: %s' % screen.name())
    print('Size: %d x %d' % (size.width(), size.height()))

    window = MainWindow(size)
    app.aboutToQuit.connect(window.onClose) # <-- connect the onClose event
    app.exec_()

Screenshot from 2022-03-08 11-42-35

danieljiang520 commented 2 years ago

Hi Marco, thank you for looking into this issue. I am still encountering similar mouse click offsets when running the code that you provided (video). Even when I change my laptop's resolution to 2048x1280, the issue still occurred (video). I would really appreciate your feedback on this.

marcomusy commented 2 years ago

Hi Daniel, thanks for reporting, let's try this exercise:

pip install -U git+https://github.com/marcomusy/vedo.git

then

import sys
from PyQt5 import Qt
from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
from vedo import Plotter, Cone, printc, Point
from vedo.cli import exe_info

class MainWindow(Qt.QMainWindow):

    def __init__(self, size, parent=None):
        Qt.QMainWindow.__init__(self, parent)
        self.frame = Qt.QFrame()
        self.layout = Qt.QVBoxLayout()
        self.vtkWidget = QVTKRenderWindowInteractor(self.frame)

        # Create renderer and add the vedo objects and callbacks
        self.plt = Plotter(qtWidget=self.vtkWidget, pos=(0,0))
        self.id1 = self.plt.addCallback("mouse click", self.onMouseClick)
        self.plt += Cone().rotateX(20)
        self.plt.show()                  # <--- show the vedo rendering

        # Set-up the rest of the Qt window
        self.layout.addWidget(self.vtkWidget)
        self.frame.setLayout(self.layout)
        self.setCentralWidget(self.frame)
        self.show()                     # <--- show the Qt Window

    def onMouseClick(self, event):
        printc("Left button pressed on 2D", event.picked2d, c='y')
        if not event.actor:
            return
        printc("Left button pressed on 3D", event.picked3d, c='g')
        pt = Point(event.picked3d).c('pink').ps(12)
        self.plt.add(pt)

if __name__ == "__main__":

    exe_info([]) # will dump some info about the system

    app = Qt.QApplication(sys.argv)
    screen = app.primaryScreen()
    size = screen.size()
    print('Screen: %s' % screen.name())
    print('Size: %d x %d' % (size.width(), size.height()))

    window = MainWindow(size)
    app.exec_()

now click the canvas corners , starting from the bottom-left anticlockwise, then the tip of the cone:

Screenshot from 2022-03-14 13-35-34

you should get something like this output:

Screenshot from 2022-03-14 13-35-43

danieljiang520 commented 2 years ago

Hi Marco, here is my output after running the test. I did not resize the window but it seems like the positions are twice as far as what they are supposed to be.

Screen Shot 2022-03-14 at 13 15 21
marcomusy commented 2 years ago

Oh I can finally reproduce the issue on my mac! I'm investigating it..

marcomusy commented 2 years ago

ok... this seems to be an upstream bug in vtk, unrelated to vedo: https://discourse.vtk.org/t/vtkproppicker-not-working-in-osx-and-qt/8057 i'm afraid we need to wait for their input on this!

danieljiang520 commented 2 years ago

Thank you for looking into this bug. Hopefully, vtk will respond soon.

marcomusy commented 2 years ago

They seem to recognize the problem but I guess it will take time to solve... maybe you can try VTK 8.1 if you have the possibility to use that version.

danieljiang520 commented 2 years ago

After downgrading VTK to version 8.1.2 and Python to 3.7, the clicking issue disappeared when running the program on my laptop. Thank you for the help! I will keep an eye on new releases from vtk.

p-j-smith commented 2 years ago

Hi, I'm very new to vedo (thanks for making such a great library!), but I've come across this issue with vtk before. Based on the comments in a previous issue related to this problem, it sounds like there is no plan to fix it as vtkmodules/qt/QVTKRenderWindowInteractor.py is not maintained by the core VTK devs. But I'm sure they'd be open to someone submitting a fix.

There used to be the opposite problem whereby coordinates would be scaled by a factor of 0.5, so a fix was made for this in QVTKRenderWindowInteractor. However, this fix is no longer necessary (possibly due to changes made in qt), and so the original fix is now actually a bug. The fix introduced a method _getPixelRatio, which determines the factor by which coordinates needed to be scaled to fix the previous bug (and on Mac retina displays this was a factor of two). As no scaling needs to be applied, I've been using a hacky fix to make this method always return a value of 1:

interactor = QVTKRenderWindowInteractor()
interactor._getPixelRatio = lambda _: 1
marcomusy commented 2 years ago

This is a great piece of information! Thanks for letting us know!