diff --git a/tests/quadStatusBar_test.py b/tests/quadStatusBar_test.py new file mode 100644 index 000000000..4f70ebbd9 --- /dev/null +++ b/tests/quadStatusBar_test.py @@ -0,0 +1,321 @@ +############################################################################### +# volumina: volume slicing and editing library +# +# Copyright (C) 2011-2019, the ilastik developers +# +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the Lesser GNU General Public License +# as published by the Free Software Foundation; either version 2.1 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# See the files LICENSE.lgpl2 and LICENSE.lgpl3 for full text of the +# GNU Lesser General Public License version 2.1 and 3 respectively. +# This information is also available on the ilastik web site at: +# http://ilastik.org/license/ +############################################################################### +import pytest +from PyQt5.QtWidgets import QMainWindow +from PyQt5.QtCore import QPointF, Qt, QCoreApplication +from PyQt5.QtGui import QColor + +import numpy as np +import vigra +from ilastik.applets.layerViewer.layerViewerGui import LayerViewerGui +from volumina.volumeEditor import VolumeEditor +from volumina.volumeEditorWidget import VolumeEditorWidget +from volumina.layerstack import LayerStackModel +from volumina.pixelpipeline.datasources import LazyflowSource +from volumina.layer import AlphaModulatedLayer, ColortableLayer, NormalizableLayer, RGBALayer +from lazyflow.graph import Operator, InputSlot, OutputSlot, Graph + +# taken from https://sashat.me/2017/01/11/list-of-20-simple-distinct-colors/ +default16_new = [ + QColor(0, 0, 0, 0).rgba(), # transparent + QColor(255, 225, 25).rgba(), # yellow + QColor(0, 130, 200).rgba(), # blue + QColor(230, 25, 75).rgba(), # red + QColor(70, 240, 240).rgba(), # cyan + QColor(60, 180, 75).rgba(), # green + QColor(250, 190, 190).rgba(), # pink + QColor(170, 110, 40).rgba(), # brown + QColor(145, 30, 180).rgba(), # purple + QColor(0, 128, 128).rgba(), # teal + QColor(245, 130, 48).rgba(), # orange + QColor(240, 50, 230).rgba(), # magenta + QColor(210, 245, 60).rgba(), # lime + QColor(255, 215, 180).rgba(), # coral + QColor(230, 190, 255).rgba(), # lavender + QColor(128, 128, 128).rgba(), # gray +] + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.volumeEditorWidget = VolumeEditorWidget(parent=self) + + +class OpTestImageSlots(Operator): + """test operator, containing 3-dim test data""" + GrayscaleIn = InputSlot() + RgbaIn = InputSlot(level=1) + ColorTblIn1 = InputSlot() + ColorTblIn2 = InputSlot() + AlphaModulatedIn = InputSlot() + Segmentation1In = InputSlot() + Segmentation2In = InputSlot() + + GrayscaleOut = OutputSlot() + RgbaOut = OutputSlot(level=1) + ColorTblOut1 = OutputSlot() + ColorTblOut2 = OutputSlot() + AlphaModulatedOut = OutputSlot() + Segmentation1Out = OutputSlot() + Segmentation2Out = OutputSlot() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + time, width, height, depth = 4, 300, 200, 10 + self.dataShape = (time, width, height, depth, 1) + shape4d = (time, width, height, depth, 1) + shape5d1 = (4, time, width, height, depth, 1) + shape5d2 = (time, width, height, depth, 3) + + # create images + grayscaleImage = np.random.randint(0, 255, shape4d) + rgbaImage = np.random.randint(0, 255, shape5d1) + colorTblImage1 = np.random.randint(0, 10, shape4d) + colorTblImage2 = np.random.randint(0, 10, shape4d) + AlphaModImage = np.zeros(shape5d2, dtype=np.int32) + Segment1Image = np.zeros(shape5d2, dtype=np.int32) + Segment2Image = np.zeros(shape5d2, dtype=np.int32) + + # define some dummy segmentations + for t in range(time): + for x in range(width): + for y in range(height): + for z in range(depth): + if t==3 and z in range(5, 9) and y in range(20, 30) and x in range(80, 140): + Segment1Image[t, x, y, z, :] = [255, 255, 255] + AlphaModImage[t, x, y, z, :] = [255, 255, 255] + colorTblImage1[t, x, y, z, :] = 0 + if t==1 and z in range(0, 6) and y in range(100, 150) and x in range(10, 60): + Segment2Image[t, x, y, z, :] = [255, 255, 255] + colorTblImage2[t, x, y, z, :] = 0 + + self.GrayscaleIn.setValue(grayscaleImage, notify=False, check_changed=False) + self.RgbaIn.setValues(rgbaImage) + self.ColorTblIn1.setValue(colorTblImage1, notify=False, check_changed=False) + self.ColorTblIn2.setValue(colorTblImage2, notify=False, check_changed=False) + self.AlphaModulatedIn.setValue(AlphaModImage, notify=False, check_changed=False) + self.Segmentation1In.setValue(Segment1Image, notify=False, check_changed=False) + self.Segmentation2In.setValue(Segment2Image, notify=False, check_changed=False) + + atags3d = "txyzc"[5 - len(shape4d):] + atags4d2 = "txyzc"[5 - len(shape5d2):] + self.GrayscaleIn.meta.axistags = vigra.defaultAxistags(atags3d) + for i in range(4): + self.RgbaIn[i].meta.axistags = vigra.defaultAxistags(atags3d) + self.ColorTblIn1.meta.axistags = vigra.defaultAxistags(atags3d) + self.ColorTblIn2.meta.axistags = vigra.defaultAxistags(atags3d) + self.AlphaModulatedIn.meta.axistags = vigra.defaultAxistags(atags4d2) + self.Segmentation1In.meta.axistags = vigra.defaultAxistags(atags4d2) + self.Segmentation2In.meta.axistags = vigra.defaultAxistags(atags4d2) + + self.GrayscaleOut.connect(self.GrayscaleIn) + self.RgbaOut.connect(self.RgbaIn) + self.ColorTblOut1.connect(self.ColorTblIn1) + self.ColorTblOut2.connect(self.ColorTblIn2) + self.AlphaModulatedOut.connect(self.AlphaModulatedIn) + self.Segmentation1Out.connect(self.Segmentation1In) + self.Segmentation2Out.connect(self.Segmentation2In) + + +class TestSpinBoxImageView(object): + + def updateAllTiles(self, imageScenes): + for scene in imageScenes: + scene.joinRenderingAllTiles() + + @pytest.fixture(autouse=True) + def setupClass(self, qtbot): + + self.qtbot = qtbot + self.main = MainWindow() + self.layerStack = LayerStackModel() + + g = Graph() + self.op = OpTestImageSlots(graph=g) + + self.grayscaleLayer = LayerViewerGui._create_grayscale_layer_from_slot(self.op.GrayscaleOut, 1) + self.segLayer1 = AlphaModulatedLayer(LazyflowSource(self.op.Segmentation1Out), tintColor=QColor(Qt.cyan), + range=(0, 255), normalize=(0, 255)) + self.segLayer2 = AlphaModulatedLayer(LazyflowSource(self.op.Segmentation2Out), tintColor=QColor(Qt.yellow), + range=(0, 255), normalize=(0, 255)) + self.alphaModLayer = AlphaModulatedLayer(LazyflowSource(self.op.AlphaModulatedOut), + tintColor=QColor(Qt.magenta), range=(0, 255), normalize=(0, 255)) + self.colorTblLayer1 = ColortableLayer(LazyflowSource(self.op.ColorTblOut1), default16_new) + self.colorTblLayer2 = ColortableLayer(LazyflowSource(self.op.ColorTblOut2), default16_new) + self.rgbaLayer = RGBALayer(red=LazyflowSource(self.op.RgbaOut[0]), green=LazyflowSource(self.op.RgbaOut[1]), + blue=LazyflowSource(self.op.RgbaOut[2]), alpha=LazyflowSource(self.op.RgbaOut[3])) + self.emptyRgbaLayer = RGBALayer() + + self.segLayer1.name = "Segmentation (Label 1)" + self.segLayer2.name = "Segmentation (Label 2)" + self.grayscaleLayer.name = "Raw Input" + self.colorTblLayer1.name = "Labels" + self.colorTblLayer2.name = "pos info in Normalizable" + self.rgbaLayer.name = "rgba layer" + self.emptyRgbaLayer.name = "empty rgba layer" + self.alphaModLayer.name = "alpha modulated layer" + + self.layerStack.append(self.grayscaleLayer) + self.layerStack.append(self.segLayer1) + self.layerStack.append(self.segLayer2) + self.layerStack.append(self.colorTblLayer1) + self.layerStack.append(self.colorTblLayer2) + self.layerStack.append(self.rgbaLayer) + self.layerStack.append(self.emptyRgbaLayer) + self.layerStack.append(self.alphaModLayer) + + self.editor = VolumeEditor(self.layerStack, self.main) + self.editorWidget = self.main.volumeEditorWidget + self.editorWidget.init(self.editor) + + self.editor.dataShape = self.op.dataShape + + # Find the xyz origin + midpos5d = [x // 2 for x in self.op.dataShape] + # center viewer there + # set xyz position + midpos3d = midpos5d[1:4] + self.editor.posModel.slicingPos = midpos3d + self.editor.navCtrl.panSlicingViews(midpos3d, [0, 1, 2]) + for i in range(3): + self.editor.navCtrl.changeSliceAbsolute(midpos3d[i], i) + + self.main.setCentralWidget(self.editorWidget) + self.main.setFixedSize(1000, 800) + self.main.show() + self.qtbot.addWidget(self.main) + + def testAddingAndRemovingPosVal(self): + assert 0 == len(self.editorWidget.quadViewStatusBar.layerValueWidgets) + + for layer in self.layerStack: + layer.visible = True + if not layer.showPosValue: + layer.showPosValue = True + if not layer.visible: + layer.visible = True + + for i in range(30): + QCoreApplication.processEvents() + + for layer in self.layerStack: + assert layer in self.editorWidget.quadViewStatusBar.layerValueWidgets + + self.layerStack[0].showPosValue = False + assert self.layerStack[0] not in self.editorWidget.quadViewStatusBar.layerValueWidgets + self.layerStack[2].showPosValue = False + assert self.layerStack[2] not in self.editorWidget.quadViewStatusBar.layerValueWidgets + self.layerStack[1].showPosValue = False + assert self.layerStack[1] not in self.editorWidget.quadViewStatusBar.layerValueWidgets + self.layerStack[2].showPosValue = True + assert self.layerStack[2] in self.editorWidget.quadViewStatusBar.layerValueWidgets + + def testLayerPositionValueStrings(self): + for layer in self.layerStack: + if not layer.showPosValue: + layer.showPosValue = True + if not layer.visible: + layer.visible = True + + t, x, y, z = 3, 90, 25, 6 + + grayValidationString = "Raw I.:" + str(self.op.GrayscaleIn.value[t, x, y, z, 0]) + labelValidationString = "Label 1" + colorTbl1ValidationString = "-" + colorTbl2ValidationString = "pos i. i. N.:" + str(self.op.ColorTblOut2.value[t, x, y, z, 0]) + rgbaValidationString = "rgba l.:{};{};{};{}".format(*[str(slot.value[t, x, y, z, 0]) for slot in self.op.RgbaIn]) + emptyRgbaValidationString = "empty r. l.:0;0;0;255" + alphaModValidationString = "alpha m. l.:" + str(self.op.AlphaModulatedOut.value[t, x, y, z, 0]) + + + signal = self.editor.posModel.cursorPositionChanged + with self.qtbot.waitSignal(signal, timeout=1000): + self.editor.navCtrl.changeSliceAbsolute(z, 2) + self.editor.navCtrl.changeTime(t) + + QCoreApplication.processEvents() + QCoreApplication.processEvents() + QCoreApplication.processEvents() + QCoreApplication.processEvents() + + + self.editor.navCtrl.positionDataCursor(QPointF(0, 0), 2) + + self.editor.navCtrl.positionDataCursor(QPointF(x, y), 2) + + assert self.editorWidget.quadViewStatusBar.xSpinBox.value() == x + assert self.editorWidget.quadViewStatusBar.ySpinBox.value() == y + assert self.editorWidget.quadViewStatusBar.zSpinBox.value() == z + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.grayscaleLayer].text() == grayValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.segLayer1].text() == labelValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.segLayer2].text() == labelValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.colorTblLayer1].text() == colorTbl1ValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.colorTblLayer2].text() == colorTbl2ValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.rgbaLayer].text() == rgbaValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.emptyRgbaLayer].text() == emptyRgbaValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.alphaModLayer].text() == alphaModValidationString + + t, x, y, z = 1, 39, 130, 3 + + grayValidationString = "Raw I.:" + str(self.op.GrayscaleIn.value[t, x, y, z, 0]) + labelValidationString = "Label 2" + colorTbl1ValidationString = "Labels:" + str(self.op.ColorTblIn1.value[t, x, y, z, 0]) + colorTbl2ValidationString = "pos i. i. N.:" + str(self.op.ColorTblIn2.value[t, x, y, z, 0]) + rgbaValidationString = "rgba l.:{};{};{};{}".format(*[str(slot.value[t, x, y, z, 0]) for slot in self.op.RgbaIn]) + emptyRgbaValidationString = "empty r. l.:0;0;0;255" + alphaModValidationString = "alpha m. l.:" + str(self.op.AlphaModulatedIn.value[t, x, y, z, 0]) + + with self.qtbot.waitSignal(signal, timeout=1000): + self.editor.navCtrl.changeSliceAbsolute(y, 1) + self.editor.navCtrl.changeTime(t) + + self.editor.navCtrl.positionDataCursor(QPointF(x, z), 1) + + assert self.editorWidget.quadViewStatusBar.xSpinBox.value() == x + assert self.editorWidget.quadViewStatusBar.ySpinBox.value() == y + assert self.editorWidget.quadViewStatusBar.zSpinBox.value() == z + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.grayscaleLayer].text() == grayValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.segLayer1].text() == labelValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.segLayer2].text() == labelValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.colorTblLayer1].text() == colorTbl1ValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.colorTblLayer2].text() == colorTbl2ValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.rgbaLayer].text() == rgbaValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.emptyRgbaLayer].text() == emptyRgbaValidationString + assert self.editorWidget.quadViewStatusBar.layerValueWidgets[ + self.alphaModLayer].text() == alphaModValidationString + diff --git a/tests/test_pixelpipeline/test_datasources/test_cachesource.py b/tests/test_pixelpipeline/test_datasources/test_cachesource.py new file mode 100644 index 000000000..8d6ae9db0 --- /dev/null +++ b/tests/test_pixelpipeline/test_datasources/test_cachesource.py @@ -0,0 +1,66 @@ +import pytest +from unittest import mock + +import numpy as np +from numpy.testing import assert_array_equal +from PyQt5.QtCore import QObject, pyqtSignal + +from volumina.pixelpipeline.datasources.cachesource import CacheSource + + +class DummySource(QObject): + isDirty = pyqtSignal(object) + numberOfChannelsChanged = pyqtSignal(int) + + class _Req: + def __init__(self, arr): + self._result = arr + + def wait(self): + return self._result + + def __init__(self, data): + self._data = data + super().__init__() + + def set_data(self, value): + self._data = value + self.isDirty.emit(np.s_[:]) + + def request(self, slicing): + return self._Req(self._data[slicing]) + + +class TestCacheSource: + @pytest.fixture + def orig_source(self): + arr = np.arange(27).reshape(3, 3, 3) + source = DummySource(arr) + return mock.Mock(wraps=source) + + @pytest.fixture + def cached_source(self, orig_source): + return CacheSource(orig_source) + + def test_consecutive_requests_are_cached(self, cached_source, orig_source): + slicing = np.s_[1:2, 1:2, 1:2] + res0 = cached_source.request(slicing).wait() + res1 = cached_source.request(slicing).wait() + + assert_array_equal(np.array([[[13]]]), res0) + assert_array_equal(np.array([[[13]]]), res1) + + orig_source.request.assert_called_once_with(slicing) + + def test_cache_invalidation(self, cached_source, orig_source): + slicing = np.s_[1:2, 1:2, 1:2] + res0 = cached_source.request(slicing).wait() + res1 = cached_source.request(slicing).wait() + + assert_array_equal(np.array([[[13]]]), res0) + assert_array_equal(np.array([[[13]]]), res1) + + orig_source.set_data(np.arange(27, 54).reshape(3, 3, 3)) + res2 = cached_source.request(slicing).wait() + assert_array_equal(np.array([[[40]]]), res2) + assert orig_source.request.call_count == 2 diff --git a/tests/test_utility/test_cache.py b/tests/test_utility/test_cache.py new file mode 100644 index 000000000..932c161c1 --- /dev/null +++ b/tests/test_utility/test_cache.py @@ -0,0 +1,66 @@ +import sys +import weakref + +import pytest +import numpy as np + +from volumina.utility.cache import KVCache + + +class TestKVCache: + BYTES_OVERHEAD = 33 + + @pytest.fixture + def cache(self): + cache = KVCache(mem_limit=8192) + cache.register_type(bytes, lambda obj: sys.getsizeof(obj)) + cache.register_type(np.ndarray, lambda obj: sys.getsizeof(obj)) + return cache + + def get_size(self, bytes_str): + return self.BYTES_OVERHEAD + len(bytes_str) + + def test_setting_value(self, cache): + cache.set("key", b"test") + assert "key" in cache + assert b"test" == cache.get("key") + + def test_memory(self, cache): + cache.set("key", b"test") + assert self.get_size(b"test") == cache.used_memory + + def test_memory_on_setting_same_key(self, cache): + cache.set("key", b"test") + cache.set("key", b"testtest") + + assert self.get_size(b"testtest") == cache.used_memory + + def test_eviction(self, cache): + for idx in range(10): + cache.set(f"key{idx}", (b"%d" % idx) * 1000) + + assert 10 * self.get_size(b"0" * 1000) == cache.used_memory + + cache.clean() + + assert "key0" not in cache + assert "key1" not in cache + assert "key2" not in cache + assert "key3" in cache + + def test_eviction_weakref(self, cache): + t = weakref.WeakValueDictionary() + + for idx in range(10): + val = np.arange(200) + t[f"key{idx}"] = val + cache.set(f"key{idx}", val) + + cache.clean() + + assert "key1" not in cache + assert "key2" not in cache + assert "key9" in cache + + assert "key1" not in t + assert "key9" in t diff --git a/volumina/layer.py b/volumina/layer.py index 006f04521..1a53e1a9e 100644 --- a/volumina/layer.py +++ b/volumina/layer.py @@ -22,9 +22,10 @@ from builtins import range import colorsys import numpy +import numbers from PyQt5.QtCore import Qt, QObject, pyqtSignal, QTimer -from PyQt5.QtGui import QColor, QPen +from PyQt5.QtGui import QColor, QPen, qGray from volumina.interpreter import ClickInterpreter from volumina.pixelpipeline.slicesources import PlanarSliceSource @@ -62,6 +63,7 @@ class Layer(QObject): somethingChanged signals is emitted.""" changed = pyqtSignal() + showPosValueChanged = pyqtSignal(object, bool) visibleChanged = pyqtSignal(bool) opacityChanged = pyqtSignal(float) nameChanged = pyqtSignal(object) # sends a python str object, not unicode! @@ -82,6 +84,15 @@ def visible(self, value): self._visible = value self.visibleChanged.emit(value) + @property + def showPosValue(self): + return self._showPosValue + + @showPosValue.setter + def showPosValue(self, value): + self._showPosValue = value + self.showPosValueChanged.emit(self, value) + def toggleVisible(self): """Convenience function.""" if self._allowToggleVisible: @@ -101,6 +112,15 @@ def opacity(self, value): def name(self): return self._name + def getNameAbbreviation(self): + """Returns an abbreviation of self.name by keeping the first word + and only the initials of all following words separated by spaces""" + words = self.name.split(" ") + name = words[0] + for w in words[1:]: + name += " " + w[0] + "." + return name + @name.setter def name(self, n): assert isinstance(n, unicode) @@ -184,6 +204,7 @@ def __init__(self, datasources, direct=False): super(Layer, self).__init__() self._name = u"Unnamed Layer" self._visible = True + self._showPosValue = False self._opacity = 1.0 self._datasources = datasources self._layerId = None @@ -411,6 +432,22 @@ def isDifferentEnough(self, other_layer): return True return self._window_leveling != other_layer._window_leveling + def getPosInfo(self, slc): + """ + This function is called by QuadStatusBar.setLayerPosIfos and is expected to return a tuple of information for + the position widgets, showing current pixelvalues at cursor position of respective layer. + :param slc: slices of current cursor position + :return: ((String)text, (QColor)foregroundcolor, (QColor)backgroundcolor) for respective widget + """ + try: + value = self.datasources[0].request(slc).wait().squeeze() + except ValueError: + return None, QColor("black"), QColor("white") + fg = 0 + if value < 128: # be sure to have high contrast + fg = 255 + return self.getNameAbbreviation() + ":" + str(value), QColor(fg, fg, fg), QColor(value, value, value) + def __init__(self, datasource, range=None, normalize=None, direct=False, window_leveling=False): assert isinstance(datasource, DataSourceABC) super(GrayscaleLayer, self).__init__([datasource], range, normalize, direct=direct) @@ -444,6 +481,38 @@ def tintColor(self, c): self._tintColor = c self.tintColorChanged.emit() + def getPosInfo(self, slc): + """ + This function is called by QuadStatusBar.setLayerPosIfos and is expected to return a tuple of information for + the position widgets, showing current pixelvalues at cursor position of respective layer. + :param slc: slices of current cursor position + :return: ((String)text, (QColor)foregroundcolor, (QColor)backgroundcolor) for respective widget + """ + try: + # This layer has only one datasource + value = self.datasources[0].request(slc).wait().squeeze() + if not isinstance(value.reshape(1)[0], numbers.Integral): # reshape here, squeeze returns empty shape + value = round(float(value), 4) + except ValueError: + return None, QColor("black"), QColor("white") + + if qGray(self.tintColor.rgb()) < 128: # high contrast with fore- and background + fg = QColor("white") + else: + fg = QColor("black") + + if value != 0: + if "Segmentation (Label " in self.name: + name = self.name[self.name.find("(") + 1:self.name.find(")")] + else: + name = self.getNameAbbreviation() + ":" + str(value) + return name, fg, self.tintColor + else: + if "Segmentation (Label " in self.name: + return None, QColor("black"), QColor("white") + else: + return self.getNameAbbreviation() + ":" + str(value), fg, self.tintColor + def __init__(self, datasource, tintColor=QColor(255, 0, 0), range=(0, 255), normalize=None): assert isinstance(datasource, DataSourceABC) super(AlphaModulatedLayer, self).__init__([datasource], range, normalize) @@ -519,6 +588,29 @@ def isDifferentEnough(self, other_layer): return True return False + def getPosInfo(self, slc): + """ + This function is called by QuadStatusBar.setLayerPosIfos and is expected to return a tuple of information for + the position widgets, showing current pixelvalues at cursor position of respective layer. + :param slc: slices of current cursor position + :return: ((String)text, (QColor)foregroundcolor, (QColor)backgroundcolor) for respective widget + """ + try: + value = self.datasources[0].request(slc).wait().squeeze() + except ValueError: + return None, QColor("black"), QColor("white") + bg = QColor(self.colorTable[int(value)]) + if self.colorTable[int(value)] != 0: + if qGray(bg.rgb()) < 128: # high contrast with fore- and background + fg = QColor("white") + else: + fg = QColor("black") + return self.getNameAbbreviation() + ":" + str(value), fg, bg + else: # transparent + if "Labels" in self.name: + return None, QColor("black"), QColor("white") + return self.getNameAbbreviation() + ":" + str(value), QColor("black"), QColor("white") + def __init__(self, datasource, colorTable, normalize=False, direct=False): assert isinstance(datasource, DataSourceABC) @@ -593,6 +685,35 @@ def color_missing_value(self): def alpha_missing_value(self): return self._alpha_missing_value + def getPosInfo(self, slc): + """ + This function is called by QuadStatusBar.setLayerPosIfos and is expected to return a tuple of information for + the position widgets, showing current pixelvalues at cursor position of respective layer. + :param slc: slices of current cursor position + :return: ((String)text, (QColor)foregroundcolor, (QColor)backgroundcolor) for respective widget + """ + + value = [] + name = self.getNameAbbreviation() + ":" + for i, ds in enumerate(self.datasources): + try: + assert isinstance(ds, DataSourceABC) + value.append(ds.request(slc).wait().squeeze()) + except (ValueError, AssertionError): + if i == 3: # alpha channel by default + value.append(self._alpha_missing_value) + else: # RGB channels by default + value.append(self._color_missing_value) + name += str(value[-1]) + ";" + bg = QColor(value[0], value[1], value[2], value[3]) + if qGray(bg.rgb()) < 128: # high contrast with fore- and background + fg = QColor("white") + else: + fg = QColor("black") + if value: + return name[:-1], fg, bg + return None, QColor("black"), QColor("white") + def __init__( self, red=None, diff --git a/volumina/layerstack.py b/volumina/layerstack.py index ec60c8908..94c3c2a24 100644 --- a/volumina/layerstack.py +++ b/volumina/layerstack.py @@ -79,6 +79,16 @@ def findMatchingIndex(self, func): return index raise ValueError("No matching layer in stack.") + def findMatchingIndices(self, func): + """Call the given function with each layer and return all indices of layers for which func is True.""" + indices = [] + for index, layer in enumerate(self._layerStack): + if func(layer): + indices.append(index) + if indices: + return indices + raise ValueError("No matching layer in stack.") + def append(self, data): self.insert(0, data) diff --git a/volumina/navigationController.py b/volumina/navigationController.py index 958ff7651..8c63fd915 100644 --- a/volumina/navigationController.py +++ b/volumina/navigationController.py @@ -379,6 +379,7 @@ def maybeUpdateSlice(oldSlicing): def changeTime(self, newTime): for i in range(3): self._imagePumps[i].syncedSliceSources.setThrough(0, newTime) + self._model.time = newTime def changeTimeRelative(self, delta): if self._model.shape5D is None or delta == 0: diff --git a/volumina/pixelpipeline/datasources/cachesource.py b/volumina/pixelpipeline/datasources/cachesource.py new file mode 100644 index 000000000..60d46abb9 --- /dev/null +++ b/volumina/pixelpipeline/datasources/cachesource.py @@ -0,0 +1,114 @@ +import threading + +from PyQt5.QtCore import QObject, pyqtSignal + +from volumina.pixelpipeline.interface import DataSourceABC +from volumina.slicingtools import is_pure_slicing +from volumina.utility.cache import KVCache + + +class _Request: + def __init__(self, cached_source, slicing, key): + self._cached_source = cached_source + self._slicing = slicing + self._key = key + self._result = None + self._rq = self._cached_source._source.request(self._slicing) + + def wait(self): + if self._result is None: + self._result = res = self._rq.wait() + self._cached_source._cache.set(self._key, res) + self._cached_source._req.pop(self._key, None) + + return self._result + + def cancel(self): + self._rq.cancel() + + +class _CachedRequest: + def __init__(self, result): + self._result = result + + def wait(self): + return self._result + + def cancel(self): + pass + + +class CacheSource(QObject, DataSourceABC): + isDirty = pyqtSignal(object) + numberOfChannelsChanged = pyqtSignal(int) + + def __init__(self, source): + super().__init__() + self._lock = threading.Lock() + + self._source = source + self._cache = KVCache() + self._req = {} + self._source.isDirty.connect(self.isDirty) + self._source.numberOfChannelsChanged.connect(self.numberOfChannelsChanged) + self._source.isDirty.connect(self.clear) + self._source.numberOfChannelsChanged.connect(self.clear) + + def clear(self, *args): + self._cache.clear() + self._req.clear() + + def cache_key(self, slicing): + parts = [] + + for el in slicing: + _, key_part = el.__reduce__() + parts.append(key_part) + + return "::".join(str(p) for p in parts) + + def request(self, slicing): + key = self.cache_key(slicing) + + with self._lock: + if key in self._cache: + return _CachedRequest(self._cache.get(key)) + + else: + if key not in self._req: + self._req[key] = _Request(self, slicing, key) + + return self._req[key] + + def __getattr__(self, attr): + return getattr(self._source, attr) + + def setDirty(self, slicing): + if not is_pure_slicing(slicing): + raise Exception("dirty region: slicing is not pure") + self.isDirty.emit(slicing) + + @property + def numberOfChannels(self): + return self._source.numberOfChannels + + def __repr__(self): + return f"" + + def __eq__(self, other): + if other is None: + return False + return self._source is other._source + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return hash(self._source) + + def dtype(self): + return self._source.dtype() + + def clean_up(self): + self._cache.clear() + self._source.clean_up() diff --git a/volumina/pixelpipeline/datasources/factories.py b/volumina/pixelpipeline/datasources/factories.py index e092ac8f8..876f32364 100644 --- a/volumina/pixelpipeline/datasources/factories.py +++ b/volumina/pixelpipeline/datasources/factories.py @@ -24,6 +24,7 @@ import numpy from .arraysource import ArraySource +from .cachesource import CacheSource hasLazyflow = True try: @@ -114,8 +115,16 @@ def _createDataSourceLazyflow(slot, withShape): return src @createDataSource.register(lazyflow.graph.OutputSlot) + def _lazyflow_out(slot, withShape=False): + if withShape: + src, shape = _createDataSourceLazyflow(slot, withShape) + return CacheSource(src), shape + else: + src = _createDataSourceLazyflow(slot, withShape) + return CacheSource(src) + @createDataSource.register(lazyflow.graph.InputSlot) - def _lazyflow_ds(source, withShape=False): + def _lazyflow_in(source, withShape=False): return _createDataSourceLazyflow(source, withShape) diff --git a/volumina/positionModel.py b/volumina/positionModel.py index c62ef59f2..aee3c497d 100644 --- a/volumina/positionModel.py +++ b/volumina/positionModel.py @@ -38,10 +38,9 @@ class PositionModel(QObject): in the same way as would be possible by manipulating the viewer with a mouse. """ - - timeChanged = pyqtSignal(int) - channelChanged = pyqtSignal(int) - cursorPositionChanged = pyqtSignal(object, object) + timeChanged = pyqtSignal(int) + channelChanged = pyqtSignal(int) + cursorPositionChanged = pyqtSignal(object, object) slicingPositionChanged = pyqtSignal(object, object) slicingPositionSettled = pyqtSignal(bool) diff --git a/volumina/quadsplitter.py b/volumina/quadsplitter.py index f4f98c65a..f1ad24f29 100644 --- a/volumina/quadsplitter.py +++ b/volumina/quadsplitter.py @@ -270,8 +270,8 @@ def addStatusBar(self, bar): def setGrayScaleToQuadStatusBar(self, gray): self.quadViewStatusBar.setGrayScale(gray) - def setMouseCoordsToQuadStatusBar(self, x, y, z): - self.quadViewStatusBar.setMouseCoords(x, y, z) + def setLayerPosIfosToQuadStatusBar(self, x, y, z): + self.quadViewStatusBar.setMousePosInfos(x, y, z) def ensureMaximized(self, axis): """ diff --git a/volumina/sliceSelectorHud.py b/volumina/sliceSelectorHud.py index 0aaa1374e..6848c22fd 100644 --- a/volumina/sliceSelectorHud.py +++ b/volumina/sliceSelectorHud.py @@ -25,8 +25,9 @@ import volumina from past.utils import old_div -from PyQt5.QtCore import QCoreApplication, QEvent, QPointF, QSize, Qt, pyqtSignal -from PyQt5.QtGui import QBrush, QColor, QFont, QIcon, QMouseEvent, QPainter, QPainterPath, QPen, QPixmap, QTransform +from PyQt5.QtCore import QCoreApplication, QEvent, QPoint, QPointF, QRect, QSize, Qt, pyqtSignal, QSize, QTimer +from PyQt5.QtGui import QBrush, QColor, QFont, QFontMetrics, QIcon, QMouseEvent, QPainter, QPainterPath, QPen, QPixmap, QTransform + from PyQt5.QtWidgets import ( QAbstractSpinBox, QCheckBox, @@ -39,11 +40,14 @@ QToolButton, QVBoxLayout, QWidget, + QLineEdit, ) from volumina.utility import ShortcutManager from volumina.widgets.delayedSpinBox import DelayedSpinBox +from volumina.slicingtools import index2slice -TEMPLATE = "QSpinBox {{ color: {0}; font: bold; background-color: {1}; border:0;}}" +SB_TEMPLATE = "QSpinBox {{ color: {0}; font: bold; background-color: {1}; border:0;}}" +LE_TEMPLATE = "QLineEdit {{ color: {0}; background-color: {1}; border:0; }}" def _load_icon(filename, backgroundColor, width, height): @@ -181,7 +185,7 @@ def __init__(self, parentView, parent, backgroundColor, foregroundColor, value, def do_draw(self): r, g, b, a = self.foregroundColor.getRgb() rgb = "rgb({0},{1},{2})".format(r, g, b) - sheet = TEMPLATE.format(rgb, self.backgroundColor.name()) + sheet = SB_TEMPLATE.format(rgb, self.backgroundColor.name()) self.spinBox.setStyleSheet(sheet) def spinBoxValueChanged(self, value): @@ -420,58 +424,89 @@ def setAxes(self, rotation, swapped): self.buttons["swap-axes"].swapped = swapped -def _get_pos_widget(name, backgroundColor, foregroundColor): - label = QLabel() - label.setAttribute(Qt.WA_TransparentForMouseEvents, True) +class PosCoordWidget(DelayedSpinBox): + def __init__(self, name, foregroundColor, backgroundColor): + super().__init__(750) + self.label = QLabel() + self.label.setAttribute(Qt.WA_TransparentForMouseEvents, True) - pixmap = QPixmap(25 * 10, 25 * 10) - pixmap.fill(backgroundColor) - painter = QPainter() - painter.begin(pixmap) - pen = QPen(foregroundColor) - painter.setPen(pen) - painter.setRenderHint(QPainter.Antialiasing) - font = QFont() - font.setBold(True) - font.setPixelSize(25 * 10 - 30) - path = QPainterPath() - path.addText(QPointF(50, 25 * 10 - 50), font, name) - brush = QBrush(foregroundColor) - painter.setBrush(brush) - painter.drawPath(path) - painter.setFont(font) - painter.end() - pixmap = pixmap.scaled(QSize(20, 20), Qt.KeepAspectRatio, Qt.SmoothTransformation) - label.setPixmap(pixmap) - - spinbox = DelayedSpinBox(750) - spinbox.setAlignment(Qt.AlignCenter) - spinbox.setToolTip("{0} Spin Box".format(name)) - spinbox.setButtonSymbols(QAbstractSpinBox.NoButtons) - spinbox.setMaximumHeight(20) - font = spinbox.font() - font.setPixelSize(14) - spinbox.setFont(font) - sheet = TEMPLATE.format(foregroundColor.name(), backgroundColor.name()) - spinbox.setStyleSheet(sheet) - return label, spinbox + pixmap = QPixmap(25 * 10, 25 * 10) + pixmap.fill(backgroundColor) + painter = QPainter() + painter.begin(pixmap) + pen = QPen(foregroundColor) + painter.setPen(pen) + painter.setRenderHint(QPainter.Antialiasing) + font = QFont() + font.setBold(True) + font.setPixelSize(25 * 10 - 30) + path = QPainterPath() + path.addText(QPointF(50, 25 * 10 - 50), font, name) + brush = QBrush(foregroundColor) + painter.setBrush(brush) + painter.drawPath(path) + painter.setFont(font) + painter.end() + pixmap = pixmap.scaled(QSize(20, 20), + Qt.KeepAspectRatio, + Qt.SmoothTransformation) + self.label.setPixmap(pixmap) + + self.setAlignment(Qt.AlignCenter) + self.setToolTip("{0} Spin Box".format(name)) + self.setButtonSymbols(QAbstractSpinBox.NoButtons) + self.setMaximumHeight(20) + font = self.font() + font.setPixelSize(14) + self.setFont(font) + sheet = SB_TEMPLATE.format(foregroundColor.name(), + backgroundColor.name()) + self.setStyleSheet(sheet) + + +class PosLayerInfoWidget(QLineEdit): + def __init__(self): + super().__init__() + self.setAlignment(Qt.AlignCenter) + font = self.font() + font.setPixelSize(14) + font.setBold(True) + self.setFont(font) + self.setReadOnly(True) + self.setMaximumHeight(20) + self.setInfo(None, QColor(0,0,0), QColor(255,255,255)) + + def setInfo(self, text, foregroundColor, backgroundColor): + sheet = LE_TEMPLATE.format(foregroundColor.name(), backgroundColor.name()) + self.setStyleSheet(sheet) + fm = QFontMetrics(self.font()) + if text is None: + pixelsWide = fm.width("-") + self.setMaximumWidth(pixelsWide + 6) + self.setText("-") + else: + pixelsWide = fm.width(text) + self.setMaximumWidth(pixelsWide+6) + self.setText(text) class QuadStatusBar(QHBoxLayout): - positionChanged = pyqtSignal(int, int, int) # x,y,z - def __init__(self, parent=None): + positionChanged = pyqtSignal(int, int, int) # x,y,z + + def __init__(self, volumeEditor, parent=None): QHBoxLayout.__init__(self, parent) self.setContentsMargins(0, 4, 0, 0) self.setSpacing(0) self.timeControlFontSize = 12 + self.editor = volumeEditor def showXYCoordinates(self): - self.zLabel.setHidden(True) + self.zSpinBox.label.setHidden(True) self.zSpinBox.setHidden(True) def showXYZCoordinates(self): - self.zLabel.setHidden(False) + self.zSpinBox.label.setHidden(False) self.zSpinBox.setHidden(False) def hideTimeSlider(self, flag): @@ -493,22 +528,27 @@ def setToolTipTimeSlider(self, croppingFlag=False): self.timeSlider.setToolTip("Choose the time coordinate of the current dataset.") def createQuadViewStatusBar( - self, xbackgroundColor, xforegroundColor, ybackgroundColor, yforegroundColor, zbackgroundColor, zforegroundColor + self, xbackgroundColor, xforegroundColor, ybackgroundColor, yforegroundColor, zbackgroundColor, + zforegroundColor, labelbackgroundColor, labelforegroundColor ): - self.xLabel, self.xSpinBox = _get_pos_widget("X", xbackgroundColor, xforegroundColor) - self.yLabel, self.ySpinBox = _get_pos_widget("Y", ybackgroundColor, yforegroundColor) - self.zLabel, self.zSpinBox = _get_pos_widget("Z", zbackgroundColor, zforegroundColor) + self.xSpinBox = PosCoordWidget("X", xforegroundColor, xbackgroundColor) + self.ySpinBox = PosCoordWidget("Y", yforegroundColor, ybackgroundColor) + self.zSpinBox = PosCoordWidget("Z", zforegroundColor, zbackgroundColor) + self.layerValueWidgets = self._get_layer_value_widgets() self.xSpinBox.delayedValueChanged.connect(partial(self._handlePositionBoxValueChanged, "x")) self.ySpinBox.delayedValueChanged.connect(partial(self._handlePositionBoxValueChanged, "y")) self.zSpinBox.delayedValueChanged.connect(partial(self._handlePositionBoxValueChanged, "z")) - self.addWidget(self.xLabel) + self.addWidget(self.xSpinBox.label) self.addWidget(self.xSpinBox) - self.addWidget(self.yLabel) + self.addWidget(self.ySpinBox.label) self.addWidget(self.ySpinBox) - self.addWidget(self.zLabel) + self.addWidget(self.zSpinBox.label) self.addWidget(self.zSpinBox) + self.zSpinBoxIndex = self.indexOf(self.zSpinBox) + for valueWidget in self.layerValueWidgets.values(): + self.insertWidget(self.zSpinBoxIndex+1, valueWidget) self.addSpacing(10) @@ -586,6 +626,72 @@ def createQuadViewStatusBar( self._registerTimeframeShortcuts() + def _layer_show_value_Changed(self, layer, showVal): + if showVal: + if "Segmentation (Label " in layer.name: + for key in self.layerValueWidgets.keys(): + if "Segmentation (Label " in key.name: + self.layerValueWidgets[layer] = self.layerValueWidgets[key] + return + self.layerValueWidgets[layer] = PosLayerInfoWidget() + self.insertWidget(self.zSpinBoxIndex + 1, self.layerValueWidgets[layer]) + # self.addWidget(self.layerValueWidgets[layer]) + else: + widget = self.layerValueWidgets[layer] + del self.layerValueWidgets[layer] + if "Segmentation (Label " in layer.name: + for key in self.layerValueWidgets.keys(): + if "Segmentation (Label " in key.name: + return + self.removeWidget(widget) + widget.deleteLater() + + def _layer_added(self, layer, row): + layer.showPosValueChanged.connect(self._layer_show_value_Changed) + if layer.showPosValue: + if "Segmentation (Label " in layer.name: + for key in self.layerValueWidgets.keys(): + if "Segmentation (Label " in key.name: + self.layerValueWidgets[layer] = self.layerValueWidgets[key] + break + else: + continue + continue + self.layerValueWidgets[layer] = PosLayerInfoWidget() + self.insertWidget(self.zSpinBoxIndex + 1, self.layerValueWidgets[layer]) + # self.addWidget(self.layerValueWidgets[layer]) + + def _layer_removed(self, layer, row): + layer.showPosValueChanged.disconnect(self._layer_show_value_Changed) + if layer in self.layerValueWidgets: + widget = self.layerValueWidgets[layer] + del self.layerValueWidgets[layer] + if "Segmentation (Label " in layer.name: + for key in self.layerValueWidgets.keys(): + if "Segmentation (Label " in key.name: + return + self.removeWidget(widget) + widget.deleteLater() + + def _get_layer_value_widgets(self): + layerValueWidgets = {} + self.editor.layerStack.layerAdded.connect(self._layer_added) + self.editor.layerStack.layerRemoved.connect(self._layer_removed) + for layer in self.editor.layerStack: # Just to be sure, however layerStack should be empty at this point + layer.showPosValueChanged.connect(self._layer_show_value_Changed) + if layer.showPosValue: + if "Segmentation (Label " in layer.name: + for key in positionMeta.keys(): + if "Segmentation (Label " in key.name: + layerValueWidgets[layer] = layerValueWidgets[key] + break + else: + continue + continue + layerValueWidgets[layer] = PosLayerInfoWidget() + + return layerValueWidgets + def _registerTimeframeShortcuts(self, enabled=True, remove=True): """ Register or deregister "," and "." as keyboard shortcuts for scrolling in time """ mgr = ShortcutManager() @@ -699,11 +805,29 @@ def updateShape5Dcropped(self, shape5DcropMin, shape5Dmax): self.zSpinBox.setValue(shape5DcropMin[3]) self.timeSlider.setValue(shape5DcropMin[0]) - def setMouseCoords(self, x, y, z): + def setMousePosInfos(self, x, y, z): self.xSpinBox.setValueWithoutSignal(x) self.ySpinBox.setValueWithoutSignal(y) self.zSpinBox.setValueWithoutSignal(z) + coords = [int(val) for val in [x,y,z]] + coords.append(self.editor.posModel.channel) + coords.insert(0, self.editor.posModel.time) + + labelSetDone = False + + for layer, widget in self.layerValueWidgets.items(): + lbl, foreground, background = layer.getPosInfo(index2slice(coords)) + if "Segmentation (Label " in layer.name: + if not labelSetDone: + if lbl is None: + widget.setInfo(lbl, foreground, background) + continue + widget.setInfo(lbl, foreground, background) + labelSetDone = True + else: + widget.setInfo(lbl, foreground, background) + if __name__ == "__main__": import sys diff --git a/volumina/tiling.py b/volumina/tiling.py index 5e8c6a54f..a91bdc9fe 100644 --- a/volumina/tiling.py +++ b/volumina/tiling.py @@ -200,7 +200,7 @@ def containsF(self, point): def intersected(self, sceneRect): if not sceneRect.isValid(): return list(range(len(self.tileRects))) - + sceneRect = sceneRect.normalized() # Patch accessor uses data coordinates rect = self.data2scene.inverted()[0].mapRect(sceneRect) patchNumbers = self._patchAccessor.getPatchesForRect( diff --git a/volumina/utility/cache.py b/volumina/utility/cache.py new file mode 100644 index 000000000..9ebf76ccd --- /dev/null +++ b/volumina/utility/cache.py @@ -0,0 +1,119 @@ +import abc +import sys +import threading +from collections import OrderedDict +from typing import NamedTuple, Dict, Any, Type, TypeVar, Callable + +import numpy as np + + +T = TypeVar("T") +_256_MB = 256 * 1024 * 1024 + + +class CacheABC(abc.ABC): + @abc.abstractmethod + def get(self, key: str, default=None) -> Any: + ... + + @abc.abstractmethod + def set(self, key: str, value: Any) -> Any: + ... + + @abc.abstractmethod + def delete(self, key: str) -> None: + ... + + @abc.abstractmethod + def touch(self, key: str) -> None: + ... + + @abc.abstractmethod + def __contains__(self, key: str) -> bool: + ... + + +class KVCache(CacheABC): + _MISSING = object() + _cachable_types: Dict[T, Callable[[T], int]] = {} + + class _Entry(NamedTuple): + obj: Any + size: int # bytes + + def __init__(self, mem_limit=_256_MB): + self._cache = OrderedDict() + self._mem_limit = mem_limit + self._mem = 0 + self._lock = threading.RLock() + + def get(self, key: str, default=None) -> Any: + with self._lock: + entry = self._cache.get(key, self._MISSING) + self._cache.move_to_end(key) + + if entry is not self._MISSING: + return entry.obj + else: + return default + + def delete(self, key: str) -> None: + with self._lock: + self._cache.pop(key, None) + + def keys(self): + return list(self._cache.keys()) + + def set(self, key, value) -> None: + with self._lock: + old_entry = self._cache.get(key) + + if old_entry is not None: + self._mem -= old_entry.size + + get_size_fn = self._cachable_types.get(type(value)) + if get_size_fn is None: + raise ValueError(f"Unknown type {type(value)}") + + size = get_size_fn(value) + self._mem += size + self._cache[key] = self._Entry(value, size) + self._cache.move_to_end(key) + + def __contains__(self, key) -> bool: + return key in self._cache + + def __len__(self): + return len(self._cache) + + def touch(self, key: str) -> None: + with self._lock: + self._cache.move_to_end(key) + + @classmethod + def register_type(cls, _type: Type[T]): + def _register(get_size_fn: Callable[[T], int]): + cls._cachable_types[_type] = get_size_fn + + return _register + + @property + def used_memory(self) -> int: + return self._mem + + def clean(self) -> None: + with self._lock: + while self._mem > self._mem_limit: + key, entry = next(iter(self._cache.items())) + del self._cache[key] + self._mem -= entry.size + + def clear(self) -> None: + with self._lock: + self._cache.clear() + self._mem = 0 + + +@KVCache.register_type(np.ndarray) +def _get_size_of_ndarray(arr: np.ndarray) -> int: + return sys.getsizeof(arr) diff --git a/volumina/volumeEditorWidget.py b/volumina/volumeEditorWidget.py index 597e9a35b..e24c3cc57 100644 --- a/volumina/volumeEditorWidget.py +++ b/volumina/volumeEditorWidget.py @@ -161,9 +161,10 @@ def onViewFocused(): self, self.editor.imageViews[2], self.editor.imageViews[0], self.editor.imageViews[1], self.editor.view3d ) self.quadview.installEventFilter(self) - self.quadViewStatusBar = QuadStatusBar() + self.quadViewStatusBar = QuadStatusBar(self.editor) self.quadViewStatusBar.createQuadViewStatusBar( - QColor("#dc143c"), QColor("white"), QColor("green"), QColor("white"), QColor("blue"), QColor("white") + QColor("#dc143c"), QColor("white"), QColor("green"), QColor("white"), QColor("blue"), QColor("white"), + QColor("black"), QColor("white") ) self.quadview.addStatusBar(self.quadViewStatusBar) self.layout.addWidget(self.quadview) @@ -457,7 +458,7 @@ def jumpToLastSlice(axis): ) def _updateInfoLabels(self, pos): - self.quadViewStatusBar.setMouseCoords(*pos) + self.quadViewStatusBar.setMousePosInfos(*pos) def eventFilter(self, watched, event): # If the user performs a ctrl+scroll on the splitter itself, diff --git a/volumina/widgets/layercontextmenu.py b/volumina/widgets/layercontextmenu.py index 836f680b7..fab479029 100644 --- a/volumina/widgets/layercontextmenu.py +++ b/volumina/widgets/layercontextmenu.py @@ -100,6 +100,9 @@ def adjust_colortable_callback(): def _add_actions(layer, menu): + def show_value_callback(checked): + layer.showPosValue = checked + if isinstance(layer, GrayscaleLayer): _add_actions_grayscalelayer(layer, menu) elif isinstance(layer, RGBALayer): @@ -107,6 +110,13 @@ def _add_actions(layer, menu): elif isinstance(layer, (ColortableLayer, ClickableColortableLayer)): _add_actions_colortablelayer(layer, menu) + if hasattr(layer, 'getPosInfo'): + showVal = QAction("%s" % 'show pixel value', menu) + showVal.setCheckable(True) + showVal.setChecked(layer.showPosValue) + menu.addAction(showVal) + showVal.triggered.connect(show_value_callback) + def layercontextmenu(layer, pos, parent=None): """Show a context menu to manipulate properties of layer.