1
\$\begingroup\$

Usually I have really noisy datasets, so peak detection is quite hard to do just with coding. However the human brain immediately sees what is just random noise. I decided to make a basic peak editing functionality with matplotlib. I added a toggle button to the navigation toolbar to switch on and off the recording. This is necessary because this way we can safely zoom. Note that this is a private class, which is not called explicitly by the user, that's why I don't have too detalied docstrings there (Mostly input validation is done elsewhere). Basically the x, y are the dataset, x_extremal and y_extremal are the detected peaks in the dataset.

import warnings

# for matplotlib
warnings.filterwarnings("ignore", category=UserWarning) 

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.backend_bases import MouseButton
from matplotlib.backend_tools import ToolToggleBase

def get_closest(x_value, x_array, y_array):
    """Finds the closest point in a graph to a given x_value, where distance is 
       measured with respect to x.
     """
    idx = (np.abs(x_array - x_value)).argmin()
    value = x_array[idx]
    return value, y_array[idx], idx

class SelectButton(ToolToggleBase):
    """
    Toggle button on matplotlib toolbar.
    """
    description = 'Enable click records'
    default_toggled = True
    default_keymap = 't'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)


class EditPeak:
    """
    This class helps to record and delete peaks from a dataset.
    Right clicks will delete the closest (distance is measured
    with regards to x) extremal point found on the graph, left
    clicks will add a new point. Edits can be saved by just
    closing the matplotlib window.
    """
    def __init__(self, x, y, x_extremal=None, y_extremal=None):

        # This is here because we don't want other figures
        # to change.
        matplotlib.rcParams["toolbar"] = "toolmanager"

        self.figure = plt.figure()
        self.cid = None
        self.x = x
        self.y = y
        plt.plot(self.x, self.y, 'r')
        self.x_extremal = x_extremal
        self.y_extremal = y_extremal
        if not len(self.x_extremal) == len(self.y_extremal):
            raise ValueError('Data shapes are different')
        self.press()
        self.lins, = plt.plot(
            self.x_extremal, self.y_extremal, 'ko', markersize=6, zorder=99
            )
        plt.grid(alpha=0.7)
        # adding the button to navigation toolbar
        tm = self.figure.canvas.manager.toolmanager
        tm.add_tool('Toggle recording', SelectButton)
        self.figure.canvas.manager.toolbar.add_tool(
            tm.get_tool('Toggle recording'), "toolgroup"
            )
        self.my_select_button = tm.get_tool('Toggle recording')
        plt.show()

    def on_clicked(self, event):
            """ Function to record and discard points on plot."""
            ix, iy = event.xdata, event.ydata
            if self.my_select_button.toggled:
                if event.button is MouseButton.RIGHT:
                    ix, iy, idx = get_closest(
                        ix, self.x_extremal, self.y_extremal
                        )
                    self.x_extremal = np.delete(self.x_extremal, idx)
                    self.y_extremal = np.delete(self.y_extremal, idx)
                elif event.button is MouseButton.LEFT:
                    ix, iy, idx = get_closest(ix, self.x, self.y)
                    self.x_extremal = np.append(self.x_extremal, ix)
                    self.y_extremal = np.append(self.y_extremal, iy)
                self.lins.set_data(self.x_extremal, self.y_extremal)
                plt.draw()
                return

    def press(self):
            """Usual function to connect matplotlib.."""
            self.cid = self.figure.canvas.mpl_connect(
                'button_press_event', self.on_clicked
                )

    def release(self):
            """ On release functionality. It's never called  but we will
            need this later on.."""
            self.figure.canvas.mpl_disconnect(self.cid)

    @property
    def selected_points(self):
        """ Returns the selected points."""
        return self.lins.get_data()

The easiest way you can try it (even though it doesn't really make sense with random values):

x = np.random.normal(0,1,100)
y = np.random.normal(0,1,100)
e = EditPeak(x, y, x[::2], y[::2])
print(e.selected_points)

As suggested, a better example is:

x = np.linspace(0,20,100)
y = np.cos(x) + 0.5 * np.random.normal(size=x.shape)
e = EditPeak(x, y, x[::4], y[::4])
print(e.selected_points)

I really appreciate every improvement in the code.

\$\endgroup\$
1
  • \$\begingroup\$ You can make example data more sensible by using something like : x = np.linspace(0,20,100), y = np.cos(x) + 0.5 * np.random.normal(size=x.shape) and e = EditPeak(x, y, x[::15], y[::15]). As long as there are no answers, it is oke to change the question. \$\endgroup\$
    – Jan Kuiken
    Dec 15, 2019 at 13:31

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.