Source code for mantidimaging.gui.widgets.bad_data_overlay.bad_data_overlay

# Copyright (C) 2021 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
from __future__ import annotations

from abc import abstractmethod, ABC
from collections.abc import Callable
from typing import Final

import numpy as np
from pyqtgraph import ColorMap, ImageItem, ViewBox
from dataclasses import dataclass

from mantidimaging.gui.utility.qt_helpers import _metaclass_sip_abc
from mantidimaging.gui.widgets.indicator_icon.view import IndicatorIconView
from mantidimaging.core.utility import finder

OVERLAY_COLOUR_NAN = [200, 0, 200, 255]
OVERLAY_COLOUR_ZERO = [255, 140, 0, 255]
OVERLAY_COLOUR_NEGATIVE = [0, 180, 0, 255]
OVERLAY_COLOUR_MESSAGE = [0, 120, 255, 255]


def _is_zero(data: np.ndarray) -> np.ndarray:
    """
    Check for zero values
    """
    return data == 0


def _is_negative(data: np.ndarray) -> np.ndarray:
    """
    Check for negative values
    """
    return data < 0


[docs] @dataclass(frozen=True) class CheckConfig: name: str color: list[int] position: int check_function: Callable[[np.ndarray], np.ndarray] message: str
VALUE_CHECKS: Final[tuple[CheckConfig, ...]] = ( CheckConfig("zero", OVERLAY_COLOUR_ZERO, 1, _is_zero, "Zero values"), CheckConfig("negative", OVERLAY_COLOUR_NEGATIVE, 2, _is_negative, "Negative values"), )
[docs] class BadDataCheck: check_function: Callable[[np.ndarray], np.ndarray] indicator: IndicatorIconView def __init__(self, check_function, indicator, overlay, color): self.check_function = check_function self.indicator = indicator self.overlay = overlay self.color = color self.setup_overlay() self.indicator.connected_overlay = self.overlay
[docs] def do_check(self, data) -> None: bad_data = self.check_function(data) any_bad = bad_data.any() # cast any_bad to python bool to prevent DeprecationWarning self.indicator.setVisible(bool(any_bad)) self.overlay.setImage(bad_data, autoLevels=False)
[docs] def setup_overlay(self) -> None: color = np.array([[0, 0, 0, 0], self.color], dtype=np.ubyte) color_map = ColorMap([0, 1], color) self.overlay.setVisible(False) lut = color_map.getLookupTable(0, 1, 2) self.overlay.setLookupTable(lut) self.overlay.setZValue(11) self.overlay.setLevels([0, 1])
[docs] def remove(self) -> None: self.overlay.getViewBox().removeItem(self.indicator) self.overlay.getViewBox().removeItem(self.overlay) self.overlay.clear()
[docs] def clear(self) -> None: self.indicator.setVisible(False) self.overlay.clear()
[docs] class BadDataOverlay(ABC, metaclass=_metaclass_sip_abc): """ Mixin class to be used with MIImageView and MIMiniImageView """ def __init__(self) -> None: super().__init__() self.enabled_checks: dict[str, BadDataCheck] = {} self.message_indicator: IndicatorIconView | None = None if hasattr(self, "sigTimeChanged"): self.sigTimeChanged.connect(self.check_for_bad_data) @property @abstractmethod def image_item(self) -> ImageItem: ... @property @abstractmethod def viewbox(self) -> ViewBox: ...
[docs] def enable_value_check(self, enable: bool = True, actions: list[tuple[str, Callable]] | None = None) -> None: """ Enable or disable all values checks (zero, negative, NaN) """ if enable: for config in VALUE_CHECKS: self.enable_check(config.name, config.color, config.position, config.check_function, config.message, actions) else: for config in VALUE_CHECKS: self.disable_check(config.name)
[docs] def enable_nan_check(self, enable: bool = True, actions: list[tuple[str, Callable]] | None = None) -> None: if enable: self.enable_check("nan", OVERLAY_COLOUR_NAN, 0, np.isnan, "Invalid values: Not a number", actions) else: self.disable_check("nan")
[docs] def enable_check(self, name: str, color: list[int], pos: int, func: Callable, message: str, actions: list[tuple[str, Callable]] | None) -> None: if name not in self.enabled_checks: icon_path = (finder.ROOT_PATH / "gui" / "ui" / "images" / "exclamation-triangle-red.png").as_posix() indicator = IndicatorIconView(self.viewbox, icon_path, pos, color, message) if actions is not None: indicator.add_actions(actions) overlay = ImageItem() self.viewbox.addItem(overlay) check = BadDataCheck(func, indicator, overlay, color) self.enabled_checks[name] = check self.check_for_bad_data()
[docs] def disable_check(self, name: str) -> None: if name in self.enabled_checks: self.enabled_checks[name].remove() self.enabled_checks.pop(name, None)
def _get_current_slice(self) -> np.ndarray | None: data = self.image_item.image return data
[docs] def check_for_bad_data(self) -> None: current_slice = self._get_current_slice() if current_slice is not None: for test in self.enabled_checks.values(): test.do_check(current_slice)
[docs] def clear_overlays(self) -> None: for check in self.enabled_checks.values(): check.clear()
[docs] def enable_message(self, enable: bool = True) -> None: if enable: icon_path = (finder.ROOT_PATH / "gui" / "ui" / "images" / "exclamation-triangle-red.png").as_posix() self.message_indicator = IndicatorIconView(self.viewbox, icon_path, 0, OVERLAY_COLOUR_MESSAGE, "") self.message_indicator.setVisible(False) else: self.message_indicator = None
[docs] def show_message(self, message: str | None) -> None: if self.message_indicator is None: return if message: self.message_indicator.set_message(message) self.message_indicator.setVisible(True) else: self.message_indicator.setVisible(False)