Source code for spacr.qt.widgets.channel_sort_dialog

"""Make Masks' example-set check.

Thin Qt over :mod:`spacr.channel_sorting`, which holds every decision:

* :class:`ExampleSetsDialog` shows three detected sets side by side, every
  channel with its mask, and asks whether they are right.
"""
from __future__ import annotations

import os
from typing import Dict, List, Optional

import numpy as np
from PySide6.QtCore import QThread, Qt, Signal
from PySide6.QtGui import QImage, QPixmap
from PySide6.QtWidgets import (
    QDialog, QGridLayout, QHBoxLayout, QLabel, QPushButton, QVBoxLayout,
    QWidget,
)

from ... import channel_sorting as cs
from ..bridge import emit_safely
from ..i18n import tr

#: Thumbnail side in the image list, in pixels.
THUMB = 64

def _grey_icon(array: Optional[np.ndarray], mask: Optional[np.ndarray] = None,
               size: int = THUMB) -> QPixmap:
    """A pixmap of a grey thumbnail, tinted red where ``mask`` has objects.

    :param array: 2-D ``uint8`` pixels, or None for a blank tile.
    :param mask: 2-D ``uint8`` mask preview of the same size, or None.
    :param size: the tile's side when ``array`` is None.
    """
    if array is None:
        pixmap = QPixmap(size, size)
        pixmap.fill(Qt.gray)
        return pixmap
    rgb = np.repeat(array[:, :, None], 3, axis=2).copy()
    if mask is not None and mask.shape == array.shape:
        on = mask > 0
        rgb[on, 0] = np.maximum(rgb[on, 0], 200)
        rgb[on, 1] = (rgb[on, 1] * 0.5).astype(np.uint8)
        rgb[on, 2] = (rgb[on, 2] * 0.5).astype(np.uint8)
    rgb = np.ascontiguousarray(rgb)
    height, width = rgb.shape[:2]
    image = QImage(rgb.data, width, height, 3 * width, QImage.Format_RGB888)
    return QPixmap.fromImage(image.copy())


class _ThumbWorker(QThread):
    """Reads thumbnails off the GUI thread, one signal per image."""

    ready = Signal(str, object, object)

    def __init__(self, folder: str, names: List[str],
                 masks_dir: Optional[str] = None, parent=None, *, size=None):
        """Remember what to read.

        :param folder: the image folder.
        :param names: the images, in list order.
        :param masks_dir: an explicit masks folder.
        :param parent: the Qt parent.
        :param size: source sampling limit; defaults to the persisted quality.
        """
        super().__init__(parent)
        from ..mask_thumbnail_quality import QUALITY_SIZES, get_quality

        self._size = int(size or QUALITY_SIZES[get_quality()])
        self._folder = folder
        self._names = list(names)
        self._masks_dir = masks_dir
        self._stop = False

    def stop(self) -> None:
        """Ask the loop to end after the image it is reading."""
        self._stop = True

    def run(self) -> None:
        """Emit ``(name, thumbnail, mask thumbnail)`` for each image."""
        for name in self._names:
            if self._stop:
                return
            try:
                image = cs.thumbnail(os.path.join(self._folder, name), self._size)
                mask_path = cs.mask_for(self._folder, name, self._masks_dir)
                mask = (cs.mask_thumbnail(mask_path, self._size)
                        if mask_path else None)
            except Exception:
                image, mask = None, None
            if not emit_safely(self.ready, name, image, mask):
                return


[docs] class ExampleSetsDialog(QDialog): """Three sets side by side -- every channel and its mask -- and a question.""" def __init__(self, folder: str, sets: Dict[cs.SetKey, Dict[int, str]], masks_dir: Optional[str] = None, parent=None): """Lay out up to three sets: the first, the middle and the last. :param folder: the image folder. :param sets: ``{set key: {channel: name}}``. :param masks_dir: an explicit masks folder. :param parent: the Qt parent. """ super().__init__(parent) self.setWindowTitle(tr("Are these sets right?")) layout = QVBoxLayout(self) intro = QLabel(tr( "spaCR paired each image with the most similar image of the same " "size in every other channel. Here are three of the {n} sets it " "found, each channel with its mask in red. Are they right?", n=len(sets))) intro.setWordWrap(True) layout.addWidget(intro) keys = list(sets) picks = sorted({0, len(keys) // 2, len(keys) - 1}) if keys else [] self.shown = [keys[i] for i in picks] self._preview_tiles = [] grid = QGridLayout() for row, key in enumerate(self.shown): for column, (channel, name) in enumerate(sorted(sets[key].items())): path = os.path.join(folder, name) mask_path = cs.mask_for(folder, name, masks_dir) tile = QLabel() tile.setFixedSize(160, 160) tile.setAlignment(Qt.AlignCenter) self._preview_tiles.append((tile, path, mask_path)) caption = QLabel(tr("Channel {channel}: {name}", channel=channel, name=name)) caption.setWordWrap(True) cell = QVBoxLayout() cell.addWidget(tile) cell.addWidget(caption) holder = QWidget() holder.setLayout(cell) grid.addWidget(holder, row, column) layout.addLayout(grid) from ..mask_thumbnail_quality import changes changes.changed.connect(self._refresh_previews) self._refresh_previews() buttons = QHBoxLayout() self.yes_button = QPushButton(tr("Yes, these are right")) self.no_button = QPushButton(tr("No")) self.yes_button.clicked.connect(self.accept) self.no_button.clicked.connect(self.reject) buttons.addStretch(1) buttons.addWidget(self.no_button) buttons.addWidget(self.yes_button) layout.addLayout(buttons) def _refresh_previews(self, _quality=None): """Refresh aligned source/mask previews without changing tile geometry. :param _quality: optional notification of a shared quality change. """ from ..hidpi import scaled_for from ..mask_thumbnail_quality import QUALITY_SIZES, get_quality size = max(160, QUALITY_SIZES[get_quality()]) for tile, path, mask_path in self._preview_tiles: picture = _grey_icon(cs.thumbnail(path, size), cs.mask_thumbnail(mask_path, size) if mask_path else None) tile.setPixmap(scaled_for(picture, tile, 160))