针对pulse-transit的工具
This commit is contained in:
421
dist/client/mne/viz/backends/_utils.py
vendored
Normal file
421
dist/client/mne/viz/backends/_utils.py
vendored
Normal file
@@ -0,0 +1,421 @@
|
||||
#
|
||||
# Authors: The MNE-Python contributors.
|
||||
# License: BSD-3-Clause
|
||||
# Copyright the MNE-Python contributors.
|
||||
|
||||
import collections.abc
|
||||
import functools
|
||||
import os
|
||||
import platform
|
||||
import signal
|
||||
import sys
|
||||
from colorsys import rgb_to_hls
|
||||
from contextlib import contextmanager
|
||||
from ctypes import c_char_p, c_void_p, cdll
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ...fixes import _compare_version
|
||||
from ...utils import _check_qt_version, _validate_type, logger, warn
|
||||
from ..utils import _get_cmap
|
||||
|
||||
VALID_BROWSE_BACKENDS = (
|
||||
"qt",
|
||||
"matplotlib",
|
||||
)
|
||||
|
||||
VALID_3D_BACKENDS = (
|
||||
"pyvistaqt", # default 3d backend
|
||||
"notebook",
|
||||
)
|
||||
ALLOWED_QUIVER_MODES = ("2darrow", "arrow", "cone", "cylinder", "sphere", "oct")
|
||||
_ICONS_PATH = Path(__file__).parents[2] / "icons"
|
||||
|
||||
|
||||
def _get_colormap_from_array(
|
||||
colormap=None, normalized_colormap=False, default_colormap="coolwarm"
|
||||
):
|
||||
from matplotlib.colors import ListedColormap
|
||||
|
||||
if colormap is None:
|
||||
cmap = _get_cmap(default_colormap)
|
||||
elif isinstance(colormap, str):
|
||||
cmap = _get_cmap(colormap)
|
||||
elif normalized_colormap:
|
||||
cmap = ListedColormap(colormap)
|
||||
else:
|
||||
cmap = ListedColormap(np.array(colormap) / 255.0)
|
||||
return cmap
|
||||
|
||||
|
||||
def _check_color(color):
|
||||
from matplotlib.colors import colorConverter
|
||||
|
||||
if isinstance(color, str):
|
||||
color = colorConverter.to_rgb(color)
|
||||
elif isinstance(color, collections.abc.Iterable):
|
||||
np_color = np.array(color)
|
||||
if np_color.size % 3 != 0 and np_color.size % 4 != 0:
|
||||
raise ValueError("The expected valid format is RGB or RGBA.")
|
||||
if np_color.dtype in (np.int64, np.int32):
|
||||
if (np_color < 0).any() or (np_color > 255).any():
|
||||
raise ValueError("Values out of range [0, 255].")
|
||||
elif np_color.dtype == np.float64:
|
||||
if (np_color < 0.0).any() or (np_color > 1.0).any():
|
||||
raise ValueError("Values out of range [0.0, 1.0].")
|
||||
else:
|
||||
raise TypeError(
|
||||
"Expected data type is `np.int64`, `np.int32`, or `np.float64` but "
|
||||
f"{np_color.dtype} was given."
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Expected type is `str` or iterable but {type(color)} was given."
|
||||
)
|
||||
return color
|
||||
|
||||
|
||||
def _alpha_blend_background(ctable, background_color):
|
||||
alphas = ctable[:, -1][:, np.newaxis] / 255.0
|
||||
use_table = ctable.copy()
|
||||
use_table[:, -1] = 255.0
|
||||
return (use_table * alphas) + background_color * (1 - alphas)
|
||||
|
||||
|
||||
@functools.lru_cache(1)
|
||||
def _qt_init_icons():
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
QIcon.setThemeSearchPaths([str(_ICONS_PATH)] + QIcon.themeSearchPaths())
|
||||
QIcon.setFallbackThemeName("light")
|
||||
return str(_ICONS_PATH)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _qt_disable_paint(widget):
|
||||
paintEvent = widget.paintEvent
|
||||
widget.paintEvent = lambda *args, **kwargs: None
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
widget.paintEvent = paintEvent
|
||||
|
||||
|
||||
_QT_ICON_KEYS = dict(app=None)
|
||||
|
||||
|
||||
def _init_mne_qtapp(enable_icon=True, pg_app=False, splash=False):
|
||||
"""Get QApplication-instance for MNE-Python.
|
||||
|
||||
Parameter
|
||||
---------
|
||||
enable_icon: bool
|
||||
If to set an MNE-icon for the app.
|
||||
pg_app: bool
|
||||
If to create the QApplication with pyqtgraph. For an until know
|
||||
undiscovered reason the pyqtgraph-browser won't show without
|
||||
mkQApp from pyqtgraph.
|
||||
splash : bool | str
|
||||
If not False, display a splash screen. If str, set the message
|
||||
to the given string.
|
||||
|
||||
Returns
|
||||
-------
|
||||
app : ``qtpy.QtWidgets.QApplication``
|
||||
Instance of QApplication.
|
||||
splash : ``qtpy.QtWidgets.QSplashScreen``
|
||||
Instance of QSplashScreen. Only returned if splash is True or a
|
||||
string.
|
||||
"""
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtGui import QGuiApplication, QIcon, QPixmap
|
||||
from qtpy.QtWidgets import QApplication, QSplashScreen
|
||||
|
||||
app_name = "MNE-Python"
|
||||
organization_name = "MNE"
|
||||
|
||||
# Fix from cbrnr/mnelab for app name in menu bar
|
||||
# This has to come *before* the creation of the QApplication to work.
|
||||
# It also only affects the title bar, not the application dock.
|
||||
# There seems to be no way to change the application dock from "python"
|
||||
# at runtime.
|
||||
if sys.platform.startswith("darwin"):
|
||||
try:
|
||||
# set bundle name on macOS (app name shown in the menu bar)
|
||||
from Foundation import NSBundle
|
||||
|
||||
bundle = NSBundle.mainBundle()
|
||||
info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
|
||||
if "CFBundleName" not in info:
|
||||
info["CFBundleName"] = app_name
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
# First we need to check to make sure the display is valid, otherwise
|
||||
# Qt might segfault on us
|
||||
app = QApplication.instance()
|
||||
if not (app or _display_is_valid()):
|
||||
raise RuntimeError("Cannot connect to a valid display")
|
||||
|
||||
if pg_app:
|
||||
from pyqtgraph import mkQApp
|
||||
|
||||
old_argv = sys.argv
|
||||
try:
|
||||
sys.argv = []
|
||||
app = mkQApp(app_name)
|
||||
finally:
|
||||
sys.argv = old_argv
|
||||
elif not app:
|
||||
app = QApplication([app_name])
|
||||
app.setApplicationName(app_name)
|
||||
app.setOrganizationName(organization_name)
|
||||
qt_version = _check_qt_version(check_usable_display=False)
|
||||
# HiDPI is enabled by default in Qt6, requires to be explicitly set for Qt5
|
||||
if _compare_version(qt_version, "<", "6.0"):
|
||||
app.setAttribute(Qt.AA_UseHighDpiPixmaps)
|
||||
|
||||
if enable_icon or splash:
|
||||
icons_path = _qt_init_icons()
|
||||
|
||||
if (
|
||||
enable_icon
|
||||
and app.windowIcon().cacheKey() != _QT_ICON_KEYS["app"]
|
||||
and app.windowIcon().isNull() # don't overwrite existing icon (e.g. MNELAB)
|
||||
):
|
||||
# Set icon
|
||||
kind = "bigsur_" if platform.mac_ver()[0] >= "10.16" else "default_"
|
||||
icon = QIcon(f"{icons_path}/mne_{kind}icon.png")
|
||||
app.setWindowIcon(icon)
|
||||
_QT_ICON_KEYS["app"] = app.windowIcon().cacheKey()
|
||||
|
||||
out = app
|
||||
if splash:
|
||||
pixmap = QPixmap(f"{icons_path}/mne_splash.png")
|
||||
pixmap.setDevicePixelRatio(QGuiApplication.primaryScreen().devicePixelRatio())
|
||||
args = (pixmap,)
|
||||
if _should_raise_window():
|
||||
args += (Qt.WindowStaysOnTopHint,)
|
||||
qsplash = QSplashScreen(*args)
|
||||
qsplash.setAttribute(Qt.WA_ShowWithoutActivating, True)
|
||||
if isinstance(splash, str):
|
||||
alignment = int(Qt.AlignBottom | Qt.AlignHCenter)
|
||||
qsplash.showMessage(splash, alignment=alignment, color=Qt.white)
|
||||
qsplash.show()
|
||||
app.processEvents()
|
||||
out = (out, qsplash)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _display_is_valid():
|
||||
# Adapted from matplotilb _c_internal_utils.py
|
||||
if sys.platform != "linux":
|
||||
return True
|
||||
if os.getenv("DISPLAY"): # if it's not there, don't bother
|
||||
libX11 = cdll.LoadLibrary("libX11.so.6")
|
||||
libX11.XOpenDisplay.restype = c_void_p
|
||||
libX11.XOpenDisplay.argtypes = [c_char_p]
|
||||
display = libX11.XOpenDisplay(None)
|
||||
if display is not None:
|
||||
libX11.XCloseDisplay.argtypes = [c_void_p]
|
||||
libX11.XCloseDisplay(display)
|
||||
return True
|
||||
# not found, try Wayland
|
||||
if os.getenv("WAYLAND_DISPLAY"):
|
||||
libwayland = cdll.LoadLibrary("libwayland-client.so.0")
|
||||
if libwayland is not None:
|
||||
if all(
|
||||
hasattr(libwayland, f"wl_display_{kind}connect") for kind in ("", "dis")
|
||||
):
|
||||
libwayland.wl_display_connect.restype = c_void_p
|
||||
libwayland.wl_display_connect.argtypes = [c_char_p]
|
||||
display = libwayland.wl_display_connect(None)
|
||||
if display:
|
||||
libwayland.wl_display_disconnect.argtypes = [c_void_p]
|
||||
libwayland.wl_display_disconnect(display)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# https://stackoverflow.com/questions/5160577/ctrl-c-doesnt-work-with-pyqt
|
||||
def _qt_app_exec(app):
|
||||
# adapted from matplotlib
|
||||
old_signal = signal.getsignal(signal.SIGINT)
|
||||
is_python_signal_handler = old_signal is not None
|
||||
if is_python_signal_handler:
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
try:
|
||||
# Make IPython Console accessible again in Spyder
|
||||
app.lastWindowClosed.connect(app.quit)
|
||||
app.exec_()
|
||||
finally:
|
||||
# reset the SIGINT exception handler
|
||||
if is_python_signal_handler:
|
||||
signal.signal(signal.SIGINT, old_signal)
|
||||
|
||||
|
||||
def _qt_detect_theme():
|
||||
try:
|
||||
import darkdetect
|
||||
|
||||
theme = darkdetect.theme().lower()
|
||||
except ModuleNotFoundError:
|
||||
logger.info(
|
||||
'For automatic theme detection, "darkdetect" has to'
|
||||
" be installed! You can install it with "
|
||||
"`pip install darkdetect`"
|
||||
)
|
||||
theme = "light"
|
||||
except Exception:
|
||||
theme = "light"
|
||||
return theme
|
||||
|
||||
|
||||
def _qt_get_stylesheet(theme):
|
||||
_validate_type(theme, ("path-like",), "theme")
|
||||
theme = str(theme)
|
||||
stylesheet = "" # no stylesheet
|
||||
if theme in ("auto", "dark", "light"):
|
||||
if theme == "auto":
|
||||
return stylesheet
|
||||
assert theme in ("dark", "light")
|
||||
system_theme = _qt_detect_theme()
|
||||
if theme == system_theme:
|
||||
return stylesheet
|
||||
_, api = _check_qt_version(return_api=True)
|
||||
# On macOS or Qt 6, we shouldn't need to set anything when the requested
|
||||
# theme matches that of the current OS state
|
||||
try:
|
||||
import qdarkstyle
|
||||
except ModuleNotFoundError:
|
||||
logger.info(
|
||||
f'To use {theme} mode when in {system_theme} mode, "qdarkstyle" has'
|
||||
"to be installed! You can install it with:\n"
|
||||
"pip install qdarkstyle\n"
|
||||
)
|
||||
else:
|
||||
if api in ("PySide6", "PyQt6") and _compare_version(
|
||||
qdarkstyle.__version__, "<", "3.2.3"
|
||||
):
|
||||
warn(
|
||||
f"Setting theme={repr(theme)} is not supported for {api} in "
|
||||
f"qdarkstyle {qdarkstyle.__version__}, it will be ignored. "
|
||||
"Consider upgrading qdarkstyle to >=3.2.3."
|
||||
)
|
||||
else:
|
||||
stylesheet = qdarkstyle.load_stylesheet(
|
||||
getattr(
|
||||
getattr(qdarkstyle, theme).palette,
|
||||
f"{theme.capitalize()}Palette",
|
||||
)
|
||||
)
|
||||
return stylesheet
|
||||
else:
|
||||
try:
|
||||
file = open(theme)
|
||||
except OSError:
|
||||
warn(
|
||||
"Requested theme file not found, will use light instead: "
|
||||
f"{repr(theme)}"
|
||||
)
|
||||
else:
|
||||
with file as fid:
|
||||
stylesheet = fid.read()
|
||||
return stylesheet
|
||||
|
||||
|
||||
def _should_raise_window():
|
||||
from matplotlib import rcParams
|
||||
|
||||
return rcParams["figure.raise_window"]
|
||||
|
||||
|
||||
def _qt_raise_window(widget):
|
||||
# Set raise_window like matplotlib if possible
|
||||
if _should_raise_window():
|
||||
widget.activateWindow()
|
||||
widget.raise_()
|
||||
|
||||
|
||||
def _qt_is_dark(widget):
|
||||
# Ideally this would use CIELab, but this should be good enough
|
||||
win = widget.window()
|
||||
bgcolor = win.palette().color(win.backgroundRole()).getRgbF()[:3]
|
||||
return rgb_to_hls(*bgcolor)[1] < 0.5
|
||||
|
||||
|
||||
def _pixmap_to_ndarray(pixmap):
|
||||
from qtpy.QtGui import QImage
|
||||
|
||||
img = pixmap.toImage()
|
||||
img = img.convertToFormat(QImage.Format.Format_RGBA8888)
|
||||
ptr = img.bits()
|
||||
count = img.height() * img.width() * 4
|
||||
if hasattr(ptr, "setsize"): # PyQt
|
||||
ptr.setsize(count)
|
||||
data = np.frombuffer(ptr, dtype=np.uint8, count=count).copy()
|
||||
data.shape = (img.height(), img.width(), 4)
|
||||
return data / 255.0
|
||||
|
||||
|
||||
def _notebook_vtk_works():
|
||||
if sys.platform != "linux":
|
||||
return True
|
||||
# check if it's OSMesa -- if it is, continue
|
||||
try:
|
||||
from vtkmodules import vtkRenderingOpenGL2
|
||||
|
||||
vtkRenderingOpenGL2.vtkOSOpenGLRenderWindow
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
return True # has vtkOSOpenGLRenderWindow (OSMesa build)
|
||||
|
||||
# if it's not OSMesa, we need to check display validity
|
||||
if _display_is_valid():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _qt_safe_window(
|
||||
*, splash="figure.splash", window="figure.plotter.app_window", always_close=True
|
||||
):
|
||||
def dec(meth, splash=splash, always_close=always_close):
|
||||
@functools.wraps(meth)
|
||||
def func(self, *args, **kwargs):
|
||||
close_splash = always_close
|
||||
error = False
|
||||
try:
|
||||
meth(self, *args, **kwargs)
|
||||
except Exception:
|
||||
close_splash = error = True
|
||||
raise
|
||||
finally:
|
||||
for attr, do_close in ((splash, close_splash), (window, error)):
|
||||
if attr is None or not do_close:
|
||||
continue
|
||||
parent = self
|
||||
name = attr.split(".")[-1]
|
||||
try:
|
||||
for n in attr.split(".")[:-1]:
|
||||
parent = getattr(parent, n)
|
||||
if name:
|
||||
widget = getattr(parent, name, False)
|
||||
else: # empty string means "self"
|
||||
widget = parent
|
||||
if widget:
|
||||
widget.close()
|
||||
del widget
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
delattr(parent, name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return func
|
||||
|
||||
return dec
|
||||
Reference in New Issue
Block a user