SarthakJariwala / seaborn-image

High-level API for attractive and descriptive image visualization in Python
https://seaborn-image.readthedocs.io/
MIT License
66 stars 7 forks source link

Wrong behavior when integrating with a Qt app (PySide2) #662

Open ismael-benito opened 11 months ago

ismael-benito commented 11 months ago

Hi!

We are trying to integrate seaborn-image into an app developed at our lab, the app is written in Python+Qt, using PySide2. To be specific the app contains a QMDIArea, where several subwindows can appear, one of them is supposed to be a Matplotlib figure. With Matplotlib this works just fine, however when using isbn.imgplot inside the subclassed matplotlib figure this happens:

image

Another image window is opened. The main reason this occours is in seaborn_image._core:

    def _setup_figure(self):
        """Wrapper to setup image with the desired parameters"""
        if self.ax is None:
            f, ax = plt.subplots()
        else:
            f = plt.gcf()
            ax = self.ax

When providing a matplotlib axes object, I think should use the axes itself to determine the figure rather than pyplot. So, I would say this has to be something like:

    def _setup_figure(self):
        """Wrapper to setup image with the desired parameters"""
        if self.ax is None:
            f, ax = plt.subplots()
        else:
            f = self.ax.get_figure() or plt.gcf()
            ax = self.ax

I might oversighted something and there is a way to deal with this situation in the seaborn-image API, if that is the case, sorry for the inconvenience.

Cheers,

Ismael

SarthakJariwala commented 11 months ago

Hi @ismael-benito, thanks for reporting. Can you share how you are calling the imgplot function (starting from setting up the figure)?

I tried replicating the behavior that you are seeing in a python (and ipython) terminal, and I've been able to replicate it in some instances, even with native matplotlib calls.

For instance, if I do the following in the Python interpreter, it leads to the behavior you are likely seeing -

>>> import matplotlib.pyplot as plt
>>> import seaborn_image as isns
>>> pol = isns.load_image("polymer")
>>> fig, ax = plt.subplots()
>>> ax.imshow(pol)
<matplotlib.image.AxesImage object at 0x1491d6170>
>>> plt.show()

But, if I update it to create subplots, and use matplotlib's imshow in the same line it succeeds...

>>> import matplotlib.pyplot as plt
>>> import seaborn_image as isns
>>> pol = isns.load_image("polymer")
>>> fig, ax = plt.subplots(); ax.imshow(pol)
<matplotlib.image.AxesImage object at 0x1450586d0>
>>> plt.show()

The same thing happens for imgplot in seaborn_image. (using ax.get_figure() like you suggested does not resolve the issue)

Could you share the code snippet of matplotlib only code (that you said worked) and with seaborn-image (that didn't work as expected)? Thanks!

ismael-benito commented 11 months ago

I cannot share the full code, but I've created a reduced script which does the same functionality. It seems is also dependent on the environment, as you pointed out. Running this snippet from python directly inside PyCharm with the run configuration set to not use the Python Console does not trigger the second window. However, running the snippet using the Python Console, e.g. using IPython, triggers the error.

Regarding this, I would assume there is something that changes in how matplotlib handles the environment using the QT backends. Would be wise to implement the avobe mentioned method using the .get_figure method from the plt.Axes object, I think.

Here is the snippet:

import numpy as np
import seaborn_image as isns
from PySide2 import QtWidgets, QtCore
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Seaborn-Image Example")
        self.setGeometry(100, 100, 800, 600)

        self.mdi_area = QtWidgets.QMdiArea(self)
        self.setCentralWidget(self.mdi_area)

        sub_window = QtWidgets.QMdiSubWindow()
        self.mdi_area.addSubWindow(sub_window)

        main_widget = QtWidgets.QWidget()
        sub_window.setWidget(main_widget)

        layout = QtWidgets.QVBoxLayout(main_widget)

        # Create a Matplotlib figure and canvas
        self.figure = Figure()
        self.canvas = FigureCanvas(self.figure)
        layout.addWidget(self.canvas)

        # Create a custom toolbar for seaborn-image settings (you can add your tools here)

        # Create an Axes for Matplotlib
        self.ax = self.figure.add_subplot(111)
        self.ax.axis('off')  # Turn off axes

        # Load and display an example image (e.g., scipy.misc.face())
        example_image = np.random.random((512, 512))  # Replace this with your image data
        isns.imgplot(example_image, ax=self.ax)  # Use isns.imgplot to display the image

if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    window = MainWindow()
    window.show()
    app.exec_()
SarthakJariwala commented 10 months ago

Yeah, this seems to be matplotlib backend-related. Unfortunately, I haven't been able to track it down.

Regarding ax.get_figure(): I agree, it's a cleaner way to determine the figure from axis - I've updated it in this PR. Thanks for suggesting.