Source code for polo.widgets.plate_inspector_widget

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QBitmap, QBrush, QColor, QIcon, QPainter, QPixmap
from PyQt5.QtWidgets import QGraphicsColorizeEffect, QGraphicsScene
from PyQt5.QtCore import Qt, pyqtSignal
from polo import (ALLOWED_IMAGE_COUNTS, COLORS, IMAGE_CLASSIFICATIONS)
from polo.crystallography.cocktail import UnitValue
from polo.crystallography.image import Image
from polo.crystallography.run import HWIRun, Run
from polo.threads.thread import QuickThread
from polo.designer.UI_plate_inspector_widget import Ui_PlateInspector
from polo.utils.io_utils import write_screen_html, RunSerializer, SceneExporter
from polo.utils.dialog_utils import make_message_box
from polo.plots.plots import StaticCanvas
from polo import make_default_logger

logger = make_default_logger(__name__)

[docs]class PlateInspectorWidget(QtWidgets.QWidget): images_per_page = [16, 64, 96] def __init__(self, parent, run=None): '''The :class:`PlateInspectorWidget` is a primary run interface widget and is designed to emulate the MarcoScopeJ image viewer with extended functionality. It allows users to view their screening images in grids of pre-set numbers of images from 24 to 96 images at a time. :param parent: Parent widget :type parent: QWidget :param run: Run to display, defaults to None :type run: Run, optional ''' super(PlateInspectorWidget, self).__init__(parent) self.ui = Ui_PlateInspector() self.ui.setupUi(self) self.run = run self.ui.plateViewer.run = self._run self._set_color_comboboxs() self._set_color_options() self._set_image_count_options() self.ui.pushButton_18.clicked.connect( lambda: self.show_current_plate(next_view=True) ) self.ui.pushButton_17.clicked.connect( lambda: self.show_current_plate(prev_view=True) ) self.image_type_checkboxes = dict(zip(IMAGE_CLASSIFICATIONS, [self.ui.checkBox_23, self.ui.checkBox_24, self.ui.checkBox_25, self.ui.checkBox_26])) # define :class:`QCheckBox` instances that determine image # classification filters and zip with the image classifications # doesnt matter what is assigned to what as long as order in # image classificaions list does not change self.ui.pushButton_19.clicked.connect(self.apply_plate_settings) self.ui.plateViewer.preserve_aspect = self.ui.checkBox_29.isChecked() self.ui.checkBox_29.stateChanged.connect(self.set_aspect_ratio_mode) # self.ui.pushButton_24.clicked.connect(self.reset_all) self.ui.pushButton_23.clicked.connect(self.show_current_plate) self.ui.pushButton_21.clicked.connect( lambda: self.show_current_plate(next_date=True)) self.ui.pushButton_20.clicked.connect( lambda: self.show_current_plate(prev_date=True) ) self.ui.pushButton_22.clicked.connect( lambda: self.show_current_plate(alt_spec=True) ) self.ui.plateViewer.images_per_page = self.images_per_page[0] self.ui.comboBox_7.currentIndexChanged.connect(self._set_images_per_page) self.ui.plateViewer.changed_images_per_page_signal.connect( self.ui.plateVis.setup_view ) self.ui.plateViewer.changed_page_signal.connect( self.ui.plateVis.set_selected_block) self.ui.pushButton.clicked.connect(self.export_current_view) self.ui.spinBox.setRange(1, 1) self.ui.spinBox.valueChanged.connect(self._set_current_page) self._set_time_resolved_buttons() self._set_alt_spectrum_buttons() @property def selected_classifications(self): '''Image classifications that are selected via the image filtering :class:`QCheckBox` instances. Also see the :attr:`~PlateInspectorWidget.image_type_checkboxes` property. :return: List of currently selected image classifications. Images who's classification is in this list should be shown / emphasized to the user. :rtype: list ''' return [k for k in self.image_type_checkboxes if self.image_type_checkboxes[k].isChecked()] @property def human(self): '''Status of human image classification :class:`QCheckBox`. :return: State of the human filter :class:`QCheckBox` :rtype: bool ''' return self.ui.checkBox_21.isChecked() @property def marco(self): '''Status of marco image classification :class:`QCheckBox`. :return: State of the marco filter :class:`QCheckBox` :rtype: bool ''' return self.ui.checkBox_22.isChecked() @property def favorite(self): '''Status of the `favorite` :class:`QCheckBox` filter. :return: State of the favorite :class:`QCheckBox` :rtype: bool ''' return self.ui.checkBox_6.isChecked() @property def color_mapping(self): '''Creates a color mapping dictionary that reflects the currently selected color selector :class:`QComboBox` instances. The dictionary maps each image classification to a :class:`QColor` instance that can then be used to color images in the plate viewer. ''' mapping = {} for each_combo_box, img_class in zip(self.color_combos, IMAGE_CLASSIFICATIONS): mapping[img_class] = COLORS[each_combo_box.currentText()] return mapping @property def run(self): return self._run @run.setter def run(self, new_run): '''Sets run attribute to the given run. Setting the run also sets the run for the `plateViewer` widget and checks if time resolved and spectrum navigation should be enabled by calling :meth:`~polo.widgets.plate_inspector_widget.PlateInspector._set_time_resolved_buttons` and :meth:`~polo.widgets.plate_inspector_widget.PlateInspector._set_alt_spectrum_buttons`. ''' if new_run: self._run = new_run self.ui.plateViewer.run = new_run logger.info('Opened new run {}'.format(new_run)) else: self._run = None self.ui.plateViewer.run = None self._set_time_resolved_buttons() self._set_alt_spectrum_buttons()
[docs] def _set_images_per_page(self): '''Private method that tells the :class:`plateViewer` UI widget to set its :attr:`images_per_page` atttribute to the value specified in the images per page :class:`QComboBox`. ''' self.ui.plateViewer.images_per_page = self.images_per_page[ self.ui.comboBox_7.currentIndex()] logger.info('Set images per page to {}'.format( self.ui.plateViewer.images_per_page))
[docs] def _set_color_comboboxs(self): '''Private method that sets the label text associated with each color selector :class:`QComboBox`. Should be called in the `__init__` method before the widget is shown to the user. ''' self.color_combos = [ self.ui.comboBox_3, self.ui.comboBox, self.ui.comboBox_5, self.ui.comboBox_4 ] labels = [ self.ui.label_19, self.ui.label_4, self.ui.label_21, self.ui.label_20 ] for each_label, each_class in zip(labels, IMAGE_CLASSIFICATIONS): each_label.setText(each_class)
[docs] def _set_image_count_options(self): '''Private method to be called in the `__init__` method that sets the allowed number of images per page. ''' self.ui.comboBox_7.clear() self.ui.comboBox_7.addItems([str(i) for i in self.images_per_page]) self.ui.comboBox_7.setCurrentIndex(0)
[docs] def _set_color_options(self): '''Private methods that uses the :const:`COLORS` constant to set the color options for each color selector :class:`QComboBox` instance in the image coloring tab. ''' colors, i = list(COLORS.keys()), 0 for each_combobox in self.color_combos: each_combobox.addItems(colors) if colors[i] == 'none': i+=1 each_combobox.setCurrentIndex(i) i += 1
[docs] def _parse_label_checkboxes(self): '''Private method that reads values from :class:`QCheckBox` instances related to image filtering. Returns a dictionary where keys are the labels of the :class:`QCheckBox` instances which should also be the possible image classifications and values are the state of the :class:`QCheckBox` (True or False). Returned dictionary will have following structure. .. code-block:: python { 'Crystals': True, 'Clear': False, 'Precipitate': True, 'Other': False } :return: Dict of :class:`QCheckBox` states. :rtype: dict ''' boxes = [ self.ui.checkBox, self.ui.checkBox_2, self.ui.checkBox_3, self.ui.checkBox_4, self.ui.checkBox_5 ] return {b.text(): b.isChecked() for b in boxes}
[docs] def _set_plate_label(self): '''Private method to change the plate label to tell the user what view or "page" they are currently looking at. ''' self.ui.label_18.setText( 'Page {} of {}'.format( self.ui.plateViewer.current_page, self.ui.plateViewer.total_pages) )
[docs] def _apply_color_mapping(self): '''Applies the current color mapping to displayed images. Images are colored based on either their human or marco classification. ''' human = self.ui.radioButton_2.isChecked() self.ui.plateViewer.set_scene_colors_from_filters( self.color_mapping, self.ui.horizontalSlider.value() / 100, human )
[docs] def _apply_image_filters(self): '''Wrapper function around `plateViewer` :meth:`~polo.widgets.plate_viewer.plateViewer.deemphasize_filtered_images` which changes the opacity of currently displayed images based on their classifications. ''' self.ui.plateViewer.set_scene_opacity_from_filters( self.selected_classifications, self.human, self.marco, self.favorite )
[docs] def _set_time_resolved_buttons(self): '''Private helper function that determines if navigation buttons that display alt spectrum images, previous and next date images can be used. ''' if self._run: if self._run.next_run: self.ui.pushButton_21.setEnabled(True) else: self.ui.pushButton_21.setEnabled(False) if self._run.previous_run: self.ui.pushButton_20.setEnabled(True) else: self.ui.pushButton_20.setEnabled(False) else: self.ui.pushButton_20.setEnabled(False) self.ui.pushButton_21.setEnabled(False)
[docs] def _set_alt_spectrum_buttons(self): '''Private helper function similar to :meth:`~polo.widgets.plate_viewer.plateViewer._set_time_resolved_buttons` that determines if the navigation button that allows users to view alt spectrum images should be enabled. If conditions are not met then the button is disabled. ''' if self._run and self._run.alt_spectrum: self.ui.pushButton_22.setEnabled(True) else: self.ui.pushButton_22.setEnabled(False)
[docs] def _set_spin_box_range(self): '''Set the allowed range for the page navigation spinbox. ''' self.ui.spinBox.setRange(1, self.ui.plateViewer.total_pages)
[docs] def _set_current_page(self, page_number): '''Set the current page number and show the view for that page by calling :meth:`~polo.widgets.plate_inspector_widget.PlateInspector.show_current_page` :param page_number: The new page number :type page_number: int ''' self.ui.plateViewer.current_page = page_number self.show_current_plate()
[docs] def set_aspect_ratio_mode(self): '''Sets the `preserve_aspect` attribute based on the status of the preserve aspect ratio :class:`QCheckBox`. Preserving the aspect ratio results in displaying undistorted crystallization images but utilizes available display space less efficiently. ''' self.ui.plateViewer.preserve_aspect = self.ui.checkBox_29.isChecked()
[docs] def reset_all(self): '''Method to un-check all user selected settings. ''' for widget in self.ui.groupBox_26.children(): if isinstance(widget, QtWidgets.QCheckBox): widget.setChecked(False) self.apply_plate_settings()
[docs] def apply_plate_settings(self): '''Parses :class:`QCheckBox` instances in the Plate View tab to determine what behavior of the :class:`plateViewer` widget is requested by the user. ''' if self._run: self.ui.plateViewer.images_per_page = self.images_per_page[ self.ui.comboBox_7.currentIndex()] if self.ui.checkBox_27.isChecked(): self._apply_image_filters() else: self.ui.plateViewer.emphasize_all_images() if self.ui.checkBox_28.isChecked(): self._apply_color_mapping() else: self.ui.plateViewer.decolor_all_images()
[docs] def show_current_plate(self, next_view=False, prev_view=False, next_date=False, prev_date=False, alt_spec=False): '''Show the images belonging to the current plate view to the user. :param next_date: Flag, if True show equivalent images from future date, defaults to False :type next_date: bool, optional :param prev_date: Flag, if True shows equivalent images from past date, defaults to False :type prev_date: bool, optional :param alt_spec: Flag if True shows equivalent images in alternative imaging spectrum, defaults to False :type alt_spec: bool, optional ''' if next_date and self._run.next_run: self.run = self._run.next_run elif prev_date and self._run.previous_run: self.run = self._run.previous_run elif alt_spec and self._run.alt_spectrum: self.run = self._run.alt_spectrum if next_view: self.ui.plateViewer.current_page += 1 elif prev_view: self.ui.plateViewer.current_page -= 1 label_dict = self._parse_label_checkboxes() self.ui.plateViewer.tile_images_onto_scene(label_dict=label_dict) self.apply_plate_settings() self._set_plate_label() self._set_time_resolved_buttons() self._set_alt_spectrum_buttons() self._set_spin_box_range() self.update()
[docs] def export_current_view(self): try: save_path = QtWidgets.QFileDialog.getSaveFileName( self, 'Save View' )[0] if save_path: save_path = RunSerializer.path_suffix_checker(save_path, '.png') self.export_view_thread = QuickThread( SceneExporter.write_image, scene=self.ui.plateViewer._scene, file_path=save_path ) def finished_saving_image(): if isinstance(self.export_view_thread.result, str): message = 'View saved to {}'.format(save_path) else: message = 'Write to {} failed {}'.format( save_path, self.export_view_thread.result) make_message_box(parent=self, message=message).exec_() self.export_view_thread.finished.connect(finished_saving_image) self.export_view_thread.start() except Exception as e: if hasattr(self, 'export_view_thread') and self.export_view_thread.isRunning(): self.export_view_thread.exit() logger.error('Caught {} at {}'.format(e, self.export_current_view)) make_message_box( parent=self, message='Failed to export the current view.').exec_()