I'm following the script given at the official mayaVI site (Multiple mlab scene models example), and would like to use the sync_camera command to sync the two figures together within a qt GUI (just as shown), such that any rotation/zoom, etc in one figure automatically rotates/zooms, etc the other in the exact same manner, at the same time.
The sync_camera command is written about briefly on another official mayaVI page Figure handling functions, but I haven't been able to find much on its proper use to utilize successfully within the class hierarchy.
Does anyone have any experience with this procedure or advice?
import numpy as np
from traits.api import HasTraits, Instance, Button, \
on_trait_change
from traitsui.api import View, Item, HSplit, Group
from mayavi import mlab
from mayavi.core.ui.api import MlabSceneModel, SceneEditor
class MyDialog(HasTraits):
scene1 = Instance(MlabSceneModel, ())
scene2 = Instance(MlabSceneModel, ())
button1 = Button('Redraw')
button2 = Button('Redraw')
#on_trait_change('button1')
def redraw_scene1(self):
self.redraw_scene(self.scene1)
#on_trait_change('button2')
def redraw_scene2(self):
self.redraw_scene(self.scene2)
def redraw_scene(self, scene):
# Notice how each mlab call points explicitely to the figure it
# applies to.
mlab.clf(figure=scene.mayavi_scene)
x, y, z, s = np.random.random((4, 100))
mlab.points3d(x, y, z, s, figure=scene.mayavi_scene)
# The layout of the dialog created
view = View(HSplit(
Group(
Item('scene1',
editor=SceneEditor(), height=250,
width=300),
'button1',
show_labels=False,
),
Group(
Item('scene2',
editor=SceneEditor(), height=250,
width=300, show_label=False),
'button2',
show_labels=False,
),
),
resizable=True,
)
m = MyDialog()
m.configure_traits()
The solution is to not use the 2-figure-in-1 method (as originally posted), but to create 2 separate figures. For my needs, I've rewritten the initial code such that each figure is in it's own class, and then simply placed them in a new frame side by side. I don't think using the sync_camera function is possible without such a separation, since it requires two separate figures as inputs. The result is basically identical. I successfully implemented the sync_camera function as follows:
import sys, os, time
import numpy as np
os.environ['ETS_TOOLKIT'] = 'qt4'
from pyface.qt import QtGui, QtCore
from traits.api import HasTraits, Instance, on_trait_change, Str, Float, Range
from traitsui.api import View, Item, HSplit, Group
from mayavi import mlab
from mayavi.core.api import PipelineBase, Engine
from mayavi.core.ui.api import MayaviScene, MlabSceneModel, SceneEditor
class Mayavi1(HasTraits):
scene = Instance(MlabSceneModel, ())
#on_trait_change('scene.activated')
def update_plot(self):
Mayavi1.fig1 = mlab.figure(1)
self.scene.mlab.clf(figure=Mayavi1.fig1)
x, y, z, s = np.random.random((4, 100))
splot = self.scene.mlab.points3d(x, y, z, s, figure=Mayavi1.fig1)
#splot.actor.actor.scale = np.array([25,25,25]) #if plot-types different
view = View(Item('scene', editor=SceneEditor(scene_class=MayaviScene),
height=300, width=300, show_label=False),
resizable=True
)
class Mayavi2(HasTraits):
scene = Instance(MlabSceneModel, ())
#on_trait_change('scene.activated')
def update_plot(self):
Mayavi2.fig2 = mlab.figure(2)
self.scene.mlab.clf(figure=Mayavi2.fig2)
x, y, z, s = np.random.random((4, 100))
cplot = self.scene.mlab.points3d(x, y, z, s, figure=Mayavi2.fig2)
#cplot.actor.actor.position = np.array([1,1,1]) #if plot-types different
view = View(Item('scene', editor=SceneEditor(scene_class=MayaviScene),
height=300, width=300, show_label=False),
resizable=True
)
class P1(QtGui.QWidget):
def __init__(self, parent=None):
super(P1, self).__init__(parent)
layout = QtGui.QGridLayout(self)
layout.setContentsMargins(20,20,20,20) #W,N,E,S
layout.setSpacing(10)
self.visualization1 = Mayavi1()
self.ui1 = self.visualization1.edit_traits(parent=self, kind='subpanel').control
layout.addWidget(self.ui1, 0, 0, 1, 1)
self.ui1.setParent(self)
self.visualization2 = Mayavi2()
self.ui2 = self.visualization2.edit_traits(parent=self, kind='subpanel').control
layout.addWidget(self.ui2, 0, 2, 1, 1)
self.ui2.setParent(self)
mlab.sync_camera(self.visualization1,self.visualization2)
mlab.sync_camera(self.visualization2,self.visualization1)
#self.visualization1.scene.mlab.view(0,0,10,[1,1,1])
class Hierarchy(QtGui.QMainWindow):
def __init__(self, parent=None):
super(Hierarchy, self).__init__(parent)
self.setGeometry(50, 50, 400, 400) #(int x, int y, int w, int h)
self.gotoP1()
def gotoP1(self):
self.P1f = P1(self)
self.setWindowTitle("Page1")
self.setCentralWidget(self.P1f)
self.show()
if __name__ == '__main__':
app = QtGui.QApplication.instance()
#app = QtGui.QApplication(sys.argv)
w = Hierarchy()
sys.exit(app.exec_())
However, in my own version, I'm using two different data sources within each plot (one a scatter plot and the other a contour plot, with the contour plot origin of interest different from the scatter plot), and because of the camera connection, neither one is on screen at the same time as the other (native coordinates distinct in both).
Thus, if you're only seeing one of the 3d objects in frame at a time, adjust the positions within the def update_plot(self) for either figure until they are both viewed on the screen at the same time. This can be done via such commands as:
splot.actor.actor.scale = np.array([25,25,25]) #with splot for fig1
cplot.actor.actor.position = np.array([-64,-64,-64]) #with cplot for fig2
I highly suggest actually going into the mayaVI pipeline (with the red light clicked to see the output code in real-time) to adjust your plots as needed. If anyone needs any further help with this down the road, please let me know.
Related
I want to get a multiscene layout described in https://docs.enthought.com/mayavi/mayavi/auto/example_multiple_mlab_scene_models.html
import numpy as np
from traits.api import HasTraits, Instance, Button, \
on_trait_change
from traitsui.api import View, Item, HSplit, Group
from mayavi import mlab
from mayavi.core.ui.api import MlabSceneModel, SceneEditor
class MyDialog(HasTraits):
scene1 = Instance(MlabSceneModel, ())
scene2 = Instance(MlabSceneModel, ())
button1 = Button('Redraw')
button2 = Button('Redraw')
#on_trait_change('button1')
def redraw_scene1(self):
self.redraw_scene(self.scene1)
#on_trait_change('button2')
def redraw_scene2(self):
self.redraw_scene(self.scene2)
def redraw_scene(self, scene):
# Notice how each mlab call points explicitly to the figure it
# applies to.
mlab.clf(figure=scene.mayavi_scene)
x, y, z, s = np.random.random((4, 100))
mlab.points3d(x, y, z, s, figure=scene.mayavi_scene)
# The layout of the dialog created
view = View(HSplit(
Group(
Item('scene1',
editor=SceneEditor(), height=250,
width=300),
'button1',
show_labels=False,
),
Group(
Item('scene2',
editor=SceneEditor(), height=250,
width=300, show_label=False),
'button2',
show_labels=False,
),
),
resizable=True,
)
m = MyDialog()
m.configure_traits()
Each scene has to render a separate volume object.
I have provided a custom redraw_scene function with
def redraw_scene(self, scene):
# Notice how each mlab call points explicitly to the figure it
# applies to.
mlab.clf(figure=scene.mayavi_scene)
s = np.random.random((100, 100, 100))
mlab.pipeline.volume(mlab.pipeline.scalar_field(s), figure=scene.mayavi_scene)
but ended up getting both volumes rendered on the second scene.
I have also tried the setup with a separate engine per scene but it yields the same result.
How I do get volume renders in separate scenes with Mayavi?
I am trying to remove an image from a figure and release the memory. when colorbar is not added for the image, memory can be released successfully, however, if colorbar is added, it fails. In the demo-code bellow:
click push button Add ColorBar will add a color bar for one image in the figure.
click push button remove will remove one image(and the related colorbar) from the figure.
each time i remove the image, the colorbar related is also removed, so i don't know why the memory recycle fails, I guess there must be some extra reference to the image when add a colorbar to it, which fails the memory recycle.
import numpy as np
from PyQt5 import QtWidgets
from memory_profiler import profile
import matplotlib
from matplotlib.figure import Figure
import matplotlib.cm as cm
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.axes._axes import Axes
matplotlib.use("Qt5Agg")
class MplCanvas(FigureCanvasQTAgg):
def __init__(self, parent=None, width=5, height=4, dpi=100):
self.fig = Figure(figsize=(width, height), dpi=dpi)
self.axe = self.fig.add_subplot(1, 1, 1, label='good')
super().__init__(self.fig)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout()
self.canvas = MplCanvas(self, width=5, height=4, dpi=100)
self.axe = self.canvas.axe
layout.addWidget(self.canvas)
self.pushButton_addColorBar = QtWidgets.QPushButton('Add ColorBar')
layout.addWidget(self.pushButton_addColorBar)
self.pushButton_remove = QtWidgets.QPushButton('remove')
layout.addWidget(self.pushButton_remove)
widget = QtWidgets.QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
self.pushButton_remove.clicked.connect(self.removeImage)
self.pushButton_addColorBar.clicked.connect(self.createColorBar)
self.pcolormesh_test()
def pcolormesh_test(self):
"""add two images"""
delta = 0.01
x = y = np.arange(-3.0, 3.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-X ** 2 - Y ** 2)
Z2 = np.exp(-(X - 1) ** 2 - (Y - 1) ** 2)
Z = (Z1 - Z2) * 2
im = self.axe.pcolormesh(X, Y, Z, cmap=cm.viridis, shading='auto')
im.set_clim(vmax=np.amax(Z), vmin=np.amin(Z))
Zx = (Z1 + Z2) * 2
imx = self.axe.pcolormesh(X, Y, Zx, cmap=cm.Blues, shading='auto')
imx.set_clim(vmax=np.amax(Zx), vmin=np.amin(Zx))
def createColorBar(self):
""" to create a color bar for an image. """
axe = self.axe
fig = axe.get_figure()
images = self.getImages(axe)
for image in images:
if not image.colorbar: # color bar doesn't exist
inset_axe = axe.inset_axes([1.0, 0, 0.05, 1], transform=axe.transAxes)
fig.colorbar(image, ax=axe, cax=inset_axe)
break # each trigger create one colorbar for one image
self.reDraw()
#profile
def removeImage(self, checked):
"""
Usage:
* each trigger remove one image
"""
images = self.getImages(self.axe)
# print(f'images={images}')
if images:
image = images[-1]
color_bar = image.colorbar
if color_bar:
color_bar.remove()
del color_bar
# remove image
image.remove()
del image
self.reDraw()
def getImages(self, axe: Axes):
"""to obtain the image list in the axe"""
images = []
images.extend(axe.images)
images.extend(axe.collections)
return images
def reDraw(self):
self.canvas.draw_idle()
self.canvas.flush_events()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
I have found the solution, and post an answer to help.
we need to add gc.collect() at the end of removeImage() method. then the memory can be reclaimed when the image is removed.
I would like to show a context menu on the position of mouse click and then create a new line on that position in the graph.
For that I need both the PyQt position and the graph data position. I thought that I could use the matplotlib transformation functions, but somehow when clicking the lower left and upper right corners of the graph I get in the print values [-0.34, 30.73], [3.02, -1.49] instead of ~[-0.3, -0.9], ~[4.3, 42].
Can anyone fix the mistake I make in the code?
P.S. I know I can connect a matplotlib signal and get the correct data positions. But I would then need to transform those positions to PyQt positions in order to place the widget correctly, resulting in the same issue.
Follows a simplified code:
import sys
import matplotlib
matplotlib.use('Qt5Agg')
from PyQt5 import QtCore, QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure
class MplCanvas(FigureCanvasQTAgg):
def __init__(self, parent=None, width=5, height=4, dpi=100):
fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = fig.add_subplot(111)
super(MplCanvas, self).__init__(fig)
self._menuPoint = None
self.canvasMenu = QtWidgets.QMenu(self)
ca = QtWidgets.QAction('Add line', self)
ca.triggered.connect(self.onAddLineClicked)
self.canvasMenu.addAction(ca)
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
self._menuPoint = event.pos()
print(self.axes.transData.inverted().transform((self._menuPoint.x(), self._menuPoint.y())))
if event.button() == QtCore.Qt.RightButton:
self.canvasMenu.exec_(self.mapToGlobal(self._menuPoint))
def onAddLineClicked(self):
pass
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
sc = MplCanvas(self)
sc.axes.plot([0, 1, 2, 3, 4], [10, 1, 20, 3, 40])
self.setCentralWidget(sc)
self.show()
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec_()
Thanks.
The coordinates returned by QMouseEvent.pos() are comprised between [0 – widget width] and [0 - widget height], while the figure coordinates are between [0 – 1]. You therefore need to divide the mouse pos() by the widget width and height. There is also the subtlety that the Qt coordinates are from the upper left corner, while the matplotlib coordinates are from the lower left corner.
Once you have your position in figure coordinates, it is relatively straightforward to convert them in data coordinates. You could also convert them to Axes coordinates to test whether the click was inside the axes or not.
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
self._menuPoint = event.pos()
w, h = self.get_width_height()
xfig = event.x()/w
yfig = 1-(event.y()/h) # necessary because Qt coordinates are from upper left, while matplotlib's are from
# lower left
x, y = self.axes.transData.inverted().transform(self.fig.transFigure.transform([xfig, yfig]))
print(event.pos(), x, y)
if event.button() == QtCore.Qt.RightButton:
self.canvasMenu.exec_(self.mapToGlobal(self._menuPoint))
I have some noisy data which I'm trying to fit with a gaussian. The problem is that I have to do it manually. By that, I mean I have to move the point on the curve (see figure below). When I move the point I have to update the curve so the curve it self can move.
For example on this curve if I move the upper point it changes the mu of my gaussian and if I move the point in the middle it update the sigma parameter. On this example, I've plotted the two curve in a FigureCanvas of matplotlib that I've embedded in a QMainWindow.
I've seached and found no way to do that in a matplotlib figure embedded in a PyQt widget. So, I've changed and tried to use PyQtGraph with the ROI tools but it didn't work very well.
Do you have any idea how i can achieve this? Is there a simple python library to do that? Thanks
EDIT :
Here is the code I've used to produce the image :
from PySide2 import QtCore, QtGui, QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import numpy as np
class PainterCanvas(FigureCanvas):
def __init__(self, parent=None, width=5, height=4, dpi=100):
fig = Figure(figsize=(width, height), dpi=dpi)
FigureCanvas.__init__(self, fig)
self.setParent(parent)
self._instructions = []
self.axes = self.figure.add_subplot(111)
def paintEvent(self, event):
super().paintEvent(event)
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
width, height = self.get_width_height()
for x, y, rx, ry, br_color in self._instructions:
x_pixel, y_pixel_m = self.axes.transData.transform((x, y))
# In matplotlib, 0,0 is the lower left corner,
# whereas it's usually the upper right
# for most image software, so we'll flip the y-coor
y_pixel = height - y_pixel_m
painter.setBrush(QtGui.QColor(br_color))
painter.drawEllipse( QtCore.QPoint(x_pixel, y_pixel), rx, ry)
def create_oval(self, x, y, radius_x=2, radius_y=2, brush_color="red"):
self._instructions.append([x, y, radius_x, radius_y, brush_color])
self.update()
class MyPaintWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.canvas = PainterCanvas()
self.canvas.mpl_connect("button_press_event", self._on_left_click)
x = np.arange(0, 10, 0.1)
rand = [np.random.uniform(-0.1, 0.2) for _ in x]
y0 = np.exp(- (x - 5) ** 2 / 2) + rand
y1 = np.exp(- (x - 3) ** 2 / 0.5)
self.canvas.axes.plot(x, y0)
self.canvas.axes.plot(x, y1)
layout_canvas = QtWidgets.QVBoxLayout(self)
layout_canvas.addWidget(self.canvas)
self.canvasMenu = QtWidgets.QMenu(self)
self.canvasMenu.addAction("test")
self.canvas.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.canvas.customContextMenuRequested.connect(self._on_left_click)
def _on_left_click(self, event):
self.canvas.create_oval(event.xdata, event.ydata, brush_color="green")
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MyPaintWidget()
w.show()
sys.exit(app.exec_())
To Add point I've added them by clicking on the curve. I know that code won't work to do what I've asked but it was just to produce an image to explain my idea.
As suggested by mkrieger1, I will briefly describe my solution :
First I've used PyQtGraph to plot my data to draw updates more efficiently than when I was using Matplotlib.
To control curves I've used QSliders (modified to support double) to control the parameters of each curve. When I move one slider it emits an event and in the function handling this event, I update my pyqtgraph plotwidget with the function setData.
I get my inspiration from to do my double cursor : Use float for QSlider
and I've used the pyqtgraph documentation : http://www.pyqtgraph.org/documentation/graphicsItems/plotdataitem.html
There is too much code to put it there but the solution is on my GitHub in the spectrofit project: https://github.com/fulmen27/SpectroFit (It's UI to process and study stellar spectrums). The solution is in the spectrofit.tools.Interactive_Fit.py file.
Hope this can help!
In update_plot function I added print statement to check if I am getting correct data after interacting with scene, I can see I am getting correct value but plot itself is not updated. I am not where I am doing wrong. I think I am doing something wrong when I pass data back to scaler_field
from traits.api import HasTraits, Range, Instance, \
on_trait_change
from traitsui.api import View, Item, HGroup
from tvtk.pyface.scene_editor import SceneEditor
from mayavi.tools.mlab_scene_model import \
MlabSceneModel
from mayavi.core.ui.mayavi_scene import MayaviScene
from mayavi import mlab
class Visualization(HasTraits):
mySlice = Range(0, 400, 100) # slice number
scene = Instance(MlabSceneModel, ())
def __init__(self):
HasTraits.__init__(self)
data = InlinemySlice(self.mySlice) # call new data
self.x_source = self.scene.mlab.pipeline.scalar_field(data)
self.plot = self.scene.mlab.pipeline.image_plane_widget(self.x_source, plane_orientation='x_axes', colormap='Greys', vmin=-0.020505040884017944 ,vmax=0.020505040884017944)
#on_trait_change('mySlice')
def update_plot(self):
x = InlinemySlice(self.mySlice)
print(x)
y_source = mlab.pipeline.scalar_field(x)
self.plot.mlab_source.trait_set(y_source)
# the layout of the dialog created
view = View(Item('scene', editor=SceneEditor(scene_class=MayaviScene),
height=500, width=600, show_label=False),
HGroup( 'mySlice' ),
)
visualization = Visualization()
visualization.configure_traits()