class ArrayVolumePlot(qt.QWidget): """ Widget for plotting a n-D array (n >= 3) as a 3D scalar field. Three axis arrays can be provided to calibrate the axes. The signal array can have an arbitrary number of dimensions, the only limitation being that the last 3 dimensions must have the same length as the axes arrays. Sliders are provided to select indices on the first (n - 3) dimensions of the signal array, and the plot is updated to load the stack corresponding to the selection. """ def __init__(self, parent=None): """ :param parent: Parent QWidget """ super(ArrayVolumePlot, self).__init__(parent) self.__signal = None self.__signal_name = None # the Z, Y, X axes apply to the last three dimensions of the signal # (in that order) self.__z_axis = None self.__z_axis_name = None self.__y_axis = None self.__y_axis_name = None self.__x_axis = None self.__x_axis_name = None from silx.gui.plot3d.ScalarFieldView import ScalarFieldView from silx.gui.plot3d import SFViewParamTree self._view = ScalarFieldView(self) def computeIsolevel(data): data = data[numpy.isfinite(data)] if len(data) == 0: return 0 else: return numpy.mean(data) + numpy.std(data) self._view.addIsosurface(computeIsolevel, '#FF0000FF') # Create a parameter tree for the scalar field view options = SFViewParamTree.TreeView(self._view) options.setSfView(self._view) # Add the parameter tree to the main window in a dock widget dock = qt.QDockWidget() dock.setWidget(options) self._view.addDockWidget(qt.Qt.RightDockWidgetArea, dock) self._hline = qt.QFrame(self) self._hline.setFrameStyle(qt.QFrame.HLine) self._hline.setFrameShadow(qt.QFrame.Sunken) self._legend = qt.QLabel(self) self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self.__selector_is_connected = False layout = qt.QVBoxLayout() layout.addWidget(self._view) layout.addWidget(self._hline) layout.addWidget(self._legend) layout.addWidget(self._selector) self.setLayout(layout) def getVolumeView(self): """Returns the plot used for the display :rtype: ScalarFieldView """ return self._view def normalizeComplexData(self, data): """ Converts a complex data array to its amplitude, if necessary. :param data: the data to normalize :return: """ if hasattr(data, "dtype"): isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating) else: isComplex = isinstance(data, numbers.Complex) if isComplex: data = numpy.absolute(data) return data def setData(self, signal, x_axis=None, y_axis=None, z_axis=None, signal_name=None, xlabel=None, ylabel=None, zlabel=None, title=None): """ :param signal: n-D dataset, whose last 3 dimensions are used as the 3D stack values. :param x_axis: 1-D dataset used as the image's x coordinates. If provided, its lengths must be equal to the length of the last dimension of ``signal``. :param y_axis: 1-D dataset used as the image's y. If provided, its lengths must be equal to the length of the 2nd to last dimension of ``signal``. :param z_axis: 1-D dataset used as the image's z. If provided, its lengths must be equal to the length of the 3rd to last dimension of ``signal``. :param signal_name: Label used in the legend :param xlabel: Label for X axis :param ylabel: Label for Y axis :param zlabel: Label for Z axis :param title: Graph title """ signal = self.normalizeComplexData(signal) if self.__selector_is_connected: self._selector.selectionChanged.disconnect(self._updateVolume) self.__selector_is_connected = False self.__signal = signal self.__signal_name = signal_name or "" self.__x_axis = x_axis self.__x_axis_name = xlabel self.__y_axis = y_axis self.__y_axis_name = ylabel self.__z_axis = z_axis self.__z_axis_name = zlabel self._selector.setData(signal) self._selector.setAxisNames(["Y", "X", "Z"]) self._view.setAxesLabels(self.__x_axis_name or 'X', self.__y_axis_name or 'Y', self.__z_axis_name or 'Z') self._updateVolume() # the legend label shows the selection slice producing the volume # (only interesting for ndim > 3) if signal.ndim > 3: self._selector.setVisible(True) self._legend.setVisible(True) self._hline.setVisible(True) else: self._selector.setVisible(False) self._legend.setVisible(False) self._hline.setVisible(False) if not self.__selector_is_connected: self._selector.selectionChanged.connect(self._updateVolume) self.__selector_is_connected = True def _updateVolume(self): """Update displayed stack according to the current axes selector data.""" data = self._selector.selectedData() x_axis = self.__x_axis y_axis = self.__y_axis z_axis = self.__z_axis offset = [] scale = [] for axis in [x_axis, y_axis, z_axis]: if axis is None: calibration = NoCalibration() elif len(axis) == 2: calibration = LinearCalibration(y_intercept=axis[0], slope=axis[1]) else: calibration = ArrayCalibration(axis) if not calibration.is_affine(): _logger.warning("Axis has not linear values, ignored") offset.append(0.) scale.append(1.) else: offset.append(calibration(0)) scale.append(calibration.get_slope()) legend = self.__signal_name + "[" for sl in self._selector.selection(): if sl == slice(None): legend += ":, " else: legend += str(sl) + ", " legend = legend[:-2] + "]" self._legend.setText("Displayed data: " + legend) self._view.setData(data, copy=False) self._view.setScale(*scale) self._view.setTranslation(*offset) self._view.setAxesLabels(self.__x_axis_name, self.__y_axis_name, self.__z_axis_name) def clear(self): old = self._selector.blockSignals(True) self._selector.clear() self._selector.blockSignals(old) self._view.setData(None)
def main(argv=None): # Parse input arguments parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('-l', '--level', nargs='?', type=float, default=float('nan'), help="The value at which to generate the iso-surface") parser.add_argument('-sx', '--xscale', nargs='?', type=float, default=1., help="The scale of the data on the X axis") parser.add_argument('-sy', '--yscale', nargs='?', type=float, default=1., help="The scale of the data on the Y axis") parser.add_argument('-sz', '--zscale', nargs='?', type=float, default=1., help="The scale of the data on the Z axis") parser.add_argument('-ox', '--xoffset', nargs='?', type=float, default=0., help="The offset of the data on the X axis") parser.add_argument('-oy', '--yoffset', nargs='?', type=float, default=0., help="The offset of the data on the Y axis") parser.add_argument('-oz', '--zoffset', nargs='?', type=float, default=0., help="The offset of the data on the Z axis") parser.add_argument('filename', nargs='?', default=None, help="""Filename to open. It supports 3D volume saved as .npy or in .h5 files. It also support nD data set (n>=3) stored in a HDF5 file. For HDF5, provide the filename and path as: <filename>::<path_in_file>. If the data set has more than 3 dimensions, it is possible to choose a 3D data set as a subset by providing the indices along the first n-3 dimensions with '#': <filename>::<path_in_file>#<1st_dim_index>...#<n-3th_dim_index> E.g.: data.h5::/data_5D#1#1 """) args = parser.parse_args(args=argv) # Start GUI global app # QApplication must be global to avoid seg fault on quit app = qt.QApplication([]) # Create the viewer main window window = ScalarFieldView() window.setAttribute(qt.Qt.WA_DeleteOnClose) # Create a parameter tree for the scalar field view treeView = SFViewParamTree.TreeView(window) treeView.setSfView(window) # Attach the parameter tree to the view # Add the parameter tree to the main window in a dock widget dock = qt.QDockWidget() dock.setWindowTitle('Parameters') dock.setWidget(treeView) window.addDockWidget(qt.Qt.RightDockWidgetArea, dock) # Load data from file if args.filename is not None: data = load(args.filename) _logger.info('Data:\n\tShape: %s\n\tRange: [%f, %f]', str(data.shape), data.min(), data.max()) else: # Create dummy data _logger.warning('Not data file provided, creating dummy data') coords = numpy.linspace(-10, 10, 64) z = coords.reshape(-1, 1, 1) y = coords.reshape(1, -1, 1) x = coords.reshape(1, 1, -1) data = numpy.sin(x * y * z) / (x * y * z) # Set ScalarFieldView data window.setData(data) # Set scale of the data window.setScale(args.xscale, args.yscale, args.zscale) # Set offset of the data window.setTranslation(args.xoffset, args.yoffset, args.zoffset) # Set axes labels window.setAxesLabels('X', 'Y', 'Z') # Add an iso-surface if not numpy.isnan(args.level): # Add an iso-surface at the given iso-level window.addIsosurface(args.level, '#FF0000FF') else: # Add an iso-surface from a function window.addIsosurface(default_isolevel, '#FF0000FF') window.show() return app.exec_()
# Create dummy data _logger.warning('Not data file provided, creating dummy data') coords = numpy.linspace(-10, 10, 64) z = coords.reshape(-1, 1, 1) y = coords.reshape(1, -1, 1) x = coords.reshape(1, 1, -1) data = numpy.sin(x * y * z) / (x * y * z) # Set ScalarFieldView data window.setData(data) # Set scale of the data window.setScale(args.xscale, args.yscale, args.zscale) # Set offset of the data window.setTranslation(args.xoffset, args.yoffset, args.zoffset) # Set axes labels window.setAxesLabels('X', 'Y', 'Z') # Add an iso-surface if not numpy.isnan(args.level): # Add an iso-surface at the given iso-level window.addIsosurface(args.level, '#FF0000FF') else: # Add an iso-surface from a function window.addIsosurface(default_isolevel, '#FF0000FF') window.show() app.exec_()