Skip to content

Instantly share code, notes, and snippets.

@GenevieveBuckley
Last active May 20, 2020 02:48
Show Gist options
  • Save GenevieveBuckley/973a096ca70027f15bacf7c2c8643133 to your computer and use it in GitHub Desktop.
Save GenevieveBuckley/973a096ca70027f15bacf7c2c8643133 to your computer and use it in GitHub Desktop.
napari LayerGroup discussion
import numpy as np
import skimage.data
from napari.layers import *
def small_layergroup():
tree = Layergroup(name='small-tree')
branch = Layergroup(name='branch')
tree.append(branch)
twig = Layergroup(name='twig')
twig.append(Image(skimage.data.chelsea(), name='chelsea'))
branch.append(twig)
branch.append(Image(skimage.data.camera(), name='camera'))
branch.append(Points(np.random.random((10, 3)) * 200, name='points')) # 10 points in 3D, range scaled to 200 pixels
tree.append(Image(skimage.data.astronaut(), name='astronaut'))
return tree
def big_layergroup():
data = skimage.data.camera()
tree = Layergroup(name='tree')
branch1 = Layergroup(name='branch1')
branch2 = Layergroup(name='branch2')
branch3 = Layergroup(name='branch3')
branch2.append(Image(skimage.data.camera(), name='image1_on_branch2_on_branch1'))
branch2.append(Image(skimage.data.chelsea(), name='image2_on_branch2_on_branch1'))
branch3.append(Image(skimage.data.astronaut(), name='image1_on_branch3_on_branch1'))
branch1.append(branch2)
branch1.append(branch3)
branch1.append(Image(skimage.data.camera(), name='image1_on_branch1'))
branch4 = Layergroup(name='branch4')
branch4.append(Image(skimage.data.camera(), name='image1_on_branch4'))
branch4.append(Image(skimage.data.chelsea(), name='image2_on_branch4'))
branch5 = Layergroup(name='branch5')
branch5.append(Image(skimage.data.astronaut(), name='image1_on_branch5'))
tree.append(branch1)
tree.append(branch4)
tree.append(branch5)
tree.append(Image(skimage.data.camera(), name='single_img'))
return tree
ltree = small_layergroup()
lg = big_layergroup()
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-19-ee15db06cedf> in <module>
----> 1 viewer.add_layer(lg)
c:\users\genevieve\documents\github\napari\napari\components\add_layers_mixin.py in add_layer(self, layer)
45 layer.dims.events.order.connect(self._on_layers_change)
46 layer.dims.events.range.connect(self._on_layers_change)
---> 47 self.layers.append(layer)
48 self._update_layers(layers=[layer])
49
c:\users\genevieve\documents\github\napari\napari\components\layergroup.py in append(self, item)
85 def append(self, item):
86 item._parent = self
---> 87 return self._children.append(item)
88
89 def _get_extent(self, *args, **kwargs):
c:\users\genevieve\documents\github\napari\napari\utils\list\_model.py in append(self, obj)
58 def append(self, obj):
59 TypedList.append(self, obj)
---> 60 self.events.added(item=obj, index=len(self) - 1)
61
62 def pop(self, key):
c:\users\genevieve\documents\github\napari\napari\utils\event.py in __call__(self, *args, **kwargs)
506 continue
507
--> 508 self._invoke_callback(cb, event)
509 if event.blocked:
510 break
c:\users\genevieve\documents\github\napari\napari\utils\event.py in _invoke_callback(self, cb, event)
523 cb(event)
524 except Exception:
--> 525 _handle_exception(
526 self.ignore_callback_errors,
527 self.print_callback_errors,
c:\users\genevieve\documents\github\napari\napari\utils\event.py in _invoke_callback(self, cb, event)
521 def _invoke_callback(self, cb, event):
522 try:
--> 523 cb(event)
524 except Exception:
525 _handle_exception(
c:\users\genevieve\documents\github\napari\napari\_qt\qt_controls.py in _add(self, event)
65 """
66 layer = event.item
---> 67 controls = create_qt_controls(layer)
68 self.addWidget(controls)
69 self.widgets[layer] = controls
c:\users\genevieve\documents\github\napari\napari\_qt\layers\utils.py in create_qt_controls(layer)
35 Qt controls widget
36 """
---> 37 controls = layer_to_controls[type(layer)](layer)
38
39 return controls
c:\users\genevieve\documents\github\napari\napari\_qt\layers\qt_image_base_layer.py in __init__(self, layer)
44 super().__init__(layer)
45
---> 46 self.layer.events.colormap.connect(self._on_colormap_change)
47 self.layer.events.gamma.connect(self.gamma_slider_update)
48 self.layer.events.contrast_limits.connect(self._on_clims_change)
AttributeError: 'EmitterGroup' object has no attribute 'colormap'
In [26]: viewer.add_layer(lg)
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-26-ee15db06cedf> in <module>
----> 1 viewer.add_layer(lg)
c:\users\genevieve\documents\github\napari\napari\components\add_layers_mixin.py in add_layer(self, layer)
45 layer.dims.events.order.connect(self._on_layers_change)
46 layer.dims.events.range.connect(self._on_layers_change)
---> 47 self.layers.append(layer)
48 self._update_layers(layers=[layer])
49
c:\users\genevieve\documents\github\napari\napari\components\layergroup.py in append(self, item)
85 def append(self, item):
86 item._parent = self
---> 87 return self._children.append(item)
88
89 def _get_extent(self, *args, **kwargs):
c:\users\genevieve\documents\github\napari\napari\utils\list\_model.py in append(self, obj)
58 def append(self, obj):
59 TypedList.append(self, obj)
---> 60 self.events.added(item=obj, index=len(self) - 1)
61
62 def pop(self, key):
c:\users\genevieve\documents\github\napari\napari\utils\event.py in __call__(self, *args, **kwargs)
506 continue
507
--> 508 self._invoke_callback(cb, event)
509 if event.blocked:
510 break
c:\users\genevieve\documents\github\napari\napari\utils\event.py in _invoke_callback(self, cb, event)
523 cb(event)
524 except Exception:
--> 525 _handle_exception(
526 self.ignore_callback_errors,
527 self.print_callback_errors,
c:\users\genevieve\documents\github\napari\napari\utils\event.py in _invoke_callback(self, cb, event)
521 def _invoke_callback(self, cb, event):
522 try:
--> 523 cb(event)
524 except Exception:
525 _handle_exception(
c:\users\genevieve\documents\github\napari\napari\_qt\qt_viewer.py in _add_layer(self, event)
225 layers = event.source
226 layer = event.item
--> 227 vispy_layer = create_vispy_visual(layer)
228 vispy_layer.camera = self.view.camera
229 vispy_layer.node.parent = self.view.scene
c:\users\genevieve\documents\github\napari\napari\_vispy\utils.py in create_vispy_visual(layer)
30 Vispy visual node
31 """
---> 32 visual = layer_to_visual[type(layer)](layer)
33
34 return visual
KeyError: <class 'napari.components.layergroup.LayerGroup'>
In [27]: lg
Out[27]:
<LayerGroup 'LayerGroup' at 0x16e52c054f0>
+--<Image layer 'camera' at 0x16e52c054f0>
+--<Image layer 'astronaut' at 0x16e52c054f0>

A LayerGroup class for napari

Original issue: napari/napari#970 Branch with in-progress work: https://github.com/GenevieveBuckley/napari/tree/layergroup

Juan suggested napari uses the composite design pattern for layergroups: https://en.wikipedia.org/wiki/Composite_pattern There's a nice example of a python implementation of this pattern here: https://refactoring.guru/design-patterns/composite/python/example

Talley's been thinking about how to handle the Qt view side of things, and suggests:

Tutorial; https://pythonspot.com/pyqt5-treeview/ This is useful: https://doc.qt.io/qtforpython/PySide2/QtCore/QAbstractItemModel.html This is a good tutorial on QTreeView: https://doc.qt.io/qt-5/modelview.html#3-1-treeview

# Talley using the __new__ method on the LayerGroup, worked ok as a proof of principle.
# He says "here's a general gist. "
# "I'm definitely not proposing we use something exactly like this, "
# "but it shows where some of these properties live and how to check for them and/or overwrite them. "
# "Haven't tested this code except to make sure it doesn't error."
class LayerGroupDescriptor:
def __init__(self, attr_name):
self.attr_name = attr_name
self.private_name = '_' + attr_name
def __get__(self, obj, objtype=None):
return getattr(obj, obj.private_name)
def __set__(self, obj, value):
setattr(obj, self.private_name, value)
obj._visible = value
if getattr(obj, '_children', None):
for child in obj:
setattr(child, self.attr_name, value)
class LayerGroup(Layer):
def __new__(cls):
obj = super().__new__(cls)
for attr_name in dir(Layer):
# things like properties and abstractmethods live on the class
# not on the instance...
attribute = getattr(LayerGroup, attr_name)
if isinstance(attribute, property):
print(attr_name + " is an @property")
setattr(LayerGroup, attr_name, LayerGroupDescriptor(attr_name))
# the getter function is at attribute.fget
# the setter function is at attribute.fset
# abstract methods live on the Layer class, not on layergroup
attribute = getattr(Layer, attr_name)
if getattr(attribute, '__isabstractmethod__', False):
print(attr_name + " is an abstract method")
return obj
(napari-dev) C:\Users\Genevieve>ipython --gui=qt
Python 3.8.1 (default, Mar 2 2020, 13:06:26) [MSC v.1916 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 7.13.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: %load_ext autoreload
In [2]: %autoreload 2
In [3]: import napari
In [4]: from skimage.data import camera, astronaut
In [5]: import numpy as np
In [6]: data = np.random.random((100,100))
In [7]: from napari.layers import *
In [8]: from napari.components import LayerGroup
In [9]: cam = Image(camera(), name='camera')
In [10]: astro = Image(astronaut(), name='astro')
In [11]: lg = LayerGroup()
In [12]: lg.append(cam)
In [13]: lg.append(astro)
In [14]: lg
Out[14]:
<LayerGroup 'LayerGroup' at 0x1be9e70bd00>
+--<Image layer 'camera' at 0x1be9e70bd00>
+--<Image layer 'astro' at 0x1be9e70bd00>
In [15]: viewer = napari.Viewer()
# https://stackoverflow.com/questions/27898718/multi-level-qtreeview
import sys
from qtpy.QtGui import QStandardItemModel, QStandardItem
from qtpy.QtWidgets import QApplication, QWidget, QTreeView, QHBoxLayout
import skimage.data
from napari.components.layergroup import LayerGroup
from napari.layers import *
def small_tree():
tree = LayerGroup(name='small-tree')
tree.append(Image(skimage.data.camera(), name='camera'))
tree.append(Image(skimage.data.astronaut(), name='astronaut'))
return tree
def construct_layergroup_tree():
data = skimage.data.camera()
tree = LayerGroup(name='tree')
branch1 = LayerGroup(name='branch1')
branch1.append(Image(data, name='image1_on_branch1'))
branch1.append(Image(data, name='image2_on_branch1'))
branch2 = LayerGroup(name='branch2')
branch2.append(Image(data, name='image1_on_branch2'))
branch3 = LayerGroup(name='branch3')
branch4 = LayerGroup(name='branch4')
branch5 = LayerGroup(name='branch5')
branch4.append(Image(data, name='image1_on_branch4_on_branch3'))
branch4.append(Image(data, name='image2_on_branch4_on_branch3'))
branch5.append(Image(data, name='image1_on_branch5_on_branch3'))
branch3.append(branch4)
branch3.append(branch5)
branch3.append(Image(data, name='image1_on_branch3'))
tree.append(branch1)
tree.append(branch2)
tree.append(branch3)
tree.append(Image(data, name='single_img'))
return tree
class MainFrame(QWidget):
def __init__(self):
QWidget.__init__(self)
tree = construct_layergroup_tree()
self.tree = QTreeView(self)
self.tree.setIndentation(20)
self.tree.setHeaderHidden(True)
self.tree.setAlternatingRowColors(True)
layout = QHBoxLayout(self)
layout.addWidget(self.tree)
root_model = QStandardItemModel()
self.tree.setModel(root_model)
root_item = QStandardItem(tree.name)
root_model.invisibleRootItem().appendRow(root_item)
self._populateTree(tree, root_item)
self.tree.expandAll()
def _populateTree(self, tree, parent):
for child in tree:
child_item = QStandardItem(child.name)
parent.appendRow(child_item)
if isinstance(child, LayerGroup):
self._populateTree(child, child_item)
if __name__ == "__main__":
app = QApplication(sys.argv)
main = MainFrame()
main.show()
sys.exit(app.exec_())
import skimage.data
from napari.layers.base.base import LayerGroup
from napari.layers import *
def small_tree():
tree = LayerGroup(name='small-tree')
tree.append(Image(skimage.data.camera(), name='camera'))
tree.append(Image(skimage.data.astronaut(), name='astronaut'))
return tree
def construct_layergroup_tree():
data = skimage.data.camera()
tree = LayerGroup(name='tree')
branch1 = LayerGroup(name='branch1')
branch1.append(Image(data, name='image1_on_branch1'))
branch1.append(Image(data, name='image2_on_branch1'))
branch2 = LayerGroup(name='branch2')
branch2.append(Image(data, name='image1_on_branch2'))
branch3 = LayerGroup(name='branch3')
branch4 = LayerGroup(name='branch4')
branch5 = LayerGroup(name='branch5')
branch4.append(Image(data, name='image1_on_branch4_on_branch3'))
branch4.append(Image(data, name='image2_on_branch4_on_branch3'))
branch5.append(Image(data, name='image1_on_branch5_on_branch3'))
branch3.append(branch4)
branch3.append(branch5)
branch3.append(Image(data, name='image1_on_branch3'))
tree.append(branch1)
tree.append(branch2)
tree.append(branch3)
tree.append(Image(data, name='single_img'))
return tree
def check_visibility(layergroup):
print("Visible? {} {}".format(layergroup.visible, layergroup.name))
for child in layergroup:
if type(child) == LayerGroup:
check_visibility(child)
else:
print("Visible? {} {}".format(child.visible, child.name))
def main():
tree = construct_layergroup_tree()
print("A heirarchical layergroup")
print(tree.__repr__())
print("\nLet's have a look at the visibility of each layer")
check_visibility(tree)
print("\nLet's change part of the layergroup tree")
tree[0].visible = False
check_visibility(tree)
print("\nNow let's change the whole tree at once")
tree.visible = False
check_visibility(tree)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment