Jupyter/iPython 笔记本中的图形化 select 几何对象

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 会向我显示:

太棒了!它对我来说特别有用,可以快速查看和编辑,例如,构成二维有限元网格的多边形。

遗憾的是,生成的图像只是静态 SVG 图形;没有内置交互。如果能够使用 iPython 中相同的图形界面 select 图像中这些对象的子集,将会很有帮助。

更具体地说,我希望能够创建一个列表并向其中添加一些显示的多边形,例如 clicking/selecting 它们,或者在它们周围拖动 lasso/box ,并可能在第二次单击时删除它们。

我研究过尝试使用 matplotlib 或 javascript 来做到这一点,虽然我已经取得了一些初步的成功,但在我目前的 knowledge/skill。

由于 Jupyter 是一个有点庞大的工具,有很多我可能不知道的功能,我想知道是否已经有解决方案可以在 Jupyter notebook 的上下文中进行此类交互?


更新#1:看来我必须自己创建一些东西。令人高兴的是,this tutorial 将使这一切变得容易得多。

更新 #2:看起来 Bokeh 是一个更适合此目的的库。我相信我会放弃创建自定义 Jupyter 小部件的想法,转而使用 Bokeh 小部件和交互创建一个应用程序。这样的应用程序可以在 Jupyter notebook 中使用,也可以在其他地方使用。

更新 #3:我最终还是使用了 jupyter 小部件系统。添加了我自己的答案,显示了概念证明。

Bokeh and Plotly are two interactive python visualization libraries with support for spatial data. you can check out some examples (1, 2) to see if this is what you are looking for. This repository contains some very cool examples of 2D and 3D visualizations which you can run right in your jupyter notebook. You can also use GeoPandas and Folium to create fully interactive maps (here 是一个很棒的教程)。

使用 vanilla javascript API 和 custom IPywidgets system. If you copy and paste this code, note that the cells are shown out of order. Code is available here.

解决了这个问题

用法

(单元格 #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属性获取当前选中的多边形索引,即"live":

(单元格 #4)

polygon_indexes = poly_selector.groups_dict.copy()
polygon_indexes

代码

工作仍在进行中,但下面是我最终所做工作的说明。这里还有一个 link to the notebook on 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
    };
});

另请参阅 jp_doodle 套索工具。

这里是独立的Javascript:

https://aaronwatters.github.io/jp_doodle/040_lasso.html

下面是如何在笔记本中使用它:

https://github.com/AaronWatters/jp_doodle/blob/a809653b5bca98de70dc9524e703d95dc7c4067b/notebooks/Feature%20demonstrations/Lasso.ipynb

希望你喜欢!