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

时间:2017-04-12 05:28:12

标签: javascript python jupyter shapely ipywidgets

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:看起来我将不得不自己创造一些东西。令人高兴的是,this tutorial将使这更容易。

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

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

3 个答案:

答案 0 :(得分:2)

BokehPlotly是两个交互式python可视化库,支持空间数据。您可以查看一些示例(12),看看这是否是您要查找的内容。 This repository包含一些非常酷的2D和3D可视化示例,您可以在jupyter笔记本中运行它们。您还可以使用GeoPandas和Folium来创建完全交互式的地图(here是一个很棒的教程)。

答案 1 :(得分:1)

使用原始javascript API和custom IPywidgets system解决了该问题。如果您复制并粘贴此代码,请注意,单元格显示不正确。 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

工具看起来像这样:

enter image description here

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

(单元格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
    };
});

答案 2 :(得分:0)

另请参阅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

希望你喜欢!