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

3

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 notebook的上下文中是否已经存在解决这种交互问题的现成解决方案?


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

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

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

3个回答

2
BokehPlotly是两个支持空间数据的Python交互式可视化库。您可以查看一些示例(12)以确定是否符合您的需求。这个代码库包含了一些非常酷的2D和3D可视化示例,您可以在Jupyter笔记本中直接运行。您还可以使用GeoPandas和Folium创建完全交互式地图(这里有一个很棒的教程)。

1
使用纯JavaScript API 和 自定义 IPywidgets 系统 解决了这个问题。如果您复制并粘贴此代码,请注意单元格的顺序可能会显示不正确。代码在此处可用

用法

(第三个单元格)

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

代码

工作仍在进行中,但下面是我最终做出的示例。这里也有一个链接到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
    };
});

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接