Source code for mdt.gui.maps_visualizer.main

import copy
import numbers
import os
import signal
from textwrap import dedent

import matplotlib
matplotlib.use('Qt5Agg')

import yaml
from PyQt5.QtCore import QTimer
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QDialog
from PyQt5.QtWidgets import QDialogButtonBox
from PyQt5.QtWidgets import QFileDialog
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtWidgets import QMessageBox

from mdt.gui.maps_visualizer.actions import NewConfigAction, SetMapsToShow, NewDataAction
from mdt.gui.maps_visualizer.config_tabs.tab_general import TabGeneral
from mdt.gui.maps_visualizer.config_tabs.tab_map_specific import TabMapSpecific
from mdt.gui.maps_visualizer.config_tabs.tab_textual import TabTextual
from mdt.gui.maps_visualizer.design.ui_save_image_dialog import Ui_SaveImageDialog
from mdt.lib.nifti import is_nifti_file

import mdt
from mdt.gui.maps_visualizer.base import DataConfigModel, \
    QtController
from mdt.gui.maps_visualizer.renderers.base import PlottingFrameInfoViewer
from mdt.visualization.maps.base import DataInfo, SimpleDataInfo, MapPlotConfig
from mdt.visualization.maps.utils import get_shortest_unique_names
from mdt.gui.maps_visualizer.renderers.matplotlib_renderer import MatplotlibPlotting
from mdt.gui.model_fit.design.ui_about_dialog import Ui_AboutDialog
from mdt.gui.utils import center_window, QtManager, get_script_file_header_text, image_files_filters, \
    enable_pyqt_exception_hook
from mdt.gui.maps_visualizer.design.ui_MainWindow import Ui_MapsVisualizer


[docs]class MapsVisualizerWindow(QMainWindow, Ui_MapsVisualizer): def __init__(self, controller, parent=None): """Instantiate the maps GUI Args: controller (mdt.gui.maps_visualizer.base.Controller): the controller to use for updating the views """ super().__init__(parent) self.setupUi(self) self._controller = controller self._controller.model_updated.connect(self.update_model) self.setAcceptDrops(True) self._coordinates_label = QLabel() self.statusBar().addPermanentWidget(self._coordinates_label) self.statusBar().setStyleSheet("QStatusBar::item { border: 0px solid black }; ") self.plotting_info_to_statusbar = PlottingFrameInfoToStatusBar(self._controller, self._coordinates_label) self.plotting_frame = MatplotlibPlotting(controller, parent=parent, plotting_info_viewer=self.plotting_info_to_statusbar) self.plotLayout.addWidget(self.plotting_frame) self.tab_general = TabGeneral(controller, self) self.generalTabPosition.addWidget(self.tab_general) self.tab_specific = TabMapSpecific(controller, self) self.mapSpecificTabPosition.addWidget(self.tab_specific) self.tab_textual = TabTextual(controller, self) self.textInfoTabPosition.addWidget(self.tab_textual) self.auto_rendering.setChecked(True) self.auto_rendering.stateChanged.connect(self._set_auto_rendering) self.manual_render.clicked.connect(lambda: self.plotting_frame.redraw()) self.actionNew_window.triggered.connect(lambda: start_gui(app_exec=False)) self.actionAbout.triggered.connect(lambda: AboutDialog(self).exec_()) self.actionAdd_new_files.triggered.connect(self._add_new_files) self.action_Clear.triggered.connect(self._remove_files) self.actionSaveImage.triggered.connect(lambda: ExportImageDialog(self, self.plotting_frame, self._controller).exec_()) self.actionSave_settings.triggered.connect(lambda: self._save_settings()) self.actionLoad_settings.triggered.connect(lambda: self._load_settings()) self.undo_config.setDisabled(not self._controller.has_undo()) self.redo_config.setDisabled(not self._controller.has_redo()) self.undo_config.clicked.connect(lambda: self._controller.undo()) self.redo_config.clicked.connect(lambda: self._controller.redo()) self._qdialog_basedir_set = False
[docs] @pyqtSlot(DataConfigModel) def update_model(self, model): self.undo_config.setDisabled(not self._controller.has_undo()) self.redo_config.setDisabled(not self._controller.has_redo()) self._set_qdialog_basedir()
def _set_qdialog_basedir(self): if not self._qdialog_basedir_set: data = self._controller.get_model().get_data() for map_name, file_path in data.get_file_paths().items(): if file_path: QFileDialog().setDirectory(file_path) self._qdialog_basedir_set = True return
[docs] def resizeEvent(self, event): ExportImageDialog.plot_frame_resized()
def _add_new_files(self): new_files = QFileDialog(self).getOpenFileNames(caption='Nifti files', filter=';;'.join(image_files_filters)) if new_files[0]: self._add_new_maps(new_files[0]) def _add_new_maps(self, paths): """Add the given set of file paths to the current visualization. This looks at the current set of file paths (from ``self._controller.get_config().get_data()``) and the given new set of file paths and creates a new merged dataset with all files. Since it is possible that adding new maps leads to naming collisions this function can rename both the old and the new maps to better reflect the map names. Args: paths (list of str): the list of file paths to add to the visualization Returns: list of str: the display names of the newly added maps """ def get_file_paths(data_info): """Get the file paths""" paths = [] for map_name in data_info.get_map_names(): file_path = data_info.get_file_path(map_name) if file_path: paths.append(file_path) else: paths.append(map_name) return paths def get_changes(old_data_info, new_data_info): additions = {} removals = [] name_updates = {} paths = get_file_paths(old_data_info) + get_file_paths(new_data_info) unique_names = get_shortest_unique_names(paths) new_map_names = unique_names[len(old_data_info.get_map_names()):] for old_name, new_name in zip(old_data_info.get_map_names(), unique_names): if old_name != new_name: removals.append(old_name) additions[new_name] = old_data_info.get_single_map_info(old_name) name_updates[old_name] = new_name for new_map_name, new_map_rename in zip(new_data_info.get_map_names(), new_map_names): additions[new_map_rename] = new_data_info.get_single_map_info(new_map_name) return additions, removals, name_updates, new_map_names current_model = self._controller.get_model() current_data = current_model.get_data() adds, rems, name_updates, new_map_names = get_changes(current_data, SimpleDataInfo.from_paths(paths)) data = current_data.get_updated(adds, removals=rems) config = copy.deepcopy(current_model.get_config()) new_maps_to_show = [] for map_name in config.maps_to_show: if map_name in name_updates: new_maps_to_show.append(name_updates[map_name]) else: new_maps_to_show.append(map_name) config.maps_to_show = new_maps_to_show new_map_plot_options = {} for map_name, plot_options in config.map_plot_options.items(): if map_name in name_updates: new_map_plot_options[name_updates[map_name]] = plot_options else: new_map_plot_options[map_name] = plot_options config.map_plot_options = new_map_plot_options if not len(current_data.get_map_names()): config.slice_index = data.get_max_slice_index(config.dimension) // 2 self._controller.apply_action(NewDataAction(data, config=config)) return new_map_names def _remove_files(self): data = SimpleDataInfo({}) config = MapPlotConfig() self._controller.apply_action(NewDataAction(data, config)) @pyqtSlot() def _set_auto_rendering(self): auto_render = self.auto_rendering.isChecked() self.plotting_frame.set_auto_rendering(auto_render) if auto_render: self.plotting_frame.redraw()
[docs] def send_sigint(self, *args): self.close()
[docs] def dragEnterEvent(self, event): """Function to allow dragging nifti files in the viewer for viewing purpose.""" if event.mimeData().hasUrls(): event.accept() else: event.ignore()
[docs] def dropEvent(self, event): """One or more files where dropped in the GUI, load all the nifti files among them.""" nifti_paths = [] for url in event.mimeData().urls(): path = url.toLocalFile() if os.path.isfile(path) and is_nifti_file(path): nifti_paths.append(path) additional_maps = self._add_new_maps(nifti_paths) map_names = copy.copy(self._controller.get_model().get_config().maps_to_show) map_names.extend(additional_maps) self._controller.apply_action(SetMapsToShow(map_names))
def _save_settings(self): """Save the current settings as a text file. """ current_model = self._controller.get_model() config_file = ['conf (*.conf)', 'All files (*)'] file_name, used_filter = QFileDialog().getSaveFileName(caption='Select the GUI config file', filter=';;'.join(config_file)) if file_name: with open(file_name, 'w') as f: f.write(current_model.get_config().to_yaml()) def _load_settings(self): config_file = ['conf (*.conf)', 'All files (*)'] file_name, used_filter = QFileDialog().getOpenFileName(caption='Select the GUI config file', filter=';;'.join(config_file)) if file_name: with open(file_name, 'r') as f: try: self._controller.apply_action(NewConfigAction(MapPlotConfig.from_yaml(f.read()))) except yaml.parser.ParserError: pass except yaml.scanner.ScannerError: pass except ValueError: pass
[docs] def set_window_title(self, title): if title is None: self.setWindowTitle('MDT Maps Visualizer') else: self.setWindowTitle('MDT Maps Visualizer - {}'.format(title))
[docs]class PlottingFrameInfoToStatusBar(PlottingFrameInfoViewer): def __init__(self, controller, status_bar_label): super().__init__() self._controller = controller self._status_bar_label = status_bar_label
[docs] def set_voxel_info(self, map_name, onscreen_coords, data_index): super().set_voxel_info(map_name, onscreen_coords, data_index) def format_value(v): if v == True or v == False: v = int(v) if not isinstance(v, numbers.Number): return 'Masked' value_format = '{:.3e}' if 1e-3 < v < 1e3: value_format = '{:.3f}' return value_format.format(v) data = self._controller.get_model().get_data() if map_name in data.get_map_names(): value = data.get_map_data(map_name)[tuple(data_index)] clipped = value config = self._controller.get_model().get_config() if map_name in config.map_plot_options: clipped = config.map_plot_options[map_name].clipping.apply(value) if clipped != value: self._status_bar_label.setText("{}, {}, {} ({})".format( onscreen_coords, data_index, format_value(clipped), format_value(value))) else: self._status_bar_label.setText("{}, {}, {}".format(onscreen_coords, data_index, format_value(value)))
[docs] def clear_voxel_info(self): super().clear_voxel_info() self._status_bar_label.setText("")
[docs]class ExportImageDialog(Ui_SaveImageDialog, QDialog): previous_values = {'width': None, 'height': None, 'dpi': None, 'output_file': None, 'writeScriptsAndConfig': False} def __init__(self, parent, plotting_frame, controller): super().__init__(parent) self._extension_filters = [['png', '(*.png)'], ['svg', '(*.svg)']] self.setupUi(self) self._plotting_frame = plotting_frame self._controller = controller self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Ok).clicked.connect(self._export_image) self.outputFile_box.textChanged.connect(self._update_ok_button) self.outputFile_chooser.clicked.connect(lambda: self._select_file()) if self.previous_values['width']: self.width_box.setValue(self.previous_values['width']) else: self.width_box.setValue(self._plotting_frame.width()) if self.previous_values['height']: self.height_box.setValue(self.previous_values['height']) else: self.height_box.setValue(self._plotting_frame.height()) if self.previous_values['dpi']: self.dpi_box.setValue(self.previous_values['dpi']) if self.previous_values['output_file']: self.outputFile_box.setText(self.previous_values['output_file']) if self.previous_values['writeScriptsAndConfig'] is not None: self.writeScriptsAndConfig.setChecked(self.previous_values['writeScriptsAndConfig'])
[docs] @staticmethod def plot_frame_resized(): ExportImageDialog.previous_values['width'] = None ExportImageDialog.previous_values['height'] = None
@pyqtSlot() def _update_ok_button(self): self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(self.outputFile_box.text() != '') def _select_file(self): graphical_image_filters = [' '.join(el) for el in self._extension_filters] + ['All files (*)'] open_file, used_filter = QFileDialog().getSaveFileName(caption='Select the output file', filter=';;'.join(graphical_image_filters)) if not any(open_file.endswith(el[0]) for el in self._extension_filters): extension_from_filter = list(filter(lambda v: ' '.join(v) == used_filter, self._extension_filters)) if extension_from_filter: extension = extension_from_filter[0][0] else: extension = self._extension_filters[0][0] open_file += '.{}'.format(extension) if open_file: self.outputFile_box.setText(open_file) self._update_ok_button() def _export_image(self): output_file = self.outputFile_box.text() if not any(output_file.endswith(el[0]) for el in self._extension_filters): output_file += '.{}'.format(self._extension_filters[0][0]) try: self._plotting_frame.export_image(output_file, self.width_box.value(), self.height_box.value(), dpi=self.dpi_box.value()) self.previous_values['width'] = self.width_box.value() self.previous_values['height'] = self.height_box.value() self.previous_values['dpi'] = self.dpi_box.value() self.previous_values['output_file'] = self.outputFile_box.text() self.previous_values['writeScriptsAndConfig'] = self.writeScriptsAndConfig.isChecked() if self.writeScriptsAndConfig.isChecked(): output_basename = os.path.splitext(output_file)[0] self._write_config_file(output_basename + '.conf') self._write_python_script_file(output_basename + '_script.py', output_basename + '.conf', output_file, self.width_box.value(), self.height_box.value(), self.dpi_box.value()) self._write_bash_script_file(output_basename + '_script.sh', output_basename + '.conf', output_file, self.width_box.value(), self.height_box.value(), self.dpi_box.value()) except PermissionError as error: msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText("Could not write the file to the given destination.") msg.setInformativeText(str(error)) msg.setWindowTitle("Permission denied") msg.exec_() def _write_config_file(self, output_basename): current_model = self._controller.get_model() with open(output_basename, 'w') as f: f.write(current_model.get_config().to_yaml()) def _write_python_script_file(self, script_fname, configuration_fname, output_image_fname, width, height, dpi): with open(script_fname, 'w') as f: f.write('#!/usr/bin/env python\n') f.write(dedent(''' {header} import mdt with open({config!r}, 'r') as f: config = f.read() mdt.view_maps( {paths}, config=config, save_filename={output_name!r}, figure_options={{'width': {width}, 'height': {height}, 'dpi': {dpi}}}) ''').format(header=get_script_file_header_text({'Purpose': 'Generate a results figure'}), paths='[' + ', '.join(['{el!r}'.format(el=el) for el in self._get_file_paths()]) + ']', config=configuration_fname, output_name=output_image_fname, width=width, height=height, dpi=dpi)) def _write_bash_script_file(self, script_fname, configuration_fname, output_image_fname, width, height, dpi): with open(script_fname, 'w') as f: f.write('#!/usr/bin/env bash\n') f.write(dedent(''' {header} mdt-view-maps \\ {paths} \\ --config "{config}" \\ --to-file "{output_name}" \\ --width {width} \\ --height {height} \\ --dpi {dpi} ''').format(header=get_script_file_header_text({'Purpose': 'Generate a results figure'}), paths=' '.join(['{el!r}'.format(el=el) for el in self._get_file_paths()]), config=configuration_fname, output_name=output_image_fname, width=width, height=height, dpi=dpi)) def _get_file_paths(self): data = self._controller.get_model().get_data() return list(data.get_file_paths().values())
[docs]class AboutDialog(Ui_AboutDialog, QDialog): def __init__(self, parent): super().__init__(parent) self.setupUi(self) self.contentLabel.setText(self.contentLabel.text().replace('{version}', mdt.__version__))
[docs]def start_gui(data=None, config=None, controller=None, app_exec=True, show_maximized=False, window_title=None): """Start the GUI with the given data and configuration. Args: data (DataInfo): the initial set of data config (MapPlotConfig): the initial configuration controller (mdt.gui.maps_visualizer.base.QtController): the controller to use in the application app_exec (boolean): if true we execute the Qt application, set to false to disable. show_maximized (true): if we want to show the window in a maximized state window_title (str): the title of the window Returns: MapsVisualizerWindow: the generated window """ controller = controller or QtController() enable_pyqt_exception_hook() app = QtManager.get_qt_application_instance() # catches the sigint timer = QTimer() timer.start(500) timer.timeout.connect(lambda: None) main = MapsVisualizerWindow(controller) main.set_window_title(window_title) signal.signal(signal.SIGINT, main.send_sigint) center_window(main) if show_maximized: main.showMaximized() main.show() if data is None: data = SimpleDataInfo({}) controller.apply_action(NewDataAction(data, config=config), store_in_history=False) QtManager.add_window(main) if app_exec: QtManager.exec_() return main