Helpers - компьютеры, интернет, программирование

Измените цвет точек данных при выборе и удалите их нажатием клавиши на диаграмме рассеяния matplotlib 3d

У меня есть трехмерный точечный график в matplotlib, и я настроил аннотации, вдохновленные ответами здесь, особенно от Дона Кристобаля.

У меня есть базовый код захвата событий, но после нескольких дней попыток мне так и не удалось достичь своих целей. Это:

(i) Изменить цвет точки (точки) при выборе левой кнопкой мыши с синего на, например. темно-синий/зеленый.

(ii) Удалите любую выделенную точку, выбранную в (i), после нажатия клавиши «удалить», включая любые аннотации.

(iii) Выберите несколько точек в (i) с помощью прямоугольника выбора и удалите, используя ' удалить ключ

Я испробовал множество подходов, в том числе анимацию диаграммы для обновления на основе изменений данных, манипулирование параметрами исполнителя, изменение точек данных, например. xs, ys, zs = graph._offsets3d (что, похоже, не задокументировано), но безрезультатно.

Я попытался в функции onpick(event):

(i) Взаимодействуйте с точками через event.ind, чтобы изменить цвет, используя event.artist.set_face_colour()

(ii) Удалить точки, используя как artist.remove()

(iii) Удалить точки с помощью xs, ys, zs = graph._offsets3d, удалив соответствующую точку по индексу (event.ind[0]) из xs, ys и zs, а затем сбросить точки графика с помощью graph._offsets3d = xs_new, ys_new, zs_new

(iv) Перерисовка диаграммы или только соответствующих разделов диаграммы (блоттинг?)

без успеха!

Мой текущий код примерно такой, как показано ниже. На самом деле у меня несколько сотен баллов, а не 3 в упрощенном примере ниже. Я хотел бы, чтобы график обновлялся плавно, если это возможно, хотя просто получить что-то полезное было бы здорово. Большая часть кода для этого, вероятно, должна находиться в 'onpick', так как это функция, которая имеет дело с событиями выбора (см. обработчик событий). Я сохранил некоторые свои попытки кода, закомментированные, которые, я надеюсь, могут оказаться полезными. Функция «forceUpdate» предназначена для обновления объекта графа при срабатывании триггера события, но я не уверен, что в настоящее время она что-то делает. функция on_key(event) также в настоящее время не работает: предположительно должна быть настройка, чтобы определить точки для удаления, например. все художники, у которых цвет лица был изменен по сравнению со значением по умолчанию (например, удалить все точки с темно-синим/зеленым цветом, а не светло-голубым).

Буду признателен за любую оказанную помощь.

Код (ниже) вызывается с помощью:

visualize3DData (Y, ids, subindustry)

Некоторые образцы данных приведены ниже:

#Datapoints
Y = np.array([[ 4.82250000e+01,  1.20276889e-03,  9.14501289e-01], [ 6.17564688e+01,  5.95020883e-02, -1.56770827e+00], [ 4.55139000e+01,  9.13454423e-02, -8.12277299e+00]])

#Annotations
ids = ['a', 'b', 'c']

subindustry =  'example'

Мой текущий код здесь:

import matplotlib.pyplot as plt, numpy as np
from mpl_toolkits.mplot3d import proj3d

def visualize3DData (X, ids, subindus):
    """Visualize data in 3d plot with popover next to mouse position.

    Args:
        X (np.array) - array of points, of shape (numPoints, 3)
    Returns:
        None
    """
    fig = plt.figure(figsize = (16,10))
    ax = fig.add_subplot(111, projection = '3d')
    graph  = ax.scatter(X[:, 0], X[:, 1], X[:, 2], depthshade = False, picker = True)  

    def distance(point, event):
        """Return distance between mouse position and given data point

        Args:
            point (np.array): np.array of shape (3,), with x,y,z in data coords
            event (MouseEvent): mouse event (which contains mouse position in .x and .xdata)
        Returns:
            distance (np.float64): distance (in screen coords) between mouse pos and data point
        """
        assert point.shape == (3,), "distance: point.shape is wrong: %s, must be (3,)" % point.shape

        # Project 3d data space to 2d data space
        x2, y2, _ = proj3d.proj_transform(point[0], point[1], point[2], plt.gca().get_proj())
        # Convert 2d data space to 2d screen space
        x3, y3 = ax.transData.transform((x2, y2))

        return np.sqrt ((x3 - event.x)**2 + (y3 - event.y)**2)


    def calcClosestDatapoint(X, event):
        """"Calculate which data point is closest to the mouse position.

        Args:
            X (np.array) - array of points, of shape (numPoints, 3)
            event (MouseEvent) - mouse event (containing mouse position)
        Returns:
            smallestIndex (int) - the index (into the array of points X) of the element closest to the mouse position
        """
        distances = [distance (X[i, 0:3], event) for i in range(X.shape[0])]
        return np.argmin(distances)


    def annotatePlot(X, index, ids):
        """Create popover label in 3d chart

        Args:
            X (np.array) - array of points, of shape (numPoints, 3)
            index (int) - index (into points array X) of item which should be printed
        Returns:
            None
        """
        # If we have previously displayed another label, remove it first
        if hasattr(annotatePlot, 'label'):
            annotatePlot.label.remove()
        # Get data point from array of points X, at position index
        x2, y2, _ = proj3d.proj_transform(X[index, 0], X[index, 1], X[index, 2], ax.get_proj())
        annotatePlot.label = plt.annotate( ids[index],
            xy = (x2, y2), xytext = (-20, 20), textcoords = 'offset points', ha = 'right', va = 'bottom',
            bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5),
            arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0'))
        fig.canvas.draw()


    def onMouseMotion(event):
        """Event that is triggered when mouse is moved. Shows text annotation over data point closest to mouse."""
        closestIndex = calcClosestDatapoint(X, event)
        annotatePlot (X, closestIndex, ids) 


    def onclick(event):
        print('%s click: button=%d, x=%d, y=%d, xdata=%f, ydata=%f' %
              ('double' if event.dblclick else 'single', event.button,
               event.x, event.y, event.xdata, event.ydata))

    def on_key(event):
        """
        Function to be bound to the key press event
        If the key pressed is delete and there is a picked object,
        remove that object from the canvas
        """
        if event.key == u'delete':
            ax = plt.gca()
            if ax.picked_object:
                ax.picked_object.remove()
                ax.picked_object = None
                ax.figure.canvas.draw()

    def onpick(event):

        xmouse, ymouse = event.mouseevent.xdata, event.mouseevent.ydata
        artist = event.artist
        # print(dir(event.mouseevent))
        ind = event.ind
        # print('Artist picked:', event.artist)
        # # print('{} vertices picked'.format(len(ind)))
        print('ind', ind)
        # # print('Pick between vertices {} and {}'.format(min(ind), max(ind) + 1))
        # print('x, y of mouse: {:.2f},{:.2f}'.format(xmouse, ymouse))
        # # print('Data point:', x[ind[0]], y[ind[0]])
        #
        # # remove = [artist for artist in pickable_artists if     artist.contains(event)[0]]
        # remove = [artist for artist in X if artist.contains(event)[0]]
        #
        # if not remove:
        #     # add a pt
        #     x, y = ax.transData.inverted().transform_point([event.x,     event.y])
        #     pt, = ax.plot(x, y, 'o', picker=5)
        #     pickable_artists.append(pt)
        # else:
        #     for artist in remove:
        #         artist.remove()
        # plt.draw()
        # plt.draw_idle()

        xs, ys, zs = graph._offsets3d
        print(xs[ind[0]])
        print(ys[ind[0]])
        print(zs[ind[0]])
        print(dir(artist))

        # xs[ind[0]] = 0.5
        # ys[ind[0]] = 0.5
        # zs[ind[0]] = 0.5   
        # graph._offsets3d = (xs, ys, zs)

        # print(artist.get_facecolor())
        # artist.set_facecolor('red')
        graph._facecolors[ind, :] = (1, 0, 0, 1)

        plt.draw()

    def forceUpdate(event):
        global graph
        graph.changed()

    fig.canvas.mpl_connect('motion_notify_event', onMouseMotion)  # on mouse motion    
    fig.canvas.mpl_connect('button_press_event', onclick)
    fig.canvas.mpl_connect('pick_event', onpick)
    fig.canvas.mpl_connect('draw_event', forceUpdate)

    plt.tight_layout()

    plt.show()

  • Разделите свою проблему на более мелкие части и попытайтесь изолировать настоящие проблемы. (Я не думаю, что этому вопросу не уделялось должного внимания, скорее, он слишком сложен, чтобы получить ответ здесь.) 19.01.2019
  • Спасибо за ваш ответ. Я подумал об этом, но решил, что (i) проблемы связаны: выбор точек данных и манипулирование в обработчике событий, (ii) полный ответ на мой вопрос может быть полезен для других и будет представлять довольно полную, базовую интерактивную трехмерную модель. построить решение в matplotlib, и (iii) у меня есть подозрение, что общее решение относительно простое, учитывая предоставленный код, и что три приведенных выше вопроса могут служить аналогичными примерами в приложении, но я просто не совсем понял, и мою борьбу, по-видимому, разделят и другие. 19.01.2019
  • В какой момент вы боретесь? Кажется, я пропустил эту информацию в вопросе. 19.01.2019
  • (i) - (iv). Я могу легко выбирать точки, так как обработчик событий делает это через onpick(event). Но я не могу понять, как на самом деле что-то делать с этими точками: изменить их цвета, чтобы отразить тот факт, что они были выбраны, удалить их, нажав клавишу (например, «удалить»), и показать это графически. Я пытался изменить атрибуты, используя «исполнитель» и т. д., но это не сработало для меня. 20.01.2019

Ответы:


1

Хорошо, у меня есть хотя бы частичное решение для вас, без выделения прямоугольника, но вы можете выбрать несколько точек и удалить их с помощью одного key_event.

Для изменения цвета нужно изменить graph._facecolor3d, подсказка была в этом отчете об ошибке по поводу set_facecolor не установки _facecolor3d.

Также может быть хорошей идеей переписать вашу функцию как класс, чтобы избавиться от любых необходимых global переменных.

В моем решении есть части, которые не совсем красивы, мне нужно перерисовать фигуру после удаления точек данных, я не смог заставить работать удаление и обновление. Также (см. РЕДАКТИРОВАТЬ 2 ниже). Я еще не реализовал, что произойдет, если удалить последнюю точку данных.

Причина, по которой ваша функция on_key(event) не работала, была проста: вы забыли ее подключить.

Итак, это решение должно удовлетворять целям (i) и (ii):

import matplotlib.pyplot as plt, numpy as np
from mpl_toolkits.mplot3d import proj3d

class Class3DDataVisualizer:    
    def __init__(self, X, ids, subindus, drawNew = True):

        self.X = X;
        self.ids = ids
        self.subindus = subindus

        self.disconnect = False
        self.ind = []
        self.label = None

        if drawNew:        
            self.fig = plt.figure(figsize = (7,5))
        else:
            self.fig.delaxes(self.ax)
        self.ax = self.fig.add_subplot(111, projection = '3d')
        self.graph  = self.ax.scatter(self.X[:, 0], self.X[:, 1], self.X[:, 2], depthshade = False, picker = True, facecolors=np.repeat([[0,0,1,1]],X.shape[0], axis=0) )         
        if drawNew and not self.disconnect:
            self.fig.canvas.mpl_connect('motion_notify_event', lambda event: self.onMouseMotion(event))  # on mouse motion    
            self.fig.canvas.mpl_connect('pick_event', lambda event: self.onpick(event))
            self.fig.canvas.mpl_connect('key_press_event', lambda event: self.on_key(event))

        self.fig.tight_layout()
        self.fig.show()


    def distance(self, point, event):
        """Return distance between mouse position and given data point

        Args:
            point (np.array): np.array of shape (3,), with x,y,z in data coords
            event (MouseEvent): mouse event (which contains mouse position in .x and .xdata)
        Returns:
            distance (np.float64): distance (in screen coords) between mouse pos and data point
        """
        assert point.shape == (3,), "distance: point.shape is wrong: %s, must be (3,)" % point.shape

        # Project 3d data space to 2d data space
        x2, y2, _ = proj3d.proj_transform(point[0], point[1], point[2], plt.gca().get_proj())
        # Convert 2d data space to 2d screen space
        x3, y3 = self.ax.transData.transform((x2, y2))

        return np.sqrt ((x3 - event.x)**2 + (y3 - event.y)**2)


    def calcClosestDatapoint(self, event):
        """"Calculate which data point is closest to the mouse position.

        Args:
            X (np.array) - array of points, of shape (numPoints, 3)
            event (MouseEvent) - mouse event (containing mouse position)
        Returns:
            smallestIndex (int) - the index (into the array of points X) of the element closest to the mouse position
        """
        distances = [self.distance (self.X[i, 0:3], event) for i in range(self.X.shape[0])]
        return np.argmin(distances)


    def annotatePlot(self, index):
        """Create popover label in 3d chart

        Args:
            X (np.array) - array of points, of shape (numPoints, 3)
            index (int) - index (into points array X) of item which should be printed
        Returns:
            None
        """
        # If we have previously displayed another label, remove it first
        if self.label is not None:
            self.label.remove()
        # Get data point from array of points X, at position index
        x2, y2, _ = proj3d.proj_transform(self.X[index, 0], self.X[index, 1], self.X[index, 2], self.ax.get_proj())
        self.label = plt.annotate( self.ids[index],
            xy = (x2, y2), xytext = (-20, 20), textcoords = 'offset points', ha = 'right', va = 'bottom',
            bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5),
            arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0'))
        self.fig.canvas.draw()


    def onMouseMotion(self, event):
        """Event that is triggered when mouse is moved. Shows text annotation over data point closest to mouse."""
        closestIndex = self.calcClosestDatapoint(event)
        self.annotatePlot (closestIndex) 


    def on_key(self, event):
        """
        Function to be bound to the key press event
        If the key pressed is delete and there is a picked object,
        remove that object from the canvas
        """
        if event.key == u'delete':
            if self.ind:
                self.X = np.delete(self.X, self.ind, axis=0)
                self.ids = np.delete(ids, self.ind, axis=0)
                self.__init__(self.X, self.ids, self.subindus, False)
            else:
                print('nothing selected')

    def onpick(self, event):
        self.ind.append(event.ind)
        self.graph._facecolor3d[event.ind] = [1,0,0,1]



#Datapoints
Y = np.array([[ 4.82250000e+01,  1.20276889e-03,  9.14501289e-01], [ 6.17564688e+01,  5.95020883e-02, -1.56770827e+00], [ 4.55139000e+01,  9.13454423e-02, -8.12277299e+00], [3,  8, -8.12277299e+00]])
#Annotations
ids = ['a', 'b', 'c', 'd']

subindustries =  'example'

Class3DDataVisualizer(Y, ids, subindustries)

Чтобы реализовать ваш прямоугольный выбор, вам придется переопределить то, что в настоящее время происходит во время перетаскивания (поворот 3D-графика), или более простым решением будет определение вашего прямоугольника двумя последовательными щелчками.

Затем используйте proj3d.proj_transform, чтобы найти, какие данные находятся внутри этого прямоугольника, найдите индекс указанных данных и перекрасьте его с помощью функции self.graph._facecolor3d[idx] и заполните self.ind этими индексами, после чего нажатие удаления позаботится об удалении всех данных, указанных self.ind.

РЕДАКТИРОВАТЬ: я добавил две строки в __init__, которые удаляют топор/подзаговор перед добавлением нового после удаления точек данных. Я заметил, что взаимодействие графиков стало медленным после того, как несколько точек данных были удалены, поскольку рисунок просто отображал каждый подграфик.

EDIT 2: я узнал, как вы можете изменить свои данные вместо перерисовки всего графика, как указано в этом answer вам придется изменить _offsets3d, который странным образом возвращает кортеж для x и y, но массив для z.

Вы можете изменить его, используя

(x,y,z) = self.graph._offsets3d # or event.artist._offsets3d
xNew = x[:int(idx)] + x[int(idx)+1:]
yNew = y[:int(idx)] + y[int(idx)+1:]
z = np.delete(z, int(idx))
self.graph._offsets3d = (xNew,yNew,z) # or event.artist._offsets3d

Но тогда вы столкнетесь с проблемой удаления нескольких точек данных в цикле, потому что индексы, которые вы сохранили ранее, не будут применяться после первого цикла, вам придется обновить _facecolor3d, список меток... так что Я решил оставить ответ как есть, потому что просто перерисовать график с новыми данными кажется проще и чище.

23.01.2019
  • Спасибо, Фрейя, это было очень полезно. Вы смотрели на использование художников, блитинг и т. д.? Я добился хорошего прогресса в вашем решении, но еще не совсем готов, так как перерисовка очень медленная со многими точками. Я также хочу добавить прозрачную 3D-плоскость наилучшего соответствия, обновляющуюся каждый раз при удалении точки, в идеале рассчитанную с использованием статистических моделей. 27.01.2019
  • @Carl, я быстро рассмотрел использование блиттинга, потому что до того, как я удалил подграфик перед перерисовкой, он уже был медленным с 4 точками данных. Но с удалением подсюжета перед чтением другого стало быстрее, так что я не стал заморачиваться. Но да, с большим количеством точек данных, возможно, стоит реализовать блиттинг. 27.01.2019
  • Новые материалы

    Интуитивное понимание тензоров в машинном обучении
    Тензор является важной концепцией во многих научных областях, таких как математика, физика, обработка сигналов и компьютерное зрение, и это лишь некоторые из них. В математике тензор — это..

    Использование машинного обучения для диагностики болезни Альцгеймера, часть 4
    Маркеры семантической согласованности для ранней диагностики болезни Альцгеймера (arXiv) Автор: Давиде Колла , Маттео Дельсанто , Марко Агосто , Бенедетто Витиелло , Даниэле Паоло Радичони..

    Почему объяснимость так важна прямо сейчас?
    По мере того, как системы искусственного интеллекта и инструменты на основе машинного обучения распространяются в нашей повседневной жизни, как практики, так и критики все чаще заявляют о..

    Анимированный математический анализ
    Использование Manim для создания математических анимированных визуализаций Визуализация данных помогает понять скрытые закономерности в данных, которые невозможно визуализировать..

    Создание простого слайдера изображений с помощью JavaScript
    Узнайте, как создать базовый слайдер изображений с помощью HTML, CSS и JavaScript. Введение В этом уроке мы создадим удобный слайдер изображений, используя JavaScript, HTML и CSS. Ползунок..

    Создание базы данных с помощью супергероя «Python»
    В этом посте мы узнаем, как создать «базу данных SQLite с помощью модуля python sqlite3, создав простую функцию входа и регистрации. Готовы ли вы к этому путешествию? Если да , давайте приступим..

    ИИ для чайников: руководство для начинающих по пониманию будущего технологий
    Вы чувствуете, что остались позади в мире ИИ? Не волнуйтесь, вы не одиноки! Со всей этой шумихой вокруг искусственного интеллекта может быть трудно понять, с чего начать. Но не позволяйте сленгу..