首页 > javascript > 在Jupyter / iPython笔记本中以图形方式选择几何对象

在Jupyter / iPython笔记本中以图形方式选择几何对象 (Graphically select geometric objects in a Jupyter/iPython notebook)

问题

Shapely和Jupyter / iPython之间的互操作性很好。我可以做很酷的事情,比如创建一堆几何形状并在笔记本中查看它们:

some_nodes = [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2]]
some_boxes = []
some_boxes.append([some_nodes[0], some_nodes[3], some_nodes[4], some_nodes[1]])
some_boxes.append([some_nodes[1], some_nodes[4], some_nodes[5], some_nodes[2]])

from shapely.geometry import MultiPolygon, Polygon
MultiPolygon([Polygon(box) for box in some_boxes])

......而Jupyter会告诉我这个:

Jupyter_Shapely_output

现在真酷!它对我来说特别有用,可以快速查看和编辑,例如构成2D有限元网格的多边形。

可悲的是,产生的图像只是静态的SVG图形; 没有内置的交互。能够在iPython中使用相同的图形界面选择图像中这些对象的子集是有帮助的。

更具体地说,我希望能够创建一个列表并添加一些显示的多边形,例如,单击/选择它们,或者在它们周围拖动套索/框,也可以在单击时删除它们。第二次。

我已经考虑过使用matplotlib或javascript尝试这样做,尽管我已经取得了一些初步成功,但这可能是我目前的知识/技能水平之外的那种项目。

由于Jupyter是一个有点庞大的工具,有很多我可能不知道的功能,我想知道在Jupyter笔记本的上下文中是否存在这种交互的现有解决方案?


更新#1:看起来我将不得不自己创造一些东西。令人高兴的是,本教程将使这更容易。

更新#2:看来Bokeh是一个更适合这个目的的图书馆。我相信我将放弃创建自定义Jupyter小部件的想法,并使用Bokeh小部件和交互来创建应用程序。这样的应用程序可以在Jupyter笔记本中使用,也可以在其他地方使用。

更新#3:无论如何我最终都使用了jupyter小部件系统。添加了我自己的答案,显示了一个概念证明。

解决方法

使用vanilla javascript API和自定义IPywidgets系统解决了这个问题。如果您复制并粘贴此代码,请注意单元格显示不按顺序。代码可在此处获得

用法

(单元格#3)

import shapely.geometry as geo

some_nodes = [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2]]
some_boxes = []
some_boxes.append([some_nodes[0], some_nodes[3], some_nodes[4], some_nodes[1]])
some_boxes.append([some_nodes[1], some_nodes[4], some_nodes[5], some_nodes[2]])

m_polygon = geo.MultiPolygon(geo.Polygon(box) for box in some_boxes)
poly_selector = PolygonSelector(m_polygon._repr_svg_())  # PolygonSelector defined below
poly_selector  # display the selector below cell, use the tool

工具看起来像这样:

在此输入图像描述

使用该工具后,您可以通过复制groups_dict选择器工具实例的属性(“实时”)来获取当前选定的多边形索引:

(Cell#4)

polygon_indexes = poly_selector.groups_dict.copy()
polygon_indexes

工作仍在进行中,但下面是我最终做的事情的说明。这里也是nbviewer上笔记本链接(该工具在那里不可见)。

我将此部分放在这里供我自己参考,但这是其他人可以学习(并改进)的概念证明。有些东西没有按照我想要的方式工作 - 例如在选择对象时更改对象的颜色。但主要功能,选择和保存点击多边形,工作。

下面是逐个单元格的代码单元格,正如我在上面的链接版本中所拥有的那样。

Python代码

(单元#1)

import ipywidgets.widgets as widgets
from traitlets import Unicode, Dict, List
from random import randint

class PolygonSelector(widgets.DOMWidget):
    _view_name = Unicode('PolygonSelectorView').tag(sync=True)
    _view_module = Unicode('polygonselector').tag(sync=True)
    groups_dict = Dict().tag(sync=True)
    current_list = List().tag(sync=True)
    content = Unicode().tag(sync=True)

    html_template = '''
    <style>
    # polygonGeometry path{{
        fill: 'pink';
    }}
    # polygonGeometry .selectedPolygon {{
        fill: {fill_selected!r};
    }}
    # polygonGeometry path:hover {{
        fill: {fill_hovered!r};
    }}
    {selection_styles}
    </style>
    <button id = "clearBtn"> Clear </button>
    <input placeholder = "Name this collection" id = "name" />
    <button id = "saveBtn"> Save </button>
    <div id = "polygonGeometry">{svg}</div>
    '''

    # provide some default colors; can override if desired
    fill_selected = "plum"
    fill_hovered = "lavender"
    group_colors = ["#{:06X}".format(randint(0,0xFFFFFF)) for _ in range(100)]

    def __init__(self, svg):
        super().__init__()
        self.update_content(svg)

    def update_content(self, svg):
        self.content = self.html_template.format(
            fill_selected = self.fill_selected,
            fill_hovered = self.fill_hovered,
            selection_styles = self.selection_styles,
            svg = svg
        )

    @property
    def selection_styles(self):
        return "".join(f'''
        # polygonGeometry .selection_{group_idx} {{
            fill: {self.group_colors[group_idx]!r};
        }}
        ''' for group_idx in range(len(self.groups_dict)))

Javascript代码

(单元格#2)

%%javascript

require.undef('polygonselector');

define('polygonselector', ["@jupyter-widgets/base"], function(widgets) {

    var PolygonSelectorView = widgets.DOMWidgetView.extend({

        initialized: 0,

        init_render: function(){

        },


        // Add item to selection list
        add: function(id) {
          this.current_list.push(id);
          console.log('pushed #', id);
        },

        // Remove item from selection list
        remove: function(id) {
          this.current_list = this.current_list.filter(function(_id) {
            return _id !== id;
          })
          console.log('filtered #', id);
        },

        // Remove all items, closure
        clear: function(thisView) {
                return function() {
                    // `this` is the button element
                    console.log('clear() clicked');
                    thisView.el.querySelector('#name').value = '';
                    thisView.current_list.length = 0;
                    Array.from(thisView.el.querySelectorAll('.selectedPolygon')).forEach(function(path) {
                        console.log("path classList is: ", path.classList)
                        path.classList.remove('selectedPolygon');
                    })
                    console.log('Data cleared');
                    console.log(thisView.current_list)
                };
        },

        // Add current selection to groups_dict, closure
        save: function(thisView) {
                return function() {
                    // `this` is the button element
                    console.log('save() clicked');
                    const newName = thisView.el.querySelector('#name').value;
                    console.log('Current name: ', newName)
                    if (!newName || thisView.current_list.length < 1) {
                        console.log("Can't save, newName: ", newName, " list length: ", thisView.current_list.length)
                        alert('A new selection must have a name and selected polygons');
                    }
                    else {
                        console.log('Attempting to save....')
                        thisView.groups_dict[newName] = thisView.current_list.slice(0)
                        console.log('You saved some data');
                        console.log("Selection Name: ", newName);
                        console.log(thisView.groups_dict[newName]);
                        thisView.model.trigger('change:groups_dict');
                    }
                }
        },

        render: function() {
            PolygonSelectorView.__super__.render.apply(this, arguments);
            this.groups_dict = this.model.get('groups_dict')
            this.current_list = this.model.get('current_list')

            this.content_changed();
            this.el.innerHTML = `${this.model.get('content')}`;

            this.model.on('change:content', this.content_changed, this);
            this.model.on('change:current_list', this.content_changed, this);
            this.model.on('change:groups_dict', this.content_changed, this);

            // Each path element is a polygon
            const polygons = this.el.querySelectorAll('#polygonGeometry path');

            // Add click event to polygons
            console.log('iterating through polygons');
            var thisView = this
            let arr = Array.from(polygons)
            console.log('created array:', arr)
            arr.forEach(function(path, i) {
              console.log("Array item #", i)
              path.addEventListener('click', function() {
                console.log('path object clicked')
                if (thisView.current_list.includes(i)) {
                  path.classList.remove('selectedPolygon')
                  thisView.remove(i);
                  console.log('path #', i, ' removed');
                } else {
                  path.classList.add('selectedPolygon')
                  thisView.add(i);
                  console.log('path #', i, ' added');
                }
                thisView.content_changed();
              });
              console.log('path #', i, ' click set');
            });

            // Attach functions to buttons
            this.el.querySelector('#clearBtn').addEventListener('click', this.clear(this));
            console.log('clearBtn action set to current view context');
            this.el.querySelector('#saveBtn').addEventListener('click', this.save(this));
            console.log('saveBtn action set to current view context');

            console.log('render exit')

        },

        content_changed: function() {
            console.log('content changed');
            this.model.save();
            console.log("Current list: ", this.current_list);
            console.log("Groups dict: ", this.groups_dict);
        },
    });

    return {
        PolygonSelectorView : PolygonSelectorView
    };
});

问题

The interoperability between Shapely and Jupyter/iPython is nice. I can do cool stuff like create a bunch of geometric shapes and view them in the notebook:

some_nodes = [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2]]
some_boxes = []
some_boxes.append([some_nodes[0], some_nodes[3], some_nodes[4], some_nodes[1]])
some_boxes.append([some_nodes[1], some_nodes[4], some_nodes[5], some_nodes[2]])

from shapely.geometry import MultiPolygon, Polygon
MultiPolygon([Polygon(box) for box in some_boxes])

...and Jupyter will show me this:

Jupyter_Shapely_output

Now THAT is cool! It has been especially useful to me for quickly viewing and editing, for example, polygons that make up 2D finite element meshes.

Sadly, the images produced are just static SVG graphics; there's no interaction built in. It would be helpful to be able to select a subset of those objects in the image using this same graphical interface in iPython.

More specifically, I'd like to be able to create a list and add some of the displayed polygons to it by, for example, clicking/selecting them, or dragging a lasso/box around them, and also perhaps remove them when clicking a second time.

I've looked into trying to do this with matplotlib or javascript and although I've had some initial success it's probably the kind of project that is out of my depth at my current level of knowledge/skill.

Since Jupyter is a somewhat sprawling tool with lots of features that I am probably not aware of, I am wondering if there are already existing solutions out there for interaction of this sort in the context of a Jupyter notebook?


UPDATE#1: It looks like I'm going to have to create something myself. Happily, this tutorial is going to make that a LOT easier.

UPDATE #2: It appears that Bokeh is a library much more suited for this purpose. I believe I am going to abandon the idea of creating custom Jupyter widgets and create an app using Bokeh widgets and interactions instead. Such an app can be used in a Jupyter notebook, but also elsewhere.

UPDATE #3: I ended up using the jupyter widget system anyway. Added my own answer showing a proof of concept.

解决方法

Solved this using vanilla javascript APIs and the custom IPywidgets system. If you copy and paste this code, note that the cells are shown out of order. Code is available here.

Usage

(Cell #3)

import shapely.geometry as geo

some_nodes = [[0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2]]
some_boxes = []
some_boxes.append([some_nodes[0], some_nodes[3], some_nodes[4], some_nodes[1]])
some_boxes.append([some_nodes[1], some_nodes[4], some_nodes[5], some_nodes[2]])

m_polygon = geo.MultiPolygon(geo.Polygon(box) for box in some_boxes)
poly_selector = PolygonSelector(m_polygon._repr_svg_())  # PolygonSelector defined below
poly_selector  # display the selector below cell, use the tool

Tool looks something like this:

enter image description here

After using the tool, you can obtain the currently selected polygon indices by copying the groups_dict attribute of the selector tool instance, which is "live":

(Cell #4)

polygon_indexes = poly_selector.groups_dict.copy()
polygon_indexes

Code

Work is still in progress, but below is an illustration of what I have ended up doing. Here also is a link to the notebook on nbviewer (the tool is not visible there).

I am putting this here partly for my own reference, but it's a proof of concept others can learn from (and improve upon). Some things aren't working the way I want-- such as changing colors of objects when they are selected. But the primary functionality, selecting and saving clicked polygons, works.

Below is the code cell by cell, as I have it in linked version above.

Python Code

(Cell #1)

import ipywidgets.widgets as widgets
from traitlets import Unicode, Dict, List
from random import randint

class PolygonSelector(widgets.DOMWidget):
    _view_name = Unicode('PolygonSelectorView').tag(sync=True)
    _view_module = Unicode('polygonselector').tag(sync=True)
    groups_dict = Dict().tag(sync=True)
    current_list = List().tag(sync=True)
    content = Unicode().tag(sync=True)

    html_template = '''
    <style>
    # polygonGeometry path{{
        fill: 'pink';
    }}
    # polygonGeometry .selectedPolygon {{
        fill: {fill_selected!r};
    }}
    # polygonGeometry path:hover {{
        fill: {fill_hovered!r};
    }}
    {selection_styles}
    </style>
    <button id = "clearBtn"> Clear </button>
    <input placeholder = "Name this collection" id = "name" />
    <button id = "saveBtn"> Save </button>
    <div id = "polygonGeometry">{svg}</div>
    '''

    # provide some default colors; can override if desired
    fill_selected = "plum"
    fill_hovered = "lavender"
    group_colors = ["#{:06X}".format(randint(0,0xFFFFFF)) for _ in range(100)]

    def __init__(self, svg):
        super().__init__()
        self.update_content(svg)

    def update_content(self, svg):
        self.content = self.html_template.format(
            fill_selected = self.fill_selected,
            fill_hovered = self.fill_hovered,
            selection_styles = self.selection_styles,
            svg = svg
        )

    @property
    def selection_styles(self):
        return "".join(f'''
        # polygonGeometry .selection_{group_idx} {{
            fill: {self.group_colors[group_idx]!r};
        }}
        ''' for group_idx in range(len(self.groups_dict)))

Javascript Code

(Cell #2)

%%javascript

require.undef('polygonselector');

define('polygonselector', ["@jupyter-widgets/base"], function(widgets) {

    var PolygonSelectorView = widgets.DOMWidgetView.extend({

        initialized: 0,

        init_render: function(){

        },


        // Add item to selection list
        add: function(id) {
          this.current_list.push(id);
          console.log('pushed #', id);
        },

        // Remove item from selection list
        remove: function(id) {
          this.current_list = this.current_list.filter(function(_id) {
            return _id !== id;
          })
          console.log('filtered #', id);
        },

        // Remove all items, closure
        clear: function(thisView) {
                return function() {
                    // `this` is the button element
                    console.log('clear() clicked');
                    thisView.el.querySelector('#name').value = '';
                    thisView.current_list.length = 0;
                    Array.from(thisView.el.querySelectorAll('.selectedPolygon')).forEach(function(path) {
                        console.log("path classList is: ", path.classList)
                        path.classList.remove('selectedPolygon');
                    })
                    console.log('Data cleared');
                    console.log(thisView.current_list)
                };
        },

        // Add current selection to groups_dict, closure
        save: function(thisView) {
                return function() {
                    // `this` is the button element
                    console.log('save() clicked');
                    const newName = thisView.el.querySelector('#name').value;
                    console.log('Current name: ', newName)
                    if (!newName || thisView.current_list.length < 1) {
                        console.log("Can't save, newName: ", newName, " list length: ", thisView.current_list.length)
                        alert('A new selection must have a name and selected polygons');
                    }
                    else {
                        console.log('Attempting to save....')
                        thisView.groups_dict[newName] = thisView.current_list.slice(0)
                        console.log('You saved some data');
                        console.log("Selection Name: ", newName);
                        console.log(thisView.groups_dict[newName]);
                        thisView.model.trigger('change:groups_dict');
                    }
                }
        },

        render: function() {
            PolygonSelectorView.__super__.render.apply(this, arguments);
            this.groups_dict = this.model.get('groups_dict')
            this.current_list = this.model.get('current_list')

            this.content_changed();
            this.el.innerHTML = `${this.model.get('content')}`;

            this.model.on('change:content', this.content_changed, this);
            this.model.on('change:current_list', this.content_changed, this);
            this.model.on('change:groups_dict', this.content_changed, this);

            // Each path element is a polygon
            const polygons = this.el.querySelectorAll('#polygonGeometry path');

            // Add click event to polygons
            console.log('iterating through polygons');
            var thisView = this
            let arr = Array.from(polygons)
            console.log('created array:', arr)
            arr.forEach(function(path, i) {
              console.log("Array item #", i)
              path.addEventListener('click', function() {
                console.log('path object clicked')
                if (thisView.current_list.includes(i)) {
                  path.classList.remove('selectedPolygon')
                  thisView.remove(i);
                  console.log('path #', i, ' removed');
                } else {
                  path.classList.add('selectedPolygon')
                  thisView.add(i);
                  console.log('path #', i, ' added');
                }
                thisView.content_changed();
              });
              console.log('path #', i, ' click set');
            });

            // Attach functions to buttons
            this.el.querySelector('#clearBtn').addEventListener('click', this.clear(this));
            console.log('clearBtn action set to current view context');
            this.el.querySelector('#saveBtn').addEventListener('click', this.save(this));
            console.log('saveBtn action set to current view context');

            console.log('render exit')

        },

        content_changed: function() {
            console.log('content changed');
            this.model.save();
            console.log("Current list: ", this.current_list);
            console.log("Groups dict: ", this.groups_dict);
        },
    });

    return {
        PolygonSelectorView : PolygonSelectorView
    };
});
相似信息