Source code for polo.widgets.slideshow_viewer

import copy
import math

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtGui import QFont

from polo import IMAGE_CLASSIFICATIONS, BLANK_IMAGE, make_default_logger
from polo.crystallography.image import Image
from polo.utils.dialog_utils import make_message_box
from polo.crystallography.run import HWIRun, Run

logger = make_default_logger(__name__)


[docs]class Slide(): '''Acts like a slide in a slideshow carousel. Holds an Image object instance as the contents of the slide. Forms a linked list with other slides through the `next_slide` and `prev_slide` attributes which act as the forwards and backwards pointers to other slides. :param image: Image that this slide will display :type image: Image :param next_slide: Next slide in the slideshow, defaults to None :type next_slide: Slide, optional :param prev_slide: Previous slide in the slideshow, defaults to None :type prev_slide: Slide, optional :param slide_number: Index of this slide in the slideshow, defaults to None :type slide_number: int, optional ''' def __init__(self, image, next_slide=None, prev_slide=None, slide_number=None): self.image = image # image object holds well data self.next_slide = next_slide self.prev_slide = prev_slide self.slide_number = slide_number def __repr__(self): return '{}: {}'.format(self.image.path, self.slide_number)
[docs]class PhotoViewer(QtWidgets.QGraphicsView): photoClicked = QtCore.pyqtSignal(QtCore.QPoint) def __init__(self, parent): super(PhotoViewer, self).__init__(parent) self.show_all_dates = False self.show_all_specs = False self._zoom = 0 self._empty = True self.scene = QtWidgets.QGraphicsScene(self) # self._photo = QtWidgets.QGraphicsPixmapItem() attempting to remove photo and just use via scenes self.setScene(self.scene) self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(30, 30, 30))) self.setFrameShape(QtWidgets.QFrame.NoFrame) self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag) # current image will be an actual image to show to the screen # slideshow images are images in the que that are ready to go # self.set_image(self._photo) need to use scene here
[docs] def hasPhoto(self): return not self._empty
[docs] def fitInView(self, scale=True): rect = self.scene.itemsBoundingRect() #rect = QtCore.QRectF(self._photo.pixmap().rect()) if not rect.isNull(): self._empty = False self.setSceneRect(rect) self.setScene(self.scene) # possibly do this instead or in addition to line above if self.hasPhoto(): unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1)) self.scale(1 / unity.width(), 1 / unity.height()) viewrect = self.viewport().rect() scenerect = self.transform().mapRect(rect) factor = min(viewrect.width() / scenerect.width(), viewrect.height() / scenerect.height()) self.scale(factor, factor) self._zoom = 0 else: self._empty = True
[docs] def add_pixmap(self, pixmap): '''Adds a `Pixmap` instances to the current sene. :param pixmap: Pixmap to add to the sene :type pixmap: Pixmap ''' self.scene.addPixmap(pixmap)
[docs] def wheelEvent(self, event): '''Handles mouse wheel events to allow for scaling for zooming in and out of the currently displayed image. :param event: Mouse scroll wheel event :type event: QEvent ''' if self.hasPhoto(): if event.angleDelta().y() > 0: factor = 1.25 self._zoom += 1 else: factor = 0.8 self._zoom -= 1 if self._zoom > 0: self.scale(factor, factor) elif self._zoom == 0: self.fitInView() else: self._zoom = 0
[docs] def toggleDragMode(self): '''Turns drag mode on and off. ''' if self.dragMode() == QtWidgets.QGraphicsView.ScrollHandDrag: self.setDragMode(QtWidgets.QGraphicsView.NoDrag) elif not self._photo.pixmap().isNull(): self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
[docs] def mousePressEvent(self, event): '''Handles mouse press events. :param event: Mouse press event :type event: QEvent ''' # if self.scene.isUnderMouse(): self.photoClicked.emit(self.mapToScene(event.pos()).toPoint()) super(PhotoViewer, self).mousePressEvent(event)
[docs]class SlideshowViewer(PhotoViewer): photoClicked = QtCore.pyqtSignal(QtCore.QPoint) '''Wrapper class around QGraphicsView and displays image to the user in the slideshow viewer tab of the main window. :param run: Current run whose images are to be shown by the viewer. :type run: Run :param parent: Parent Widget of this instance. :type parent: QWidget :param current_image: Image that is currently displayed by the viewer. :type current_image: Image ''' def __init__(self, parent, run=None, current_image=None): super(SlideshowViewer, self).__init__(parent) self.run = run self.current_image = current_image self._carousel = Carousel() logger.debug('Made {}'.format(self)) @property def run(self): return self._run @run.setter def run(self, new_run): '''Setter function for the run attribute. Updates the current slides by calling :meth:`~polo.widgets.slideshow_viewer.SlideshowViewer.update_slides_from_filters with arguments that ensure all images in the run are included. Effectively resets the images in the slideshow to reflect the new run. :param new_run: The run to replace the current run. :type new_run: Run ''' if isinstance(new_run, Run) or isinstance(new_run, HWIRun): self._run = new_run logger.info( 'Run attribute of {} set to {}'.format(self, self._run)) self.update_slides_from_filters( image_types=set([]), human=False, marco=False ) logger.info('Opened new run {}'.format(new_run)) else: # if run is none then interpret as request to delete current run self._run = None self._carousel = Carousel() self.scene.clear() self.current_image = None @property def current_slide_number(self): if self._carousel.current_slide: return self._carousel.current_slide.slide_number + 1 else: return 0 @property def total_slides(self): if self._carousel.current_slide: current_slide = self._carousel.current_slide slide_numbers, i = set([]), current_slide.slide_number while i not in slide_numbers: slide_numbers.add(i) current_slide = current_slide.next_slide i = current_slide.slide_number return max(slide_numbers) + 1
[docs] def _set_all_dates_scene(self, image): '''Private method that creates a time resolved view from the :class:`~polo.crystallography.image.Image` instance passed through the `image` argument. :param image: Image to create time resolved view from :type image: Image ''' if isinstance(image, Image): all_dates = image.get_linked_images_by_date() self.scene.clear() self.arrange_multi_image_scene(all_dates, render_date=True) self.fitInView() logger.debug('Displayed multi-date view') else: logger.warning('Attempted to add object of type {} to scene'.format( type(image) ))
[docs] def _set_all_spectrums_scene(self, image): '''Private method that creates a view that includes all alt spectrum images the :class:`~polo.crystallography.image.Image` instance is linked to. :param image: Image to create the view from :type image: Image ''' if isinstance(image, Image): all_specs = sorted(image.get_linked_images_by_spectrum(), key=lambda i: len(i.spectrum)) self.scene.clear() self.arrange_multi_image_scene(all_specs) self.fitInView() logger.debug('Displayed multi-spectrum view') else: logger.warning('Attempted to add object of type {} to scene'.format( type(image) ))
[docs] def _set_single_image_scene(self, image): '''Private method that creates a standard single image view from the :class:`~polo.crystallography.image.Image` instance passed to the `image` argument. :param image: Image to display :type image: Image ''' if isinstance(image, Image): if image.isNull(): image.setPixmap() self.scene.clear() self.scene.addPixmap(image) self.fitInView() logger.debug('Displayed single image view') else: logger.warning('Attempted to add object of type {} to scene'.format( type(image) ))
[docs] def _add_text_to_scene(self, text, x, y, size=40): '''Private method to add text on top of an image. Adds the text to the current scene at the position specified by the `x` and `y` arguments. :param text: Text to add to image :type text: str :param x: X cordinate of text :type x: int :param y: Y cordinate of text :type y: int :param size: Size of text, defaults to 40 :type size: int, optional ''' try: t = QtWidgets.QGraphicsTextItem() t.setPlainText(text) f = QFont() f.setPointSize(size) t.setFont(f) self.scene.addItem(t) t.setPos(x, y) except Exception as e: logger.error('Caught {} at {}'.format(e, self._add_text_to_scene))
[docs] def set_current_image_by_well_number(self, well_number): '''Set the current image to the :class:`~polo.crystallography.image.Image` instance associated with a specific well number. :param well_number: Well number to display :type well_number: int ''' if self.run: try: self.current_image = self.run.images[well_number-1] except Exception as e: logger.error('Caught {} while calling {}'.format( e, self.set_current_image_by_well_number)) return
[docs] def carousel_controls(self, next_image=False, previous_image=False): '''Wrapper around the :meth:`~polo.widgets.slideshow_viewer.Carousel.controls` method that allows image navigation. Does not actually display the image. :param next_image: If True, tells carousel to advance by one slide. :type next_image: bool :param previous_image: If True, tells carousel to retreat by one slide. :type previous_image: bool :returns The current image. :rtype Image ''' if isinstance(self._carousel, Carousel) and self._carousel.current_slide: if next_image: self._carousel.controls(next_slide=True) elif previous_image: self._carousel.controls(prev_slide=True) self.current_image = self._carousel.current_slide.image return self.current_image
[docs] def update_slides_from_filters(self, image_types, human, marco, favorite=False, sort_function=None): '''Creates new `Carousel` slides based on selected image filters. Sets the `current_image` attribute to the :class:`~polo.crystallography.image.Image` instance at the the `current slide` attribute of `_carousel` attribute. :param image_types: Set of image classifications to include in results. :type image_types: set or list :param human: If True, `image_types` refers to human classification of the image. :type human: bool :param marco: If True, `image_types` refers to the machine (MARCO) classification of the image. :type marco: bool ''' if self.run: images = list(self.run.image_filter_query( image_types, human, marco, favorite)) logger.debug('Filter returned {} image(s)'.format(len(images))) if len(images) == 1 and images[0].path == str(BLANK_IMAGE): make_message_box( parent=self, message='Filters returned no images!' ).exec_() self._carousel.add_slides(images, sort_function) self.current_image = self._carousel.current_slide.image
[docs] def arrange_multi_image_scene(self, image_list, render_date=False): '''Helper method to arrange multiple images into the same view. :param image_list: List of images to add to the view :type image_list: list :param render_date: If True adds a date label to each image, defaults to False :type render_date: bool, optional ''' x, y = 0, 0 # set starting cords for item in image_list: if isinstance(item, (list, tuple)): # 2D list pass list_midpoint = math.floor(len(item) / 2) for sub_item in item: if isinstance(item, Image): pass elif isinstance(item, Image): if item.isNull(): item.setPixmap() scene_item = self.scene.addPixmap(item) scene_item.setToolTip(item.get_tool_tip()) scene_item.setPos(x, y) if render_date and item.date: self._add_text_to_scene(item.formated_date, x, y) x += item.width()
[docs] def display_current_image(self): '''Renders the Image instance currently stored in the `current_image` attribute. ''' cur_img = self.current_image if isinstance(cur_img, Image): # parse the flags on how to display the image here if self.show_all_dates: self._set_all_dates_scene(cur_img) elif self.show_all_specs: self._set_all_spectrums_scene(cur_img) else: self._set_single_image_scene(cur_img) else: logger.warning('Attempted to display object of type {}'.format( type(self.current_image) ))
[docs] def get_cur_img_cocktail_str(self): '''Retruns the `current_image` cocktail information as a string. :return: Cocktail information string :rtype: str ''' if isinstance(self.current_image, Image): cocktail_string = str(self.current_image.cocktail) # NOTE: User on Mac Mojave reported issue with meta data text # not updating but no errors related in the log file return cocktail_string
[docs] def get_cur_img_meta_str(self): '''Returns the `current_image` metadata as a string. :return: Metadata string :rtype: str ''' if isinstance(self.current_image, Image): image_string = str(self.current_image) return image_string
[docs] def set_alt_image(self, next_date=False, prev_date=False, alt_spec=False): '''Sets the `current_image` attribute to a linked image specified by one of the three boolean flags. :param next_date: If True, sets the `current_image` to the next image by date :param prev_date: If True, sets the `current_image` to the previous image by date :param alt_spec: If True, sets the `current_image` to an alt spectrum image ''' cur_img = self.current_image if next_date and cur_img.next_image: self.current_image = cur_img.next_image elif prev_date and cur_img.previous_image: self.current_image = cur_img.previous_image elif alt_spec and cur_img.alt_image: self.current_image = cur_img.alt_image
[docs] def classify_current_image(self, classification): '''Changes the human classification of the current image. ''' if isinstance(self.current_image, Image): self.current_image.human_class = classification