diff --git a/.gitignore b/.gitignore index 1b607862ab354a72e8a64f9306e954eb9d87d3fb..ba372b72dfc67d0e584b6b48374b517d89f17401 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ audio_examples/SCW1807_20200713_064545.wav +line_clicker/__pycache__ +__pycache__ diff --git a/args.py b/args.py index 32eeed42aa9818719019d328dd3c117fb01bd849..36389f18906445c47784a568dee672db0833c144 100644 --- a/args.py +++ b/args.py @@ -1,5 +1,6 @@ ##### IMPORTATIONS ##### import os +import sys import argparse from argparse import RawTextHelpFormatter @@ -36,8 +37,9 @@ def fetch_inputs(): parser = argparse.ArgumentParser( formatter_class=RawTextHelpFormatter, description= - ("This script requires LIBRARIES." - "\nAnd it does things, TO BE DESCRIBED.") + ("This script requires libraries that can be installed with: \n" + "'$ pip install -r requirements.txt'" + "Read README.md to get an overview of this software.") ) parser.add_argument( @@ -122,7 +124,7 @@ def fetch_inputs(): f"\nInputError: Max number of contours cannot exceed 50, got {contour}.") except Exception as e: print(e) - exit() + sys.exit(1) return (explore, contour, resampl, outputs, modify_file, from_wav) diff --git a/functions.py b/functions.py index 669efca7ac0e64893d6ebc614bd04a0c81c43769..80d4566e0339e6fbaa49ef507d4a35026f3d1b4e 100644 --- a/functions.py +++ b/functions.py @@ -6,9 +6,6 @@ import numpy as np from librosa import load, amplitude_to_db, stft, pcen from scipy.signal import resample -from line_clicker.line_clicker import to_curve - - ##### FUNCTIONS ##### def save_dict(dictionary, folder, name): """ diff --git a/interface.py b/interface.py index 75e1defbfb783d351dde8f09ee38b64c28a5e8f7..2d982ad82a02273918bf6b4b63080f3a558ab177 100644 --- a/interface.py +++ b/interface.py @@ -82,116 +82,166 @@ class FileExplorer(object): ('All files', '*.*') )) -class App(object): + +class Popup(object): """ - A Class to construct an contours annotation tool for audio data. - - ... + A Class that opens a popup asking if the user wants to save its work + before leaving. Parameters ---------- - DIR : str - Path to a folder in which the file explorer will be opened. - DIR_OUT : str - Path to a folder where the contours will be saved. - MAX_C : int - Maximum number of contours that can be drawn at once. - NEW_SR : int - Resampling rate of the audio recording. - WAVEFILE : str - Path to the audio recording to be opened. Should be a '.wav' file. - coords_to_modify : dict - Coordinates of points (from a previous annotation) that can be used - as input to add modifications. + options : list + The list of options that will show in popup. + prompt : str + The prompt that will be shown on top of the buttons. + """ - Attributes - ---------- - _default_bounds : list of float - Boundaries for the matplotlib canvas. - (Default value is loaded from 'PARAMETERS.py' file). - _default_clipping : int - Default clipping value for spectrogram, in dB. - (Default value is loaded from 'PARAMETERS.py' file). - _default_cmap : str - Name of a matplotlib.pyplot color map. - (Default value is loaded from 'PARAMETERS.py' file). - _default_height : int - Default height of the window in which the app will run, in pixels. - (Default value is loaded from 'PARAMETERS.py' file). - _default_hop_length : int - Default hop length for spectrogram, in samples. - (Default value is loaded from 'PARAMETERS.py' file). - _default_left_panel_width : int - Default width for the left panel of the window of the app. - (Default value is loaded from 'PARAMETERS.py' file). - _default_nfft : int - Default fft size for spectrogram, in samples. - (Default value is loaded from 'PARAMETERS.py' file). - _default_width: int - (Default) width of the window in which the app will run, in pixels. - (Default value is loaded from 'PARAMETERS.py' file). - - canvas : matplotlib object - Interface to include matplotlib plot in tkinter canvas. - CHECK_bspline : tkinter int variable - User checkbox about plotting curves or not. - CLIP_IN : tkinter float variable (Double) - User input for clipping value. - FFT_IN : tkinter int variable - User input for fft. - figure, axis, data_showed : matplotlib objects - Objects used to show matplotlib.pyplot plot. - HOP_IN : tkinter int variable - User input for hop length. - klicker : mpl_point_clicker instance - Adds widgets to matplotlib plot that allows to draw contours. - NAME0 : int - Number used to name the first contour available for annotation. - NAME1 : int - Number used to name the last contour available for annotation. - OPTIONS : tkinter list variable - User listbox to select category item. - root : tkinter Tk instance - Initialises tkinter interpreter and creates root window. - spectrogram : numpy array - Spectrogram of the waveform. - waveform : numpy array - Waveform of the audio recording. - - Other attributes, buttons and labels have self explenatory names. + def __init__(self, options, prompt): + self.options = options + self.prompt = prompt + self.popup_window() # start function auto - Methods - ------- - bspline_activation(): - Activates/deactivates the visualisation of lines as curves. - create_canvas(): - Creates matplotlib figure to show spectrogram in tkinter canvas. - entry_setup(): - Creates variables that save inputs from the user in the entry fields. - get_key_pressed(event): - Updates plot and tkinter interface - when a key is pressed to add a new category. - layout(): - Lays the main structure of the tkinter window. - link_select(event): - Changes the focus to be on a new category, corresponding to - the selected item in listbox widget. - load_audio(): - Loads audio data. Waveform and spectrogram. - select_file(): - Opens a file explorer window to select a new wavefile. - Saves contours if a new file is selected. - Updates the canvas to show the new spectrogram. - setup(): - Loads default variables to local variables. - submit(): - Loads user inputs to local variables. - switch(self) - Updates spectrogram displayed to PCEN (and conversely). - _frame_listbox_scroll(): - Just a callable part of layout() - _quit(): - Saves contours and closes the app. + def popup_window(self): + """ + Creates the windown and the layout for buttons/text. + """ + self.root = Toplevel() + Label(self.root, text=self.prompt).grid(row=0, columnspan=len(self.options)) + + for i, option in enumerate(self.options): + Button( + self.root, + text=option, + command=lambda x=option: self.button_pressed(event=x) + ).grid(row=1, column=i) + + self.root.mainloop() + + def button_pressed(self, event): + """ + Saves the button that is pressed to self.pressed. + Then shuts down the window. + + Parameters + ---------- + event : str + The text from the selected option. + """ + self.pressed = event + self.root.quit() + self.root.destroy() + + +class App(object): + """ + A Class to construct an contours annotation tool for audio data. + + ... + + Parameters + ---------- + DIR : str + Path to a folder in which the file explorer will be opened. + DIR_OUT : str + Path to a folder where the contours will be saved. + MAX_C : int + Maximum number of contours that can be drawn at once. + NEW_SR : int + Resampling rate of the audio recording. + WAVEFILE : str + Path to the audio recording to be opened. Should be a '.wav' file. + coords_to_modify : dict + Coordinates of points (from a previous annotation) that can be used + as input to add modifications. + + Attributes + ---------- + _default_bounds : list of float + Boundaries for the matplotlib canvas. + (Default value is loaded from 'PARAMETERS.py' file). + _default_clipping : int + Default clipping value for spectrogram, in dB. + (Default value is loaded from 'PARAMETERS.py' file). + _default_cmap : str + Name of a matplotlib.pyplot color map. + (Default value is loaded from 'PARAMETERS.py' file). + _default_height : int + Default height of the window in which the app will run, in pixels. + (Default value is loaded from 'PARAMETERS.py' file). + _default_hop_length : int + Default hop length for spectrogram, in samples. + (Default value is loaded from 'PARAMETERS.py' file). + _default_left_panel_width : int + Default width for the left panel of the window of the app. + (Default value is loaded from 'PARAMETERS.py' file). + _default_nfft : int + Default fft size for spectrogram, in samples. + (Default value is loaded from 'PARAMETERS.py' file). + _default_width: int + (Default) width of the window in which the app will run, in pixels. + (Default value is loaded from 'PARAMETERS.py' file). + + canvas : matplotlib object + Interface to include matplotlib plot in tkinter canvas. + CHECK_bspline : tkinter int variable + User checkbox about plotting curves or not. + CLIP_IN : tkinter float variable (Double) + User input for clipping value. + FFT_IN : tkinter int variable + User input for fft. + figure, axis, data_showed : matplotlib objects + Objects used to show matplotlib.pyplot plot. + HOP_IN : tkinter int variable + User input for hop length. + klicker : mpl_point_clicker instance + Adds widgets to matplotlib plot that allows to draw contours. + NAME0 : int + Number used to name the first contour available for annotation. + NAME1 : int + Number used to name the last contour available for annotation. + OPTIONS : tkinter list variable + User listbox to select category item. + root : tkinter Tk instance + Initialises tkinter interpreter and creates root window. + spectrogram : numpy array + Spectrogram of the waveform. + waveform : numpy array + Waveform of the audio recording. + + Other attributes, buttons and labels have self explenatory names. + + Methods + ------- + bspline_activation(): + Activates/deactivates the visualisation of lines as curves. + create_canvas(): + Creates matplotlib figure to show spectrogram in tkinter canvas. + entry_setup(): + Creates variables that save inputs from the user in the entry fields. + get_key_pressed(event): + Updates plot and tkinter interface + when a key is pressed to add a new category. + layout(): + Lays the main structure of the tkinter window. + link_select(event): + Changes the focus to be on a new category, corresponding to + the selected item in listbox widget. + load_audio(): + Loads audio data. Waveform and spectrogram. + select_file(): + Opens a file explorer window to select a new wavefile. + Saves contours if a new file is selected. + Updates the canvas to show the new spectrogram. + setup(): + Loads default variables to local variables. + submit(): + Loads user inputs to local variables. + switch(self) + Updates spectrogram displayed to PCEN (and conversely). + _frame_listbox_scroll(): + Just a callable part of layout() + _quit(): + Saves contours and closes the app. """ from parameters import (_default_width, _default_height, _default_hop_length, @@ -243,8 +293,21 @@ class App(object): self.figure.canvas.mpl_disconnect(self.klicker.key_press) self.root.bind('<Key>', self.get_key_pressed) + # just to be sure + self.root.protocol("WM_DELETE_WINDOW", self.on_close) + self.root.mainloop() + def on_close(self): + save = Popup(prompt="Save and exit?", options=["Yes", "No", "Cancel"]).pressed + if save == "Yes": + self._quit() + if save == "No": + self.root.quit() + self.root.destroy() + else: + pass + def bspline_activation(self): """ Activates/deactivates the visualisation of lines as curves. @@ -554,7 +617,7 @@ class App(object): os.path.basename(self.WAVEFILE)[:-4]+"-contours.json") self.WAVEFILE = new_wavefile - # display loading scree + # display loading screen self.loading_screen.grid(row=1, column=1, rowspan=14) self.canvas.get_tk_widget().destroy() @@ -578,7 +641,7 @@ class App(object): self.entry_setup() self.layout() self.loading_screen.grid_forget() - + def setup(self): """ A function to create variables based on default values diff --git a/line_clicker/__pycache__/__init__.cpython-39.pyc b/line_clicker/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d7f4827996913926ebbfe4cdca728bac46115bac Binary files /dev/null and b/line_clicker/__pycache__/__init__.cpython-39.pyc differ diff --git a/line_clicker/__pycache__/line_clicker.cpython-39.pyc b/line_clicker/__pycache__/line_clicker.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4fdac3d06e156ecb3508baef0ebe5e9db968a698 Binary files /dev/null and b/line_clicker/__pycache__/line_clicker.cpython-39.pyc differ diff --git a/line_clicker/line_clicker.py b/line_clicker/line_clicker.py index 8b689089580bad3b305d93cc1501ee9d48c347d4..687ed06cd7106cabec6c70002df2b7e13ff5a876 100644 --- a/line_clicker/line_clicker.py +++ b/line_clicker/line_clicker.py @@ -364,8 +364,8 @@ class clicker(object): """ if self.legend_labels[self.current_line] in list(self.coords.keys()): # does this point already exists ? - if [x,y] in self.coords[self.legend_labels[self.current_line]]: - warnings.warn("This point already exists!", UserWarning, stacklevel=2) + if x in np.array(self.coords[self.legend_labels[self.current_line]])[:,0]: + warnings.warn("Cannot place two points at the same timestamp!", UserWarning, stacklevel=2) else: # where should it be inserted ? here = np.where(np.array(self.coords[self.legend_labels[self.current_line]])[:,0] > x)[0] @@ -592,15 +592,20 @@ class clicker(object): if (self.currently_pressed and event.xdata != None and event.ydata != None): - # update coords - current_lines = self.figure_lines[self.current_line].get_data() - current_lines[0][self.index] = event.xdata - current_lines[1][self.index] = event.ydata - self.coords[self.legend_labels[self.current_line]][self.index] = [event.xdata, event.ydata] - - # update plot - self.update_lines() - self.figure.canvas.draw() + + # does this point already exists ? + if event.xdata in np.array(self.coords[self.legend_labels[self.current_line]])[:,0]: + warnings.warn("Cannot place two points at the same timestamp!", UserWarning, stacklevel=2) + else: + # update coords + current_lines = self.figure_lines[self.current_line].get_data() + current_lines[0][self.index] = event.xdata + current_lines[1][self.index] = event.ydata + self.coords[self.legend_labels[self.current_line]][self.index] = [event.xdata, event.ydata] + + # update plot + self.update_lines() + self.figure.canvas.draw() def set_legend(self): """ diff --git a/outputs/SCW1807_20200713_064554-contours.json b/outputs/SCW1807_20200713_064554-contours.json index 9ee2ef6e84849a8dc6bd4a64f92c3402a9d8f412..55c8c0c2f4e1c0984a983b2fc70e77cbdce1e873 100644 --- a/outputs/SCW1807_20200713_064554-contours.json +++ b/outputs/SCW1807_20200713_064554-contours.json @@ -101,8 +101,8 @@ 9632.580796877977 ], [ - 1.1769355956020835, - 9171.762083884514 + 1.1794070626717919, + 9479.66604006841 ], [ 1.2234946472488883, @@ -187,28 +187,32 @@ 15162.405352799564 ], [ - 1.446383724281465, - 16496.35425883328 + 1.4413724760470132, + 16287.542250441644 ], [ - 1.44676171875, - 17700.91851851852 + 1.4490057835548567, + 19078.485781877196 ], [ - 1.44676171875, - 19269.57037037037 + 1.4515679818356721, + 19196.707818930037 ], [ - 1.4532515624999998, - 19254.340740740743 + 1.4733184911158894, + 18661.618655692724 ], [ - 1.4662312499999999, - 19117.274074074077 + 1.4937878542675378, + 17396.273242381794 ], [ - 1.4857007812499998, - 18751.762962962963 + 1.507527807781656, + 16956.60405592277 + ], + [ + 1.5294099559708072, + 16899.255901167242 ] ], "Line3": [ diff --git a/post_annotation.py b/post_annotation.py index e0dc1305a7dfe2c74b1725225071d4fd4f582426..f3fd29d5e20b7d34b32e15472952febcf72fd200 100644 --- a/post_annotation.py +++ b/post_annotation.py @@ -126,10 +126,13 @@ class Results(object): for idx, key in enumerate(list(self.coords.keys())): if mode=="curves": - cx, cy = to_curve( - np.array(self.coords[key])[:,0], - np.array(self.coords[key])[:,1], - kind="quadratic") + if len(np.array(self.coords[key])[:,0])>2: + cx, cy = to_curve( + np.array(self.coords[key])[:,0], + np.array(self.coords[key])[:,1], + kind="quadratic") + else: + cx, cy = np.array(self.coords[key])[:,0], np.array(self.coords[key])[:,1] ax.plot(cx, cy, color=self.colors[idx%len(self.colors)]) else: @@ -150,10 +153,10 @@ class Results(object): fig, ax = self.display_image(img) for idx, key in enumerate(list(self.coords.keys())): - min_min = (min(np.array(annot_data.coords[key])[:,0]), - min(np.array(annot_data.coords[key])[:,1])) - max_max = (max(np.array(annot_data.coords[key])[:,0]), - max(np.array(annot_data.coords[key])[:,1])) + min_min = (min(np.array(self.coords[key])[:,0]), + min(np.array(self.coords[key])[:,1])) + max_max = (max(np.array(self.coords[key])[:,0]), + max(np.array(self.coords[key])[:,1])) min_min = (min_min[0]-tol*min_min[0], min_min[1]-tol*min_min[1]) max_max = (max_max[0]+tol*max_max[0], max_max[1]+tol*max_max[1])