diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..1b607862ab354a72e8a64f9306e954eb9d87d3fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +audio_examples/SCW1807_20200713_064545.wav diff --git a/PyAVA.py b/PyAVA.py index b054f7b4445bbcda1c80dc337c5a2534ac4f48cf..98c0d3b2d98a74934804a948e46fb0df82f42121 100644 --- a/PyAVA.py +++ b/PyAVA.py @@ -1,20 +1,40 @@ ##### IMPORTATIONS ##### +import os +import json from tkinter import * -from classes import FileExplorer, App +from interface import FileExplorer, App from args import fetch_inputs ##### MAIN ##### if __name__ == '__main__': # fetching inputs. - dir_explore, max_traj, new_sr, output = fetch_inputs() + dir_explore, max_traj, new_sr, output, modify, initial_basename = fetch_inputs() - # open explorer to select firt file - groot = Tk() - initial_file = FileExplorer(dir_explore).file - groot.quit() - groot.destroy() + if modify: + with open(os.path.join(output, modify), "r") as f: + coords_to_change = json.load(f) - if len(initial_file) > 0: - # launch UI - MainWindow = App(dir_explore, max_traj, new_sr, output, initial_file) \ No newline at end of file + MainWindow = App( + dir_explore, + max_traj, + new_sr, + output, + os.path.join(dir_explore, initial_basename), + coords_to_change) + + else: + # open explorer to select firt file + groot = Tk() + initial_file = FileExplorer(dir_explore).file + groot.quit() + groot.destroy() + + if len(initial_file) > 0: + # launch UI + MainWindow = App( + dir_explore, + max_traj, + new_sr, + output, + initial_file) diff --git a/README.md b/README.md index 24102004712bf6e2dccfbac9dfc59115fa9f4a12..9f1c8986c3564c4a84e671e05a5136993f4991b6 100644 --- a/README.md +++ b/README.md @@ -11,21 +11,17 @@ - [x] Spectrogram contour annotations. - [x] Spectrogram automatically computed from waveform. -- [x] Switch between custom spectrogram resolutions (fft, hop length, clipping dB value). +- [x] Choose custom spectrogram resolutions (fft, hop length, clipping dB value and PCEN). - [x] Select a new file directly from the interface. - [x] Exportation of contours to local `.json`. - - -## TODO - -- [ ] Switch between waveform and spectrogram views. -- [ ] Bounding Box, time segments and pixel selection. +- [x] Move points once they are placed (with mouse wheel). +- [x] Modification of previous annotation. Save & return later! ## Requirements -- Ubuntu / Windows / macOS (tested only on Ubuntu but it *should* work on all OS). -- Python3 +- Ubuntu / Windows / macOS (tested only on Ubuntu but it *should* work on any OS). +- Python 3.9.7 - Packages in `requirements.txt` Install packages in your python environment with `$ pip install -r requirements.txt`. @@ -34,55 +30,48 @@ Install packages in your python environment with `$ pip install -r requirements. ## Usage ### Execution -For classic use, run `$python PyAVA.py -dir myWavefileFolder -out myOutputFolder` in terminal. +For classic use, download PyAVA folder, then run `$python PyAVA.py -dir myWavefileFolder -out myOutputFolder` in terminal. Run `$python PyAVA.py --help` for details. The annotations are saved in [JSON](http://www.json.org/) files. Each file contains a dict of contours. For each contour there is a list of points, each point is defined by a list of two elements : [time (in sec), frequency (in kHz)]. ### User interaction - Use the toolbar to interact with the plot. -- No toolbar item must be selected in order to annotate. -- Left-click on a name in the legend to activate annotation with it. +- User must not have any toolbar item selected in order to annotate the image. +- Left-click on a name in the listbox to activate annotation with it. - Left-click to place a point from the selected category. - Right-click to remove the nearest point from the selected category. - Click on `Open file explorer` Button to change Wavefile (will end annotation of the current file). - Change resolution of the spectrogram with `FFT`, `Hop length` and `clipping fields`. - Click on `Update display` button to validate inputs. -- Add more contours by clicking on the `MORE CONTOURS` button. ### Re-use data -Two functions are available to load and display the saved annotations : `load_contours_file` and `display_contours`. +To load and display the saved annotations, use the "Results" object. It contains several infos: coordinates of the annotations, spectrogram, waveform and more. -Run the following lines in a python terminal to see an exemple of use : +Open a terminal window in PyAVA folder and run the following lines with Python to see an exemple of use: ```python import os import json -from functions import load_contours_file, display_contours # load functions - -# load annotations -contours = load_contours_file( - os.path.join( - ".", - "outputs", - "SCW6070_20220717_174215-contours.json") - ) -print(json.dumps(contours, indent=4)) # show file content +from post_annotation import Results # load functions -# display annotations -display_contours( - os.path.join( - ".", +# load object with annotation data +annot_data = Results(os.path.join( + ".", "audio_examples", - "SCW1807_20200713_064545.wav"), + "SCW1807_20200713_064554.wav"), os.path.join( - ".", + ".", "outputs", - "SCW1807_20200713_064545-contours.json"), - cmap='jet' - ) + "SCW1807_20200713_064554-contours.json")) +print(json.dumps(annot_data.coords, indent=4)) # show file content +# display annotations +annot_data.display_contours() # or annot_data.display_contours(img="pcen") ``` +To modify previous annotations, run the following line (using annotations on file SCW6070_20220717_174215.wav as an example): +`$python PyAVA_V2.py --modify SCW6070_20220717_174215-contours.json SCW6070_20220717_174215.wav` + ## Support Please contact [me](mailto:loic.lehnhoff@gmail.com) for any question and/or idea. diff --git a/args.py b/args.py index efc65dc431ed032ad4ef6cf2bf92ccaa4e5040ce..7298bce485a494e520475261e105b741d87960d8 100644 --- a/args.py +++ b/args.py @@ -26,6 +26,10 @@ def fetch_inputs(): Resampling rate for audio data. outputs : int Path to folder that will contain outputs. + modify_file : str or False + Path to a json file containing annotations. + from_wav : str or False + Path to a wavefile. """ # setting up arg parser @@ -39,8 +43,8 @@ def fetch_inputs(): parser.add_argument( '-dir', '--directory', type=str, - default=".", - nargs=1, + default="./audio_examples", + nargs='?', required=False, help=("Directory in which the file explorer will open." "\nPick any .wav file to begin annotation. " @@ -51,7 +55,7 @@ def fetch_inputs(): '-max', '--max_contours', type=int, default=15, - nargs=1, + nargs='?', required=False, help=("Number of contours that can be annotated at the same time." "\nDue to restrictions with the module used," @@ -63,7 +67,7 @@ def fetch_inputs(): '-new', '--resampling_rate', type=int, default=96_000, - nargs=1, + nargs='?', required=False, help=("Resampling rate for the Wavefile. " "\nCan be used to speed up spectrogram visualisation. " @@ -73,7 +77,7 @@ def fetch_inputs(): parser.add_argument( '-out', '--output', type=str, - nargs=1, + nargs='?', default=os.path.join(".","outputs"), required=False, help=("Path where the contours will be saved." @@ -81,28 +85,48 @@ def fetch_inputs(): "\nDefault value is './outputs'.\n\n") ) + parser.add_argument( + '-m', '--modify', + type=str, + nargs=2, + required=False, + help=("Name of a file generated by a previous annitation," + "\nand name of the associated wavefile." + "\nThis will open the interface with the contours from this file" + "\nand enable modification of these contours.")) + # fetching arguments args = parser.parse_args() outputs = args.output explore = args.directory contour = args.max_contours resampl = args.resampling_rate + if args.modify == None: + modify_file, from_wav = False, False + else: + modify_file, from_wav = args.modify + # verifying arguments try: assert (os.path.exists(outputs)), ( - f"\nInputError: Could not find dir {outputs}." + f"\nInputError: Could not find dir '{outputs}'." "\n\tCreate a folder that will contain outputs," " or type in an existing folder with `-out folder_name`.") assert (os.path.exists(explore)), ( - f"\nInputError: Could not find dir {explore}.") + f"\nInputError: Could not find dir '{explore}'.") + if modify_file: + assert (os.path.exists(os.path.join(outputs, modify_file))), ( + f"\nInputError: Could not find file '{modify_file}' in '{outputs}'.") + assert (os.path.exists(os.path.join(explore, from_wav))), ( + f"\nInputError: Could not find file '{from_wav}' in '{explore}'.") assert (contour <= 50), ( f"\nInputError: Max number of contours cannot exceed 50, got {contour}.") except Exception as e: print(e) exit() - return (explore, contour, resampl, outputs) + return (explore, contour, resampl, outputs, modify_file, from_wav) # if running `$python ARGS.py -h` for help. if __name__ == '__main__': diff --git a/audio_examples/SCW1807_20200713_064545.wav b/audio_examples/SCW1807_20200713_064545.wav deleted file mode 100644 index 05b0a36a62220beb1d02328ab3fcc3bc95111cb2..0000000000000000000000000000000000000000 Binary files a/audio_examples/SCW1807_20200713_064545.wav and /dev/null differ diff --git a/audio_examples/SCW1807_20200713_064554.wav b/audio_examples/SCW1807_20200713_064554.wav new file mode 100644 index 0000000000000000000000000000000000000000..9bb76a7725268b0417beeecc152657695a77165b Binary files /dev/null and b/audio_examples/SCW1807_20200713_064554.wav differ diff --git a/classes.py b/classes.py deleted file mode 100644 index 121de7f8d64a3282fd1d0e57b159f217b3562c70..0000000000000000000000000000000000000000 --- a/classes.py +++ /dev/null @@ -1,427 +0,0 @@ -##### IMPORTATIONS ##### -import os -import numpy as np -from tkinter import * -from tkinter import filedialog as fd -from matplotlib.backends.backend_tkagg import (NavigationToolbar2Tk, - FigureCanvasTkAgg) -from matplotlib.figure import Figure -from mpl_point_clicker import clicker - -# Import external functions -from functions import (load_waveform, wave_to_spectrogram, get_contours, - save_dict) - - -##### CLASSES ##### -class FileExplorer(object): - """ - A Class to open a file explorer when it is run in an active Tkinter loop. - - ... - - Attributes - ---------- - path : str - Path to a folder in which the file explorer will be opened. - file : str - Path to a file selected by the user in the file explorer window. - - Methods - ------- - explorer_window(): - Calls the tkinter functions that opens a file explorer window. - - """ - - def __init__(self, path): - """ - Constructs all the necessary attributes for the FileExplorer object. - - Parameters - ---------- - path : str - Path to a folder in which the file explorer will be opened. - file : str - Path to a file selected by the user in the file explorer window. - """ - self.path = path # folder to be opened - self.file = "" # file to be returned - self.explorer_window() # start function auto - - def explorer_window(self): - """ - Calls the tkinter function that opens a file explorer window. - Affect the select pass to 'file' attribute. - - ... - - Parameters - ---------- - path : str - Path to the directory in which the file explorer will be opened. - """ - self.file = fd.askopenfilename( - title='Open a file', - initialdir=self.path, - filetypes=( - ('Audio Files', '*.wav'), - ('All files', '*.*') - )) - -class App(object): - """ - A Class to construct an contours annotation tool for audio data. - - ... - - Attributes - ---------- - WIDTH (init with DEFAULT_WIDTH) : int - (Default) width of the window in which the app will run, in pixels. - (Default value is loaded from 'PARAMETERS.py' file). - HEIGHT (init with DEFAULT_HEIGHT) : int - Default height of the window in which the app will run, in pixels. - (Default value is loaded from 'PARAMETERS.py' file). - LEFT_WIDTH (init with DEFAULT_LEFT_WIDTH) : int - Default width for the left panel of the window of the app. - (Default value is loaded from 'PARAMETERS.py' file). - NFFT (init with DEFAULT_NFFT) : int - Default fft size for spectrogram, in samples. - (Default value is loaded from 'PARAMETERS.py' file). - HOP_LENGTH (init with DEFAULT_HOP_LENGTH) : int - Default hop length for spectrogram, in samples. - (Default value is loaded from 'PARAMETERS.py' file). - CLIPPING (init with DEFAULT_CLIPPING) : int - Default clipping value for spectrogram, in dB. - (Default value is loaded from 'PARAMETERS.py' file). - CMAP (init with DEFAULT_CMAP) : str - Name of a matplotlib.pyplot color map. - (Default value is loaded from 'PARAMETERS.py' file). - COORDS (init with DEFAULT_COORDS) : dict - Empty dictionary. - DIR : str - Path to a folder in which the file explorer will be opened. - MAX_C : int - Maximum number of contours that can be drawn at once. - NEW_SR : int - Resampling rate of the audio recording. - DIR_OUT : str - Path to a folder where the contours will be saved. - WAVEFILE : str - Path to the audio recording to be opened. Should be a '.wav' file. - NAME0 : int - Number used to name the first contour available for annotation. - NAME1 : int - Number used to name the last contour available for annotation. - root : tkinter Tk instance - Initialises tkinter interpreter and creates root window. - klicker : mpl_point_clicker instance - Adds widgets to matplotlib plot that allows to draw contours. - waveform : numpy array - Waveform of the audio recording. - spectrogram : numpy array - Spectrogram of the waveform. - audio_duration : int - Duration of the audio recording in seconds. - FFT_IN : int - User input for fft. - HOP_IN : int - User input for hop length. - CLIP_IN : float - User input for clipping value. - figure, axis, data_showed : matplotlib objects - Objects used to show matplotlib.pyplot plot. - canvas : matplotlib object - Interface to include matplotlib plot in tkinter canvas. - Other buttons and labels have self explenatory names. - - Methods - ------- - load_audio(): - Loads audio data. Waveform and spectrogram. - layout(): - Lays the main structure of the tkinter window. - setup(): - Loads default variables to local variables. - entry_setup(): - Creates variables that save inputs from the user in the entry fields. - create_canvas(): - Creates matplotlib figure to be shown in tkinter canvas. - reset_toolbar(): - Resets the toolbar associated to the matplotlib figure, - this is to avoid overlapping when the canvas is updated. - submit(): - Loads user inputs to local variables. - 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. - add_contours(): - Saves current drawn contours, add new contours that can be drawn. - Draws previous contours in a different color/style on the same image. - Previous contours are no longer editable. - _quit(): - Saves contours and closes the app. - """ - - from parameters import (width, height, left_panel_width, nfft, hop_length, - clipping, cmap) - - DEFAULT_WIDTH = width - DEFAULT_HEIGHT = height - DEFAULT_LEFT_WIDTH = left_panel_width - DEFAULT_NFFT = nfft - DEFAULT_HOP_LENGTH = hop_length - DEFAULT_CLIPPING = clipping - DEFAULT_CMAP = cmap - DEFAULT_COORDS = {} - - def __init__(self, DIR, MAX_C, NEW_SR, DIR_OUT, WAVEFILE): - # init variables - self.setup() - self.DIR = DIR - self.MAX_C = MAX_C - self.NEW_SR = NEW_SR - self.DIR_OUT = DIR_OUT - self.WAVEFILE = WAVEFILE - self.NAME0 = 0 - self.NAME1 = MAX_C - - # load audio data - self.load_audio() - - # init interface - self.root = Tk() - self.entry_setup() - self.create_canvas() - self.layout() - - # addon - self.klicker = clicker( - ax=self.axis, - classes=["Line" + str(i+1) for i in range(self.NAME0, self.NAME1)], - **{"linestyle":"solid", "mfc":"white"}) - - # main loop - self.root.mainloop() - - - def load_audio(self): - self.waveform = load_waveform(self.WAVEFILE, self.NEW_SR) - self.spectrogram, self.audio_duration = wave_to_spectrogram( - self.waveform, - self.NEW_SR, - self.NFFT, - self.HOP_LENGTH, - self.CLIPPING) - - - def layout(self): - self.root.wm_title("Spectrogram annotator") - self.root.geometry(f"{str(self.WIDTH)}x{str(self.HEIGHT)}") - self.root.rowconfigure(1, weight=1) - self.root.rowconfigure(10, weight=1) - - - self.fft_label = Label( - self.root, - width=self.LEFT_WIDTH, - text='FFT window size:', - font=('calibre',10, 'bold')) - self.fft_label.grid(row=2, column=0) - - self.fft_entry = Entry( - self.root, - width=self.LEFT_WIDTH, - textvariable=self.FFT_IN, - font=('calibre',10,'normal')) - self.fft_entry.grid(row=3, column=0) - - self.win_label = Label( - self.root, - width=self.LEFT_WIDTH, - text='Hop length:', - font=('calibre',10,'bold')) - self.win_label.grid(row=4, column=0) - - self.win_entry = Entry( - self.root, - width=self.LEFT_WIDTH, - textvariable=self.HOP_IN, - font=('calibre',10,'normal')) - self.win_entry.grid(row=5, column=0) - - self.clip_label = Label( - self.root, - width=self.LEFT_WIDTH, - text='Clipping (dB):', - font=('calibre',10,'bold')) - self.clip_label.grid(row=6, column=0) - - self.clip_entry = Entry( - self.root, - width=self.LEFT_WIDTH, - textvariable=self.CLIP_IN, - font=('calibre',10,'normal')) - self.clip_entry.grid(row=7, column=0) - - self.submit_button = Button( - self.root, - text='Update display', - width=self.LEFT_WIDTH, - command=self.submit) - self.submit_button.grid(row=8, column=0) - - self.more_button = Button( - self.root, - width=self.LEFT_WIDTH, - fg="white", bg="tomato", - activeforeground="white", activebackground="red", - text = ('MORE CONTOURS\nWARNING: current contours' - '\nwill no longer be editable.'), - command=self.add_contours) - self.more_button.grid(row=9, column=0) - - self.quit_button = Button(self.root, text="Save & Quit", command=self._quit) - self.quit_button.grid(row=11, column=0) - - self.explore_button = Button( - self.root, - text="Open file explorer", - command=self.select_file) - self.explore_button.grid(row=11, column=1) - - self.toolbarFrame = Frame(self.root) - self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbarFrame) - self.toolbar.update() - self.toolbarFrame.grid(row=0, column=1, sticky='W') - - self.canvas.get_tk_widget().grid(row=1, column=1, rowspan=10) - - self.loading_screen = Label( - self.root, - text="LOADING SPECTROGRAM... \nThis can take a few seconds.", - font=("gothic", 30), - justify=LEFT) - - - def setup(self): - self.WIDTH = self.DEFAULT_WIDTH - self.HEIGHT = self.DEFAULT_HEIGHT - self.LEFT_WIDTH = self.DEFAULT_LEFT_WIDTH - self.NFFT = self.DEFAULT_NFFT - self.HOP_LENGTH = self.DEFAULT_HOP_LENGTH - self.CLIPPING = self.DEFAULT_CLIPPING - self.CMAP = self.DEFAULT_CMAP - self.COORDS = self.DEFAULT_COORDS.copy() - - - def entry_setup(self): - self.FFT_IN = IntVar(value=self.DEFAULT_NFFT) - self.HOP_IN = IntVar(value=self.DEFAULT_HOP_LENGTH) - self.CLIP_IN = DoubleVar(value=self.DEFAULT_CLIPPING) - - - def create_canvas(self): - self.figure = Figure(figsize=(16, 9)) - self.axis = self.figure.add_subplot() - self.data_showed = self.axis.imshow( - self.spectrogram[::-1], - cmap=self.CMAP, - interpolation='nearest', aspect='auto', - extent=(0, self.audio_duration, 0, self.NEW_SR/2)) - self.axis.set_xlabel("Time (in sec)") - self.axis.set_ylabel("Frequencies (in Hz)") - self.axis.set_title(f"Spectrogram of {os.path.basename(self.WAVEFILE)}") - self.canvas = FigureCanvasTkAgg(self.figure, master=self.root) - self.canvas.draw() - - - def reset_toolbar(self): - self.toolbar.destroy() - self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbarFrame) - self.toolbar.update() - - - def submit(self): - if ((self.FFT_IN != self.NFFT) or - (self.HOP_IN != self.HOP_LENGTH) or - (self.CLIP_IN != self.CLIPPING)): - - self.NFFT = self.FFT_IN.get() - self.HOP_LENGTH = self.HOP_IN.get() - self.CLIPPING = self.CLIP_IN.get() - - self.spectrogram, self.audio_duration = wave_to_spectrogram( - self.waveform, - self.NEW_SR, - self.NFFT, - self.HOP_LENGTH, - self.CLIPPING) - self.data_showed.set_data(self.spectrogram[::-1]) - self.canvas.draw() - - - def select_file(self): - self.new_wavefile = FileExplorer(self.DIR).file - - if len(self.new_wavefile) > 0 : - self.COORDS = get_contours(self.klicker, self.COORDS) - save_dict(self.COORDS, self.DIR_OUT, - os.path.basename(self.WAVEFILE)[:-4]+"-contours.json") - self.COORDS = self.DEFAULT_COORDS.copy() - self.WAVEFILE = self.new_wavefile - del self.new_wavefile - - self.loading_screen.grid(row=1, column=1, rowspan=10) - self.canvas.get_tk_widget().destroy() - - self.load_audio() - self.create_canvas() - self.NAME0 = 0 - self.NAME1 = self.MAX_C - self.klicker = clicker( - ax=self.axis, - classes=["Line"+str(i+1) for i in range(self.NAME0,self.NAME1)], - **{"linestyle":"solid", "mfc":"white"}) - - self.canvas.get_tk_widget().grid(row=1, column=1, rowspan=10) - self.loading_screen.grid_forget() - self.reset_toolbar() - - - def add_contours(self): - self.loading_screen.grid(row=1, column=1, rowspan=10) - self.canvas.get_tk_widget().destroy() - - self.COORDS = get_contours(self.klicker, self.COORDS) - - self.create_canvas() - self.NAME0 = self.NAME0 + self.MAX_C - self.NAME1 = self.NAME1 + self.MAX_C - self.klicker = clicker( - ax=self.axis, - classes=["Line"+str(i+1) for i in range(self.NAME0,self.NAME1)], - **{"linestyle":"solid", "mfc":"white"}) - - for key in list(self.COORDS.keys()): - self.axis.plot( - np.array(self.COORDS[key])[:,0], - np.array(self.COORDS[key])[:,1], - "black", marker="s", mfc="white") - self.canvas.draw() - - self.canvas.get_tk_widget().grid(row=1, column=1, rowspan=10) - self.loading_screen.grid_forget() - self.reset_toolbar() - - - def _quit(self): - save_dict( - get_contours(self.klicker, self.COORDS), - self.DIR_OUT, - os.path.basename(self.WAVEFILE)[:-4]+"-contours.json") - - self.root.quit() - self.root.destroy() \ No newline at end of file diff --git a/functions.py b/functions.py index 50439e319f99ccd3b7dea583966c74c54d766229..ae8ceb51e73cd8da6f46fdbd5bf8f0db075b2f24 100644 --- a/functions.py +++ b/functions.py @@ -3,187 +3,112 @@ import os import json import numpy as np -from matplotlib.figure import Figure -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -import matplotlib.pyplot as plt - -import tkinter as tk -from tkinter import filedialog as fd - -from librosa import load, amplitude_to_db, stft +from librosa import load, amplitude_to_db, stft, pcen from scipy.signal import resample +from line_clicker.line_clicker import to_curve -##### FUNCTIONS ###### + +##### FUNCTIONS ##### def save_dict(dictionary, folder, name): - """ - A function that saves a dictionary to a given path. - - ... - - Parameters - ---------- - dictionary : dict - Any dictionary. - folder : str - Path to the folder where the dictionary will be saved. - name : str - Name of the file in which the dictionary will be saved. - If there is an extension, should not be different than '.json' extension. - """ - if len(dictionary) > 0: - with open(os.path.join(folder, name), "w") as f: - json.dump(dictionary, f, indent=4) + """ + A function that saves a dictionary to a given path. + + ... + + Parameters + ---------- + dictionary : dict + Any dictionary. + folder : str + Path to the folder where the dictionary will be saved. + name : str + Name of the file in which the dictionary will be saved. + If there is an extension, should not be different than .json extension. + + Returns + ------- + None : save dict to json file. + """ + if len(dictionary) > 0: + with open(os.path.join(folder, name), "w") as f: + json.dump(dictionary, f, indent=4) def load_waveform(wavefile_name, sr_resample): - """ - A function that loads any given wavefile - and it resamples it to a given sampling rate. - - ... - - Parameters - ---------- - wavefile_name : str - Path of the wavefile that will be loaded. - sr_resample : int - Resampling rate for the waveform. - - Returns - ------- - wavefile_dec : numpy array - Loaded and resampled waveform - """ - wavefile, sr = load(wavefile_name, sr=None) - wavefile_dec = resample(wavefile, - int(((len(wavefile)/sr)*sr_resample))) - - return wavefile_dec - -def wave_to_spectrogram(waveform, SR, n_fft, w_size, clip): - """ - A function that transforms any given waveform to a spectrogram. - - ... - - Parameters - ---------- - waveform : numpy array - Waveform of an audio recording. Shape should be (N, 1). - SR : int - Sampling rate of the waveform - n_fft : int - Desired size for fft window. Should be in [1, N-1]. - w_size : int - Desired size for hop length between two fft. Should be in [1, N-1]. - clip : int - Clipping value for dB. If pixel value < clip, pixel is turned into a NaN. - - Returns - ------- - spectro : numpy array - Spectrogram of the waveform using the provided parameters. - audio_length : float - Duration of the audio in seconds. - """ - spectro = amplitude_to_db(np.abs(stft(waveform, - n_fft=n_fft, hop_length=w_size))) - spectro = spectro - (np.max(spectro)) - spectro[spectro < clip] = np.nan - - audio_length = len(waveform)/SR - - return spectro, audio_length - -def get_contours(clicker, coords): - """ - A function to get the positions of the clicks - contained in a mpl_point_clicker OBJECT - - ... - - Parameters - ---------- - clicker : mpl_point_clicker object - Object containing the positions of mouse-drawn points in a plt plot. - - Returns - ------- - coords : dict - Gives coordinates of contours in lists [[time (in sec), freq (in Hz)]]. - """ - temp_coords = clicker.get_positions() - for key in list(temp_coords.keys()): - if len(temp_coords[key])>0: - coords[key] = temp_coords[key].tolist() - - return coords - -def load_contours_file(jsonfile): - """ - A function to import the contours saved from the interface. - - ... - - Parameters - ---------- - jsonfile : str - Path to a json file. - - Returns - ------- - contours : dict - Data contained in json file. - """ - with open(jsonfile, "r") as f: - contours = json.load(f) - return contours - -def display_contours(wavefile, jsonfile, - SR=96_000, n_fft=4098, w_size=156, clip=-80, cmap="viridis"): - """ - A function to show the results of annotations, after using the interface. - - ... - - Parameters - ---------- - wavefile : str - Path to a wavefile. - jsonfile : str - Path to a json file. - SR : int - Sampling rate of the waveform - n_fft : int - Desired size for fft window. Should be in [1, N-1]. - w_size : int - Desired size for hop length between two fft. Should be in [1, N-1]. - clip : int - Clipping value for dB. If pixel value < clip, pixel is turned into a NaN. - cmap : str - Color map for matplotlib.pyplot plot. - - Returns - ------- - None. Plots the contours fetched from a jsonfile onto a specgram. - """ - lines = load_contours_file(jsonfile) - - waveform = load_waveform(wavefile, SR) - specgram, duration = wave_to_spectrogram(waveform, SR, n_fft, w_size, clip) - - fig, ax = plt.subplots(figsize=(16,9)) - ax.imshow(specgram[::-1], cmap=cmap, interpolation='nearest', aspect='auto', - extent=(0, duration, 0, SR/2)) - - ax.set_xlabel("Time (in sec)") - ax.set_ylabel("Frequencies (in Hz)") - ax.set_title(f"Spectrogram of {os.path.basename(wavefile)}") - - for key in list(lines.keys()): - ax.plot( - np.array(lines[key])[:,0], - np.array(lines[key])[:,1], - marker="s", mfc="white") - - plt.show() \ No newline at end of file + """ + A function that loads any given wavefile + and it resamples it to a given sampling rate. + + ... + + Parameters + ---------- + wavefile_name : str + Path of the wavefile that will be loaded. + sr_resample : int + Resampling rate for the waveform. + + Returns + ------- + wavefile_dec : numpy array + Loaded and resampled waveform + """ + wavefile, sr = load(wavefile_name, sr=None) + wavefile_dec = resample(wavefile, + int(((len(wavefile)/sr)*sr_resample))) + + return wavefile_dec + +def wave_to_spectrogram(waveform, SR, n_fft, w_size, clip, as_pcen=False): + """ + A function that transforms any given waveform to a spectrogram. + + ... + + Parameters + ---------- + waveform : numpy array + Waveform of an audio recording. Shape should be (N, 1). + SR : int + Sampling rate of the waveform + n_fft : int + Desired size for fft window. Should be in [1, N-1]. + w_size : int + Desired size for hop length between two fft. Should be in [1, N-1]. + clip : int + Clipping value for dB. If pixel value < clip, pixel is turned into NaN. + If as_pcen is selected, clipping will be applied using the values of pixels + in the orgiginal spectrogram. + as_pcen : bool, optional. + Whether the returned image should be a PCEN or not. + Aka : spectrogram with enhanced contrast. + Default is False. + + Returns + ------- + spectro : numpy array + Spectrogram of the waveform using the provided parameters. + audio_length : float + Duration of the audio in seconds. + """ + base = np.abs(stft( + waveform, + n_fft=n_fft, + hop_length=w_size)) + + + spectro_pcen = pcen(base * (2**31), bias=10) + spectro_og = amplitude_to_db(base) + spectro_og = spectro_og - (np.max(spectro_og)) + spectro_og[spectro_og < clip] = np.nan + + if as_pcen: + spectro_pcen[np.isnan(spectro_og)] = np.nan + spectro = spectro_pcen + else: + spectro = spectro_og + + audio_length = len(waveform)/SR + + return spectro, audio_length + diff --git a/images/PyAVA_show.gif b/images/PyAVA_show.gif index 8ba57d86bfe069b86537aea85226f3b78c3593aa..4b02cb6a8fa9cb43ac7bba8d169205e9f836216f 100644 Binary files a/images/PyAVA_show.gif and b/images/PyAVA_show.gif differ diff --git a/interface.py b/interface.py new file mode 100644 index 0000000000000000000000000000000000000000..0dc89000fd6baa4968b9702ac47622ab0323a329 --- /dev/null +++ b/interface.py @@ -0,0 +1,711 @@ +##### IMPORTATIONS ##### +import os +import numpy as np +from tkinter import * +from tkinter import filedialog as fd +from tkinter import ttk +from matplotlib.backends.backend_tkagg import (NavigationToolbar2Tk, + FigureCanvasTkAgg) +from matplotlib.figure import Figure +import matplotlib.colors as mc +from line_clicker.line_clicker import clicker + +# Import external functions +from functions import load_waveform, wave_to_spectrogram, save_dict + + +##### CLASSES ##### +class FileExplorer(object): + """ + A Class that opens a file explorer when it runs in an active Tkinter loop. + + ... + + Parameters + ---------- + path : str + Path to a folder in which the file explorer will be opened. + + Attributes + ---------- + file : str + Path to a file selected by the user in the file explorer window. + + Methods + ------- + explorer_window(): + Calls the tkinter functions that opens a file explorer window. + + """ + + def __init__(self, path): + """ + Constructs all the necessary attributes for the FileExplorer object. + + Parameters + ---------- + path : str + Path to a folder in which the file explorer will be opened. + file : str + Path to a file selected by the user in the file explorer window. + """ + self.path = path # folder to be opened + self.explorer_window() # start function auto + + def explorer_window(self): + """ + Calls the tkinter function that opens a file explorer window. + Affect the select pass to 'file' attribute. + + ... + + Parameters + ---------- + path : str + Path to the directory in which the file explorer will be opened. + """ + self.file = fd.askopenfilename( + title='Open a file', + initialdir=self.path, + filetypes=( + ('Audio Files', '*.wav'), + ('All files', '*.*') + )) + +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. + reset_toolbar(): + Resets the toolbar associated to the matplotlib figure, + this is to avoid overlapping when the canvas is updated. + 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, + _default_nfft, _default_clipping, _default_cmap, _default_bounds, + _default_left_panel_width) + + def __init__( + self, + DIR, + MAX_C, + NEW_SR, + DIR_OUT, + WAVEFILE, + coords_to_modify={}): + + # init variables + self.DIR = DIR + self.MAX_C = MAX_C + self.NEW_SR = NEW_SR + self.DIR_OUT = DIR_OUT + self.WAVEFILE = WAVEFILE + self.NAME0 = 0 + self.NAME1 = MAX_C + self.setup() + + # load audio data + self.load_audio() + + # init interface + self.root = Tk() + self.root.style = ttk.Style() + self.root.style.theme_use('clam') + self.create_canvas() + + # addon + self.klicker = clicker( + axis=self.axis, + names=["Line" + str(i+1) for i in range(self.NAME0, self.NAME1)], + bspline='quadratic', maxlines=99, legend_bbox=(2,0.5), + coords=coords_to_modify) + + # main loop + self.entry_setup() + self.layout() + self.axis.set_position(self._default_bounds) + + # To avoid probles, disconnect matplotlib keypress + # and replace it with tkinter keypress. + self.figure.canvas.mpl_disconnect(self.klicker.key_press) + self.root.bind('<Key>', self.get_key_pressed) + + self.root.mainloop() + + def bspline_activation(self): + """ + Activates/deactivates the visualisation of lines as curves. + + ... + + Returns + ------- + None : Updates klicker. + (It uses the "wait" parameter to force straigth lines). + """ + if self.CHECK_bspline.get(): + self.klicker.wait = 2 + else: + self.klicker.wait = np.inf + self.klicker.update_lines() + self.klicker.figure.canvas.draw() + + def create_canvas(self): + """ + Creates a figure based on imported spectrogram. + + ... + + Returns + ------- + None : Creates figure, axis, data_showed and canvas variables. + """ + self.figure = Figure(figsize=(16, 9)) + self.axis = self.figure.add_subplot() + self.data_showed = self.axis.imshow( + self.spectrogram[::-1], + cmap=self._default_cmap, + interpolation='nearest', aspect='auto', + extent=(0, self.audio_duration, 0, self.NEW_SR/2)) + self.data_showed.set_clim( + vmin=np.nanmin(self.spectrogram), + vmax=np.nanmax(self.spectrogram)) + self.axis.set_xlabel("Time (in sec)") + self.axis.set_ylabel("Frequencies (in Hz)") + self.axis.set_title(f"Spectrogram of {os.path.basename(self.WAVEFILE)}") + self.axis.set_position(self._default_bounds) + self.figure.set_facecolor("gainsboro") + self.canvas = FigureCanvasTkAgg(self.figure, master=self.root) + self.canvas.draw() + + def entry_setup(self): + """ + Creates tkinter variables that will be used in entry fields. + (Objects that are specific to tkinter) + + ... + + Returns + ------- + None : Creates FFT_IN, HOP_IN, CHECK_bspline, CLIP_IN and OPTIONS, + variables that are tkinter variables (3 integers, 1 float, 1 list). + """ + self.FFT_IN = IntVar(value=self._default_nfft) + self.HOP_IN = IntVar(value=self._default_hop_length) + self.CHECK_bspline = IntVar(value=1) + self.CLIP_IN = DoubleVar(value=self._default_clipping) + self.OPTIONS = Variable(value=self.klicker.legend_labels) + + def get_key_pressed(self, event): + """ + Updates plot and tkinter interface + when a key is pressed to add a new category. + + ... + + Parameters + ---------- + event : tkinter event + tkinter object containing the name of the key pressed. + + Returns + ------- + None : updates klicker, listbox, axis and figure. + """ + class EmptyObject(object): + """ + Empty class that is just a hacky of creating an object that + can be used in matplotlib. + """ + pass + + # create key attribute and use it + dummy_event = EmptyObject() + dummy_event.key = event.char + self.klicker.get_key_event(dummy_event, show=False) + + # if a category is added. Update listbox and canvas. + if len(self.klicker.legend_labels) > self.listbox.size(): + self.listbox.insert( + self.listbox.size(), + self.klicker.legend_labels[-1]) + self.listbox.itemconfig( + self.listbox.size()-1, + { + 'bg': self.klicker.colors[ + (self.listbox.size()-1)%len(self.klicker.colors)], + 'selectbackground': mc.to_hex(tuple([min(0.1+x,1) + for x in mc.to_rgb( + self.klicker.colors[ + (self.listbox.size()-1)%len(self.klicker.colors)])])), + 'selectforeground': 'white'}) + self.listbox.select_clear(0, END) + self.listbox.select_set(self.listbox.size()-1) + self.listbox.see(self.listbox.size()-1) + self.listbox.activate(self.listbox.size()-1) + self.listbox.selection_anchor(self.listbox.size()-1) + self.axis.set_position(self._default_bounds) + self.figure.canvas.draw() + + def layout(self): + """ + This *long* function lays the structure of the tkinter interface + + ... + + Returns + ------- + None : + Updates root + Creates list_label, frame_list, listbox, scrollbar, empty_frame, + activate_bspline, fft_label, fft_entry,win_label, win_entry, + clip_label, clip_entry, submit_button, quit_button, explore_button + toolbarFrame, toolbar, loading_screen + """ + # configure main window + self.root.wm_title("Spectrogram annotator") + self.root.geometry( + f"{str(self._default_width)}x{str(self._default_height)}") + self.root.rowconfigure(1, weight=1) + self.root.rowconfigure(14, weight=1) + self.root.configure(bg='gainsboro') + + # Add Panel for line selection on Left side + self.list_label = Label( + self.root, + width=self._default_left_panel_width, + text='Pick a line to draw.\n(Shift+a adds a new line)', + font=('calibre',10,'bold')) + self.list_label.grid(row=2, column=0) + + self._frame_listbox_scroll() + + self.activate_bspline = Checkbutton( + self.root, + text='Activate interpolation', + variable=self.CHECK_bspline, + command=self.bspline_activation) + self.activate_bspline.grid(row=4, column=0) + + # Add space between panels + self.empty_frame = Label( + self.root, + width=self._default_left_panel_width, + height=30) + self.empty_frame.grid(row=5, column=0) + + # Add panel for spectrogram personalisation on Left side. + self.fft_label = Label( + self.root, + width=self._default_left_panel_width, + text='FFT window size:', + font=('calibre',10, 'bold')) + self.fft_label.grid(row=6, column=0) + + self.fft_entry = Entry( + self.root, + width=self._default_left_panel_width, + textvariable=self.FFT_IN, + font=('calibre',10,'normal')) + self.fft_entry.grid(row=7, column=0) + + self.win_label = Label( + self.root, + width=self._default_left_panel_width, + text='Hop length:', + font=('calibre',10,'bold')) + self.win_label.grid(row=8, column=0) + + self.win_entry = Entry( + self.root, + width=self._default_left_panel_width, + textvariable=self.HOP_IN, + font=('calibre',10,'normal')) + self.win_entry.grid(row=9, column=0) + + self.clip_label = Label( + self.root, + width=self._default_left_panel_width, + text='Clipping (dB):', + font=('calibre',10,'bold')) + self.clip_label.grid(row=10, column=0) + + self.clip_entry = Entry( + self.root, + width=self._default_left_panel_width, + textvariable=self.CLIP_IN, + font=('calibre',10,'normal')) + self.clip_entry.grid(row=11, column=0) + + self.submit_button = Button( + self.root, + text='Update display', + width=self._default_left_panel_width, + command=self.submit) + self.submit_button.grid(row=12, column=0) + + self.switch_view_button = Button( + self.root, + text="Switch to PCEN", + width=self._default_left_panel_width, + command=self.switch) + self.switch_view_button.grid(row=13, column=0) + + # Add buttons at the bottom of the interface + self.quit_button = Button( + self.root, + text="Save & Quit", + command=self._quit) + self.quit_button.grid(row=15, column=0) + + self.explore_button = Button( + self.root, + text="Open file explorer", + command=self.select_file) + self.explore_button.grid(row=15, column=1) + + # Add matplotlib tools at the top of the interface + self.toolbarFrame = Frame(self.root) + self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbarFrame) + self.toolbar.update() + self.toolbarFrame.grid(row=0, column=1, sticky='W') + + # Add main panel : canvas. + self.canvas.get_tk_widget().grid(row=1, column=1, rowspan=14) + self.loading_screen = Label( + self.root, + text="LOADING SPECTROGRAM... \nThis can take a few seconds.", + font=("gothic", 30), + justify=LEFT) + + def link_select(self, event): + """ + Changes the focus to be on a new category, corresponding to + the selected item in listbox widget. + + ... + + Parameters + ---------- + event : tkinter object + event containing the item clicked in listbox widget. + + Returns + ------- + None : Updates klicker. + """ + if len(event.widget.curselection()): + self.klicker.current_line = event.widget.curselection()[0] + + # Manually update display + for legend_line in self.klicker.legend.get_lines(): + legend_line.set_alpha(0.2) + self.klicker.legend.get_lines()[self.klicker.current_line].set_alpha(1) + self.klicker.figure.canvas.draw() + + def load_audio(self): + """ + A class to load the waveform and spectrogram of a wavefile. + + ... + + Returns + ------- + None : Creates waveform and spectrogram arrays. + """ + self.waveform = load_waveform(self.WAVEFILE, self.NEW_SR) + self.spectrogram, self.audio_duration = wave_to_spectrogram( + self.waveform, + self.NEW_SR, + self.NFFT, + self.HOP_LENGTH, + self.CLIPPING) + + def reset_toolbar(self): + """ + A function to destroy and create a new toolbar + that interacts with matplotlib canvas. + """ + self.toolbar.destroy() + self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbarFrame) + self.toolbar.update() + + def select_file(self): + """ + A function that calls a new window to select a wavefile. + Then replaces spectrogram in canvas using the newly selected file. + + ... + + Returns + ------- + None : If a new file is selected, saves current coordinates to json file + and generate a new window for annotation. + """ + new_wavefile = FileExplorer(self.DIR).file + + if len(new_wavefile) > 0 : + # save current coords + save_dict(self.klicker.coords, self.DIR_OUT, + os.path.basename(self.WAVEFILE)[:-4]+"-contours.json") + self.WAVEFILE = new_wavefile + + # display loading scree + self.loading_screen.grid(row=1, column=1, rowspan=13) + self.canvas.get_tk_widget().destroy() + + # load new data + self.load_audio() + self.create_canvas() + self.NAME0 = 0 + self.NAME1 = self.MAX_C + + # display new data + self.klicker = clicker( + axis=self.axis, + names=["Line" + str(i+1) for i in range(self.NAME0, self.NAME1)], + bspline='quadratic', maxlines=99, legend_bbox=(2,0.5)) + self.axis.set_position(self._default_bounds) + self.figure.canvas.mpl_disconnect(self.klicker.key_press) + + # update interface + self.entry_setup() + self._frame_listbox_scroll() + self.canvas.get_tk_widget().grid(row=1, column=1, rowspan=13) + self.loading_screen.grid_forget() + self.reset_toolbar() + + def setup(self): + """ + A function to create variables based on default values + This is a security to avoid mixing default values with new values + during use... But it could be removed. + """ + self.NFFT = self._default_nfft + self.HOP_LENGTH = self._default_hop_length + self.CLIPPING = self._default_clipping + + def submit(self): + """ + A function that fetches the new values in entry fields. + Updates spectrogram accordingly. + + --- + + Returns + ------- + None : Updates spectrogram and data_showed + according to new fft, hop_length and clipping values. + """ + if ((self.FFT_IN != self.NFFT) or + (self.HOP_IN != self.HOP_LENGTH) or + (self.CLIP_IN != self.CLIPPING)): + + self.NFFT = self.FFT_IN.get() + self.HOP_LENGTH = self.HOP_IN.get() + self.CLIPPING = self.CLIP_IN.get() + + self.spectrogram, self.audio_duration = wave_to_spectrogram( + self.waveform, + self.NEW_SR, + self.NFFT, + self.HOP_LENGTH, + self.CLIPPING) + self.data_showed.set_data(self.spectrogram[::-1]) + self.data_showed.set_clim( + vmin=np.nanmin(self.spectrogram), + vmax=np.nanmax(self.spectrogram)) + self.canvas.draw() + + def switch(self): + """ + Updates spectrogram displayed to PCEN (and conversely). + + --- + + Returns + ------- + None : Updates switch_view_button, spectrogram and data_showed. + """ + current_text = self.switch_view_button['text'] + + if current_text == "Switch to PCEN": + self.spectrogram, _ = wave_to_spectrogram( + self.waveform, + self.NEW_SR, + self.NFFT, + self.HOP_LENGTH, + self.CLIPPING, + as_pcen=True) + self.switch_view_button['text'] = "Switch to Spectrogram" + else: + self.spectrogram, _ = wave_to_spectrogram( + self.waveform, + self.NEW_SR, + self.NFFT, + self.HOP_LENGTH, + self.CLIPPING) + self.switch_view_button['text'] = "Switch to PCEN" + + self.data_showed.set_data(self.spectrogram[::-1]) + self.data_showed.set_clim( + vmin=np.nanmin(self.spectrogram), + vmax=np.nanmax(self.spectrogram)) + self.canvas.draw() + + def _frame_listbox_scroll(self): + """ + Just a callable part of "layout" + """ + self.frame_list = Frame( + self.root, + width=self._default_left_panel_width, + height=50) + self.frame_list.grid(row=3, column=0) + + self.listbox = Listbox( + self.frame_list, + height=9, + width=self._default_left_panel_width, + selectmode=SINGLE, + listvariable=self.OPTIONS) + for idx in range(len(self.klicker.legend_labels)): + self.listbox.itemconfig(idx, + { + 'bg': self.klicker.colors[idx%len(self.klicker.colors)], + 'selectbackground': mc.to_hex(tuple([min(0.1+x,1) + for x in mc.to_rgb( + self.klicker.colors[idx%len(self.klicker.colors)])])), + 'selectforeground': 'white'}) + self.listbox.pack(side="left", fill="y") + self.listbox.bind("<<ListboxSelect>>", self.link_select) + self.listbox.select_set(0) + + self.scrollbar = Scrollbar(self.frame_list, orient="vertical") + self.scrollbar.config(command=self.listbox.yview) + self.scrollbar.pack(side="right", fill="y") + self.listbox.config(yscrollcommand=self.scrollbar.set) + + def _quit(self): + """ + A function that saves coordinates of lines before closing app. + + ... + + Returns + ------- + None : Saves coords in a json file, quits and destroys root. + """ + save_dict( + self.klicker.coords, + self.DIR_OUT, + os.path.basename(self.WAVEFILE)[:-4]+"-contours.json") + + self.root.quit() + self.root.destroy() \ No newline at end of file diff --git a/line_clicker/__init__.py b/line_clicker/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/line_clicker/line_clicker.py b/line_clicker/line_clicker.py new file mode 100644 index 0000000000000000000000000000000000000000..103db70fcf7d583c4ad81eba03345e937ccd02b0 --- /dev/null +++ b/line_clicker/line_clicker.py @@ -0,0 +1,735 @@ +""" +Add default parameters for keys and mousebutton so it can be customized +""" + +##### IMPORTATIONS ###### +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.backend_bases import MouseButton +import matplotlib.lines as mlines + +from scipy.interpolate import interp1d + +##### FUNCTIONS ##### +def to_curve(x, y, kind): + """ + A function to compute a curvy line between two points. + It interpolates a line with 100 points. + + ... + + Parameters + ---------- + x : list or numpy array + List of coordinates on x axis. + y : list or numpy array + List of coordinates on y axis. + kind : string + The interpolation method to use. Can be any method in : ‘linear’, + ‘nearest’, ‘nearest-up’, ‘zero’, ‘slinear’, ‘quadratic’, ‘cubic’, + ‘previous’, or ‘next’. + + Returns + ------- + xi : numpy array + List of the coordinates of the curve on x-axis. + yi : numpy array + List of the coordinates of the curve on y-axis. + """ + i = np.arange(len(x)) + + interp_i = np.linspace(0, i.max(), 100 * i.max()) + + xi = interp1d(i, x, kind=kind)(interp_i) + yi = interp1d(i, y, kind=kind)(interp_i) + + return xi, yi + +##### CLASSES ##### +class clicker(object): + """ + A Class that is an "add-on" for an existing matplotlib figure axis. + Added to an axis, it allows to draw lines from mouse clicks. + Points can be removed using the right click. + + Different categories can be selected using the generated legend : + by clicking on a line or by using up/down arrows. + It is also possible to add more categories using with 'ctrl+a' shortcut + + ... + + Params + ---------- + axis : matplotlib object + Axis instance. + bspline : string or boolean, optional. + If False, app will plot points linked by lines. + If string, app will plot points linked by curve. + For curves, use 'cubic' or 'quadratic' + colors : list, optional. + List of color (hexadecimal) to cycle over when plotting lines. + Default is the default list of colors from matplotlib. + legend_bbox : tuple, optional. + The position of the legend (x-axis, y-axis) relative to the figure. + marker : dict, optional. + Additional parameters to change the look of the lines. + Default is None, lines will be solid with points as white dots. + maxlines : int, optional. + Maximum number of labels in legend. + Default is 30. + Default is False. + names : str or list, optional. + Names for legend labels. + If string, names will be name+{line number} + If list, when a category is added, it will have the name "DefaultNameX". + Default is "Line". + n_names : int, optional. + Number of categories at launch when name is a string. + Default is 10. + pick_dist : int, optional. + Distance to legend category for picking mouse. + Default is 10. + wraplines : int, optional. + Maximum number of labels in legend before creating a new column. + Default is 15. + Default is (1, 0.5) + coords : dict, optional. + Coordinates of each point for each category (line, key of the dict). + Coordinates are expressed in (time, frequency) + or (pixel, pixel) if no extent given. + Default is {}. If given, initates figure with annotations. + + + Attributes + ---------- + current_line : int + Index of the ctegory (line) currently selected + figure : matplotlib object + Abbreviation for axis.figure. + figure_bsplines : list of matplotlib 2D lines + Secondary lines, in use if bspline mode is selected. + Draws curves instead of straight lines. + figure_lines : list of matplotlib 2D lines + Main lines, displayed on image as straight lines if bspline is False. + If bspline is true, linestyle is removed and they are displayed as dots. + key_press : matplotlib event + Information on the last key_press event. + legend : matplotlib object + legend of the plot + legend_labels : list of str + labels of the categories in legend. + linestyle : str + If bspline is True, saves linestyle. + mouse_press : matplotlib event + Information on the last mouse click in matplotlib canvas. + pick_press : matplotlib event + Information on the last mouse click event in matplotlib legend. + pressed : bool + Is used to check if a specific mouse button is pressed or not. + + Other attributes have self explenatory names. + + Methods + ------- + add_category(show): + Adds a category to the list in legend. + add_point(x,y): + Adds a point to the category in focus at the given coordinates. + Focus is updated to be on this new category. + clear_category(): + Clears all points from the category in focus. + double_lines(): + Creates a copy of the current set of lines from figure_lines. + It is used to interpolate curves between figure_lines points. + distance_mouse(points, mouse_x, mouse_y): + Gets the distance bewteen a mouse position and the currently + selected category (points). + get_focus(event): + Puts focus on a category in legend. Allows the user to interact with it. + get_key_event(event): + Activates functions when specific keyboard keys are pressed. + get_mouse_press(event, show=True): + Activates functions when specific mouse buttons are pressed. + move_point(event): + Moves a selected point of the currently selected category to a new + position (event). Updates figure accordingly. + rm_point(x,y): + Removes a point from the category in focus at the given coordinates. + set_legend(): + Adds legend to axis given. + switch_line(arrow): + Puts focus on previous/next category in legend based on arrow input. + update_lines(): + Updates data in lines. Called when coords is changed. + + References + ---------- + Heavily inspired from mpl_point_clicker : + mpl-point-clicker.readthedocs.io/ + """ + + DEFAULT_marker = { # Markerstyle for categories. + "marker":"o", + "mfc":"white", + "linestyle":"-", + } + + DEFAULT_colors = [ # Colors for categories. Will cycle through them. + '#1f77b4', # List can be appened or reduced. + '#ff7f0e', + '#2ca02c', + '#d62728', + '#9467bd', + '#8c564b', + '#e377c2', + '#7f7f7f', + '#bcbd22', + '#17becf' + ] + + DEFAULT_param = { + "add_point" : MouseButton.LEFT, + "rm_point" : MouseButton.RIGHT, + "move_point" : MouseButton.MIDDLE, + "add_category" : "A", # shift+a + "clear_category" : "R", # shift+r + "focus_up" : "up", + "focus_down" : "down", + } + + + def __init__( + self, + axis, + marker=None, + bspline=False, + colors=None, + names="Line", + maxlines=30, + wraplines=15, + legend_bbox=(1,0.5), + pick_dist=10, + n_names=10, + coords={}): + + # Variable assignement + self.axis = axis + self.legend_bbox = legend_bbox + self.pick_dist = pick_dist + self.param = self.DEFAULT_param.copy() + + if isinstance(marker, dict): + self.marker = marker + else: + self.marker = self.DEFAULT_marker.copy() + if bspline=="quadratic": + self.bspline = bspline + else: + self.bspline = False + self.wait = 2 + if isinstance(colors, list): + self.colors = colors + else: + self.colors = self.DEFAULT_colors.copy() + if isinstance(names, str): + self.names = names + self.legend_labels = [self.names + str(i+1) for i in range(n_names)] + elif isinstance(names, list): + self.legend_labels = names.copy() + self.names = "DefaultName" + else: + self.names = self.DEFAULT_name + self.legend_labels = [self.names + str(i+1) for i in range(n_names)] + if isinstance(maxlines, int): + self.maxlines = maxlines + else: + self.maxlines = self.DEFAULT_maxlines + if isinstance(wraplines, int): + self.wraplines = wraplines + else: + self.wraplines = self.DEFAULT_wraplines + + if self.bspline: + # replace linestyle + self.linestyle = self.marker['linestyle'] + self.marker['linestyle'] = "" + + if isinstance(coords, dict) and coords != {}: + self.coords = coords + self.legend_labels = list(self.coords.keys()).copy() + self.figure_lines = [mlines.Line2D( + np.array(self.coords[legend_label])[:,0], + np.array(self.coords[legend_label])[:,1], + label=legend_label, + color=self.colors[idx%len(self.colors)], **self.marker) + for idx, legend_label in enumerate(self.legend_labels)] + else: + self.coords = {} + self.figure_lines = [mlines.Line2D([], [], label=legend_label, + color=self.colors[idx%len(self.colors)], **self.marker) + for idx, legend_label in enumerate(self.legend_labels)] + + # Creation of lines + if self.bspline: + self.double_lines() + + # Creation of legend + self.figure = self.axis.figure + self.current_line = 0 + self.set_legend() + + # Linking actions in matplotlib canvas + self.mouse_press_event = self.figure.canvas.mpl_connect( + 'button_press_event', + self.get_mouse_press) + self.mouse_release_event = self.figure.canvas.mpl_connect( + 'button_release_event', + self.get_mouse_press) + self.motoin_event = self.figure.canvas.mpl_connect( + 'motion_notify_event', + self.move_point) + self.currently_pressed = False + + self.pick_event = self.figure.canvas.mpl_connect( + 'pick_event', + self.get_focus) + self.key_press = self.figure.canvas.mpl_connect( + 'key_press_event', + self.get_key_event) + + + + def add_category(self, show): + """ + A method to add a line (therefore a new category) to the plot. + Also change current focus to be on the newly created category. + + ... + + Parameters + ---------- + show : boolean + Shows change in legend. + + Returns + ------- + None : updates legend_labels, figure_lines, legend, figure + and current_line. + """ + # make new name + self.legend_labels += [self.names + str(len(self.legend_labels)+1)] + + # add to categories + self.figure_lines += [mlines.Line2D([], [], + label=self.legend_labels[len(self.legend_labels)-1], + color=self.colors[(len(self.legend_labels)-1)%len(self.colors)], + **self.marker)] + + if self.bspline: + self.figure_bsplines += [mlines.Line2D([], [], + label=self.legend_labels[len(self.legend_labels)-1], + color=self.colors[(len(self.legend_labels)-1)%len(self.colors)], + **{'linestyle':self.linestyle})] + + self.set_legend() + + # focus auto on new category + self.current_line = len(self.legend_labels)-1 + + for legend_line in self.legend.get_lines(): + legend_line.set_alpha(0.2) + self.legend.get_lines()[self.current_line].set_alpha(1) + if show: + self.figure.canvas.draw() + + def add_point(self, x, y): + """ + A method to add a point on plot at given coordinates. + + ... + + Parameters + ---------- + x : float + x-axis coordinate. + y : float + y-axis coordinate. + + Returns + ------- + None : updates coords, figure_lines, figure. + """ + if self.legend_labels[self.current_line] in list(self.coords.keys()): + self.coords[self.legend_labels[self.current_line]] += [[x, y]] + else: + self.coords[self.legend_labels[self.current_line]] = [[x, y]] + + self.update_lines() + self.figure.canvas.draw() + + def double_lines(self): + """ + Creates an other set of lines, additionnally to figure_lines. + It is necessary to display curves instead of straight lines. + + ... + + Returns + ------- + None : Updates linestyle and marker. Creates figure_bsplines. + """ + # create new lines + if self.coords == {}: + self.figure_bsplines = [mlines.Line2D([], [], + label=legend_label, + color=self.colors[idx%len(self.colors)], + **{'linestyle':self.linestyle}) + for idx, legend_label in enumerate(self.legend_labels)] + + else: + self.figure_bsplines = [] + for idx, legend_label in enumerate(self.legend_labels): + curves = to_curve( + np.array(self.coords[legend_label])[:,0], + np.array(self.coords[legend_label])[:,1], + kind="quadratic") + self.figure_bsplines += [mlines.Line2D(curves[0], curves[1], + label=legend_label, + color=self.colors[idx%len(self.colors)], + **{'linestyle':self.linestyle})] + + def clear_category(self): + """ + A method to remove a line (therefore a whole category) from the plot. + Removes the category that is in focus. + + ... + + Returns + ------- + None : updates legend_labels, figure_lines, legend, figure. + """ + + # we only need to remove data from coords + if self.legend_labels[self.current_line] in list(self.coords.keys()): + del self.coords[self.legend_labels[self.current_line]] + # set data to have no point in memory + self.figure_lines[self.current_line].set_data([], []) + if self.bspline: + self.figure_bsplines[self.current_line].set_data([], []) + # update plot + self.figure.canvas.draw() + + def distance_mouse(self, points, mouse_x, mouse_y): + """ + Gets the distance bewteen a mouse position + and the currently selected category (points). + + ... + Parameters + ---------- + points : numpy array + A list of coordinates with shape (2,n). + mouse_x : int or float + Coordinate of the mouse on x-axis. + mouse_y : int or float + Coordinate of the mouse on y-axis. + Returns + ------- + distances : numpy array + Distances of each point to mous coordinates. + """ + size_x, size_y = self.figure.get_size_inches() + max_x, max_y=self.axis.get_xbound()[1],self.axis.get_ybound()[1] + + # look for closest coordinates of the category currently selected. + distances = [np.linalg.norm([(point[0]/max_x)*size_x, + (point[1]/max_y)*size_y] - np.array([(mouse_x/max_x)*size_x, + (mouse_y/max_y)*size_y])) + for point in points] + + return distances + + def get_focus(self, event): + """ + A method to highlight a given marker in legend. + Points can be added/removed only to the category in focus. + + ... + + Parameters + ---------- + event : matplotlib object + Is used to acces event.artist attribute. + + Returns + ------- + None : updates figure, current_line and legend attributes. + """ + # set all legend lines to alpha = 0.2 + for legend_line in self.legend.get_lines(): + legend_line.set_alpha(0.2) + # set legend line in focus to alpha = 1 + selected_legend = event.artist + current_alpha = selected_legend._alpha + selected_legend.set_alpha(1.0 if (current_alpha==0.2) else 0.2) + self.figure.canvas.draw() + + # new focus + self.current_line = int(np.where( + np.array(self.legend.get_lines()) == event.artist)[0]) + + def get_mouse_press(self, event): + """ + A Method that retrieves mouse interactions with matplotlib plot. + + ... + + Parameters + ---------- + event : matplotlib object + Contains 3 attributes : button pressed, x and y at that time. + + Returns + ------- + None : is used to trigger methods add_points and rm_points(). + """ + if self.figure.canvas.widgetlock.available(self): + pressed, x, y = event.button, event.xdata, event.ydata + if ((pressed is self.param["add_point"]) and + (isinstance(x, float)) and + (isinstance(y, float)) and + event.name == 'button_press_event'): + self.add_point(x, y) + + elif ((pressed is self.param["rm_point"]) and + (isinstance(x, float)) and + (isinstance(y, float)) and + event.name == 'button_press_event'): + self.rm_point(x, y) + + elif ((pressed is self.param["move_point"]) and + (isinstance(x, float)) and + (isinstance(y, float)) and + event.name == 'button_press_event'): + distances = self.distance_mouse( + np.array(self.coords[self.legend_labels[self.current_line]]), + event.xdata, event.ydata) + self.index, dist = np.argmin(distances), np.min(distances) + if dist < 0.1: + self.currently_pressed = True + + elif ((pressed is self.param["move_point"]) and + (isinstance(x, float)) and + (isinstance(y, float)) and + event.name == 'button_release_event'): + self.currently_pressed = False + self.index = False + + def get_key_event(self, event, show=True): + """ + A method that retrieve key interactions with matplotlib plot. + + ... + + Parameters + ---------- + event : matplotlib object + Contains 3 attributes : button pressed, x and y at that time. + show : boolean + Parameter passed to add_category() + + Returns + ------- + None : is used to trigger other functions. + """ + key = event.key + if ((key == self.param["add_category"]) and + (len(self.legend_labels) < self.maxlines)): + self.add_category(show) + + elif ((key == self.param["clear_category"]) and + (len(self.legend_labels) > 1)): + self.clear_category() + + elif ((key == self.param["focus_up"]) and + (self.current_line != 0)): + self.switch_line(-1) + + elif ((key == self.param["focus_down"]) and + (self.current_line != len(self.legend_labels)-1)): + self.switch_line(1) + + def move_point(self, event): + """ + Moves a selected point of the currently selected category to a new + position (event). Updates figure accordingly. + + ... + + Parameters + ---------- + event : matplotlib object + Matplotlib event containig information on positions. + + Returns + ------- + None : it updates the coordinates of a point in a category + and re-draws the figure. + """ + 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() + + def set_legend(self): + """ + A method to create matplotlib.pyplot legend. + Legend contains empty lines and can be clicked. + + ... + + Returns + ------- + None : updates axis, creates legend. + """ + # Make some space to include legend + scale = (19-((len(self.legend_labels)//self.wraplines)+1))/19 + self.axis.set_position([0, 0, scale, 1]) + + # Add legend to plot + self.legend = self.axis.legend( + loc="center left", + bbox_to_anchor=self.legend_bbox, + ncol=(len(self.legend_labels)//self.wraplines)+1, + title="Selection of lines", + handles=self.figure_bsplines if self.bspline else self.figure_lines) + + # Add lines to plot + if self.bspline: + for bspline in self.figure_bsplines: + self.axis.add_line(bspline) + + for legend_l, line in zip(self.legend.get_lines(), self.figure_lines): + legend_l.set_picker(True) + legend_l.set_pickradius(self.pick_dist) + legend_l.set_alpha(0.2) + self.axis.add_line(line) + + # Focus on selected line + self.legend.get_lines()[self.current_line].set_alpha(1) + + def switch_line(self, arrow): + """ + Puts focus on previous/next category in legend based on arrow input. + + ... + + Parameters + ---------- + arrow : int + Can be 1 or -1. Change the index of the current category in focus. + + Returns + ------- + None : updates currentline, legend and figures attributes. + """ + # new focus + self.current_line += arrow + + # adapt alpha + for legend_line in self.legend.get_lines(): + legend_line.set_alpha(0.2) + self.legend.get_lines()[self.current_line].set_alpha(1) + self.figure.canvas.draw() + + def rm_point(self, x, y): + """ + A method to remove closest point in plot to given coordinates. + + ... + + Parameters + ---------- + x : float + x-axis coordinate. + y : float + y-axis coordinate. + + Returns + ------- + None : updates coords, figure_lines, figure. + """ + if self.legend_labels[self.current_line] in list(self.coords.keys()): + list_coords = np.array( + self.coords[self.legend_labels[self.current_line]]) + + if len(list_coords) > 1: + # figure is distorted on screen + # we want to look for coordinated based on what the user sees + # not the actual coordinates of the pixels in the image. + distances = self.distance_mouse(list_coords, x, y) + + # remove it + list_coords = np.delete(list_coords, np.argmin(distances), + axis=0) + self.coords[self.legend_labels[self.current_line]] = list_coords.tolist() + + self.update_lines() + + else: + del self.coords[self.legend_labels[self.current_line]] + self.figure_lines[self.current_line].set_data([], []) + if self.bspline: + self.figure_bsplines[self.current_line].set_data([],[]) + + self.figure.canvas.draw() + + def update_lines(self): + """ + Updates data in lines. Called when coords is changed. + + ... + + Returns + ------- + None : Updates figure_lines and figure_bsplines (if bspline is True). + """ + if self.bspline: + if len(self.coords[self.legend_labels[self.current_line]]) > self.wait: + curvex, curvey = to_curve( + np.array(self.coords[ + self.legend_labels[self.current_line]])[:,0], + np.array(self.coords[ + self.legend_labels[self.current_line]])[:,1], + kind=self.bspline) + self.figure_bsplines[self.current_line].set_data(curvex, curvey) + else: + self.figure_bsplines[self.current_line].set_data( + np.array(self.coords[ + self.legend_labels[self.current_line]])[:,0], + np.array(self.coords[ + self.legend_labels[self.current_line]])[:,1]) + + self.figure_lines[self.current_line].set_data( + np.array(self.coords[self.legend_labels[self.current_line]])[:,0], + np.array(self.coords[self.legend_labels[self.current_line]])[:,1]) + + +##### MAIN ##### +if __name__ == '__main__': + # dummy example + img = np.array([[0,1,0],[1,0,1],[0,1,0]]) + + fig, ax = plt.subplots(figsize=(16, 9)) + ax.imshow(img, cmap="viridis") + base = clicker(axis=ax, bspline="quadratic") + plt.show(block=True) \ No newline at end of file diff --git a/outputs/SCW1807_20200713_064545-contours.json b/outputs/SCW1807_20200713_064545-contours.json deleted file mode 100644 index 966d1d53f1a3ecf0ffc5131647d6cc173db70a65..0000000000000000000000000000000000000000 --- a/outputs/SCW1807_20200713_064545-contours.json +++ /dev/null @@ -1,322 +0,0 @@ -{ - "Line1": [ - [ - 9.289816200669629, - 15170.245041329019 - ], - [ - 9.312578889208721, - 14591.547174625195 - ], - [ - 9.322334327154048, - 13940.51207458339 - ], - [ - 9.335341577747815, - 13048.352863414993 - ], - [ - 9.371111516880676, - 11963.29436334532 - ], - [ - 9.403629643365095, - 10709.44898548703 - ], - [ - 9.442651395146397, - 9455.603607628742 - ], - [ - 9.488176772224584, - 9045.692618713532 - ], - [ - 9.556464837841864, - 9335.041552065444 - ], - [ - 9.611745652865375, - 10347.76281879714 - ], - [ - 9.660522842592004, - 11408.708907754153 - ], - [ - 9.712551844967074, - 12204.418474471913 - ], - [ - 9.77433628528747, - 12903.678396739037 - ], - [ - 9.82636528766254, - 13554.71349678084 - ], - [ - 9.858883414146959, - 13699.387963456797 - ], - [ - 9.891401540631378, - 13506.488674555521 - ], - [ - 9.936926917709565, - 13193.027330090948 - ], - [ - 9.99545954538152, - 12614.329463387123 - ], - [ - 10.060495798350356, - 11842.732307782022 - ], - [ - 10.102769362780101, - 10974.685507726284 - ], - [ - 10.148294739858288, - 10034.301474332568 - ], - [ - 10.184064678991149, - 9166.254674276828 - ], - [ - 10.242597306663104, - 8852.793329812257 - ], - [ - 10.310885372280383, - 8201.758229770452 - ], - [ - 10.343403498764802, - 7719.510007517265 - ], - [ - 10.369417999952336, - 7020.250085250142 - ], - [ - 10.385677063194546, - 6103.978462969084 - ], - [ - 10.405187939085199, - 5742.292296279193 - ] - ], - "Line2": [ - [ - 10.226338243420894, - 14302.198241273281 - ], - [ - 10.232841868717777, - 13916.399663470731 - ], - [ - 10.245849119311545, - 13289.476974541587 - ], - [ - 10.258856369905313, - 11673.945429993406 - ], - [ - 10.278367245795964, - 10830.011041050328 - ], - [ - 10.336899873467917, - 10709.44898548703 - ], - [ - 10.346655311413244, - 10275.425585459161 - ], - [ - 10.372669812600778, - 10395.987641022459 - ], - [ - 10.385677063194546, - 11505.158552204792 - ], - [ - 10.38892887584299, - 12734.89151895042 - ], - [ - 10.408439751733638, - 13868.174841245413 - ], - [ - 10.427950627624291, - 14953.233341315085 - ], - [ - 10.4442096908665, - 16399.97800807465 - ] - ], - "Line15": [ - [ - 10.821419958085759, - 15604.26844135689 - ], - [ - 10.870197147812387, - 15339.031919117635 - ], - [ - 10.935233400781225, - 15146.13263021636 - ], - [ - 10.97425515256253, - 15122.0202191037 - ], - [ - 10.97425515256253, - 14760.33405241381 - ], - [ - 11.000269653750063, - 14615.659585737854 - ], - [ - 11.032787780234482, - 14157.523774597325 - ], - [ - 11.04579503082825, - 13747.612785682115 - ], - [ - 11.06205409407046, - 13434.151441217544 - ] - ], - "Line24": [ - [ - 11.232771852237253, - 7991.754277468563 - ], - [ - 11.260604188345473, - 8213.039485766758 - ], - [ - 11.288436524453694, - 9565.337980922395 - ], - [ - 11.347193678459938, - 12319.109461966604 - ], - [ - 11.402858350676379, - 13794.344183954574 - ], - [ - 11.4306906867846, - 14138.565619085099 - ], - [ - 11.47089295005203, - 14064.803882985701 - ], - [ - 11.511095213319457, - 13523.884484923445 - ], - [ - 11.542020031217481, - 12589.569160997731 - ], - [ - 11.597684703433922, - 11999.475272202544 - ], - [ - 11.671904266389177, - 10991.398212177433 - ], - [ - 11.764678720083246, - 9909.559416052922 - ], - [ - 11.826528355879292, - 9565.337980922395 - ], - [ - 11.900747918834547, - 9171.942055058938 - ], - [ - 12.002799817898023, - 8926.069601394276 - ], - [ - 12.095574271592092, - 8606.435411630217 - ], - [ - 12.17597879812695, - 8335.97571259909 - ], - [ - 12.231643470343391, - 8040.928768201495 - ], - [ - 12.29658558792924, - 7573.771106238639 - ], - [ - 12.35225026014568, - 6541.106800847061 - ], - [ - 12.398637486992715, - 5803.489439853076 - ], - [ - 12.463579604578563, - 5901.83842131894 - ], - [ - 12.500689386056191, - 6516.519555480594 - ], - [ - 12.522336758584807, - 7327.8986525739765 - ], - [ - 12.547076612903226, - 7647.532842338038 - ], - [ - 12.60583376690947, - 7967.167032102097 - ], - [ - 12.652220993756503, - 8311.388467232624 - ], - [ - 12.714070629552548, - 8508.086430164352 - ] - ] -} \ No newline at end of file diff --git a/outputs/SCW1807_20200713_064554-contours.json b/outputs/SCW1807_20200713_064554-contours.json new file mode 100644 index 0000000000000000000000000000000000000000..9ee2ef6e84849a8dc6bd4a64f92c3402a9d8f412 --- /dev/null +++ b/outputs/SCW1807_20200713_064554-contours.json @@ -0,0 +1,944 @@ +{ + "Line1": [ + [ + 0.2556607438674337, + 16569.1151082533 + ], + [ + 0.2804261968710533, + 16278.071710573218 + ], + [ + 0.28339805123148765, + 15210.912585746242 + ], + [ + 0.3081635042351073, + 14677.333023332758 + ], + [ + 0.34877884716104346, + 12712.790088992195 + ], + [ + 0.39335666256755875, + 11160.558634698416 + ], + [ + 0.43100015113306056, + 9656.83441335132 + ], + [ + 0.4825122933805894, + 9074.74761799115 + ], + [ + 0.5478930893101452, + 9341.537399197896 + ], + [ + 0.6142645033598458, + 10287.428441658165 + ], + [ + 0.682617153649836, + 11936.674361845304 + ], + [ + 0.7252137328160617, + 12373.239458365431 + ], + [ + 0.7846508200247487, + 13003.833486672276 + ], + [ + 0.825266162950685, + 13610.173898505785 + ], + [ + 0.856965942795318, + 13610.173898505785 + ], + [ + 0.8995625219615438, + 13464.652199665743 + ], + [ + 0.926309211205453, + 13294.876884352361 + ], + [ + 0.9570183729299413, + 12931.072637252259 + ], + [ + 0.9649433178910996, + 12882.565404305577 + ], + [ + 0.9926806252551535, + 12688.536472518856 + ], + [ + 1.0115023695379044, + 12470.253924258792 + ], + [ + 1.0303241138206554, + 12276.22499247207 + ], + [ + 1.0521177124638406, + 11960.927978318647 + ], + [ + 1.1541513788387534, + 9923.624194558062 + ], + [ + 1.1729731231215044, + 9632.580796877977 + ], + [ + 1.1769355956020835, + 9171.762083884514 + ], + [ + 1.2234946472488883, + 9001.986768571129 + ], + [ + 1.267081844535259, + 8565.421672051005 + ], + [ + 1.3205752230230774, + 8104.60295905754 + ], + [ + 1.3483125303871313, + 7571.023396644054 + ], + [ + 1.3631718021893031, + 6988.936601283886 + ], + [ + 1.3810029283519092, + 6188.567257663657 + ], + [ + 1.3988340545145153, + 5752.002161143532 + ] + ], + "Line2": [ + [ + 1.2314195922100466, + 14095.246227972591 + ], + [ + 1.2413257734114946, + 13173.608801985662 + ], + [ + 1.262128753934535, + 11863.913512425286 + ], + [ + 1.2789692619769963, + 10893.768853491674 + ], + [ + 1.3116596599417742, + 10772.500771124971 + ], + [ + 1.336425112945394, + 10772.500771124971 + ], + [ + 1.3483125303871313, + 10311.682058131508 + ], + [ + 1.3601999478288687, + 10287.428441658165 + ], + [ + 1.3740686015108956, + 11160.558634698416 + ], + [ + 1.390909109553357, + 12421.74669131211 + ], + [ + 1.4027965269950944, + 13513.159432612425 + ], + [ + 1.4166651806771213, + 14192.260693865952 + ], + [ + 1.432515070599438, + 15162.405352799564 + ], + [ + 1.446383724281465, + 16496.35425883328 + ], + [ + 1.44676171875, + 17700.91851851852 + ], + [ + 1.44676171875, + 19269.57037037037 + ], + [ + 1.4532515624999998, + 19254.340740740743 + ], + [ + 1.4662312499999999, + 19117.274074074077 + ], + [ + 1.4857007812499998, + 18751.762962962963 + ] + ], + "Line3": [ + [ + 0.37038441199132144, + 23845.111348935927 + ], + [ + 0.4050533169552495, + 21890.595023197493 + ], + [ + 0.4152003135300576, + 20611.713970553825 + ], + [ + 0.43211197448807126, + 19139.794268454512 + ], + [ + 0.4811557912663108, + 18174.60102117627 + ], + [ + 0.5589494316731736, + 18681.327475997346 + ], + [ + 0.5961550857808036, + 19694.780385639497 + ], + [ + 0.6316695737926322, + 21287.34924364859 + ], + [ + 0.6680296448523616, + 23072.956751113335 + ], + [ + 0.6891692210498787, + 23965.760504845708 + ], + [ + 0.7111543802952963, + 24496.61679084874 + ], + [ + 0.7365218717323168, + 24930.953752123947 + ], + [ + 0.7593526140256353, + 25510.06970049089 + ], + [ + 0.8320727561450939, + 27295.677207955632 + ], + [ + 0.8811165729233335, + 27102.638558499988 + ], + [ + 0.8980282338813471, + 26885.47007786238 + ], + [ + 0.9267780575099703, + 26692.431428406733 + ], + [ + 0.9614469624738983, + 25847.88733703827 + ], + [ + 1.0054172809647337, + 25075.73273921568 + ], + [ + 1.0510787655513707, + 23869.241180117882 + ], + [ + 1.1035049145212128, + 22011.24417910727 + ], + [ + 1.1339459042456375, + 20828.882451191428 + ], + [ + 1.1474752330120483, + 19936.07869745906 + ], + [ + 1.2108939616045995, + 18053.951865266492 + ], + [ + 1.2464084496164283, + 17450.70608571759 + ] + ], + "Line4": [ + [ + 1.220195375131507, + 26813.08058431651 + ], + [ + 1.1821441379759765, + 27898.92298750453 + ], + [ + 1.177916222736473, + 28550.42842941734 + ], + [ + 1.079828589179994, + 34462.23706899656 + ], + [ + 1.024020108018549, + 37044.129005465846 + ], + [ + 0.9606013794259978, + 38853.866344112546 + ], + [ + 0.9242413083662684, + 40036.22807202839 + ], + [ + 0.9115575626477581, + 40374.04570857577 + ], + [ + 0.8946459016897446, + 40277.52638384795 + ], + [ + 0.8608225797737172, + 40904.9019945788 + ], + [ + 0.8160066782349811, + 40398.17553975773 + ], + [ + 0.7771098580315496, + 38853.866344112546 + ], + [ + 0.7500512004987279, + 37840.41343447039 + ], + [ + 0.720455793822204, + 37019.99917428389 + ], + [ + 0.6917059701935808, + 35861.76727755 + ], + [ + 0.6739487261876664, + 34703.535380816116 + ], + [ + 0.6638017296128582, + 33714.212302355925 + ], + [ + 0.6426621534153412, + 32242.29260025661 + ], + [ + 0.6265960755052282, + 31325.359015342277 + ], + [ + 0.5995374179724065, + 29829.30948206101 + ], + [ + 0.5682508452000812, + 28453.90910468952 + ], + [ + 0.5361186893798553, + 27585.235182139102 + ], + [ + 0.49553070308062247, + 27199.157883227806 + ], + [ + 0.45325155068558837, + 27826.53349395866 + ], + [ + 0.41520031353005765, + 30722.113235793382 + ] + ], + "Line5": [ + [ + 1.4205985574839692, + 28502.16876705343 + ], + [ + 1.4036868965259557, + 27609.36501332106 + ], + [ + 1.3960766490948493, + 26716.56125958869 + ], + [ + 1.3952310660469487, + 25775.497843492405 + ], + [ + 1.386775235567942, + 24906.82392094199 + ], + [ + 1.3800105711847364, + 23603.813037116368 + ], + [ + 1.3757826559452329, + 22590.360127474218 + ], + [ + 1.3707091576578287, + 21359.73873719446 + ], + [ + 1.3664812424183255, + 20828.882451191428 + ], + [ + 1.348723998412411, + 20273.89633400644 + ], + [ + 1.3444960831729078, + 20949.53160710121 + ], + [ + 1.3343490865980996, + 21190.82991892077 + ], + [ + 1.3089815951610793, + 21407.99839955837 + ], + [ + 1.2785406054366546, + 21673.426542559886 + ], + [ + 1.2658568597181443, + 22807.528608111817 + ], + [ + 1.256555446191237, + 24737.9151026683 + ], + [ + 1.24809961571223, + 26089.185648857834 + ] + ], + "Line6": [ + [ + 2.0661243541857277, + 13285.951796092879 + ], + [ + 2.039050804089352, + 13986.16628625902 + ], + [ + 2.028385466172598, + 14382.954497353168 + ], + [ + 1.9766995978067898, + 14756.402225441776 + ], + [ + 1.960291385627168, + 15153.190436535922 + ], + [ + 1.8150787078375163, + 15573.319130635608 + ] + ], + "Line7": [ + [ + 2.234323175389529, + 7952.965023630615 + ], + [ + 2.2500756801682016, + 8003.241178682962 + ], + [ + 2.2694633783573366, + 8832.797737046665 + ], + [ + 2.2912745388201143, + 9913.735070672095 + ], + [ + 2.3385320531561318, + 11824.228962661231 + ], + [ + 2.3979068788603586, + 13759.860932176538 + ], + [ + 2.426988426144062, + 14212.346327647649 + ], + [ + 2.480304596164184, + 13910.689397333574 + ], + [ + 2.517868261405634, + 13332.513614231599 + ], + [ + 2.544526346415695, + 12628.647443498761 + ], + [ + 2.5869369362044288, + 12301.852435658513 + ], + [ + 2.6257123325826996, + 11723.676652556538 + ], + [ + 2.6669111912346124, + 10994.672404297527 + ], + [ + 2.7262860169388397, + 10215.39200098617 + ], + [ + 2.7844491115062455, + 9762.90660551506 + ], + [ + 2.8280714324318, + 9536.663907779503 + ], + [ + 2.9383389658825076, + 9184.730822413085 + ], + [ + 3.1188869052688313, + 8606.55503931111 + ], + [ + 3.2024963537094777, + 8254.62195394469 + ], + [ + 3.2255192453090755, + 7927.826946104442 + ], + [ + 3.3127638871601848, + 7450.20347310716 + ], + [ + 3.3345750476229625, + 6846.889612479012 + ], + [ + 3.370926981727591, + 6092.747286693827 + ], + [ + 3.398796797874473, + 5841.366511432098 + ], + [ + 3.403643722421757, + 6168.161519272346 + ], + [ + 3.41939622720043, + 6243.5757518508635 + ], + [ + 3.421819689474072, + 5841.366511432098 + ], + [ + 3.4618068169891636, + 6092.747286693827 + ], + [ + 3.5042174067778973, + 6520.094604638764 + ], + [ + 3.5236051049670323, + 7450.20347310716 + ], + [ + 3.5587453079348403, + 7776.998480947406 + ], + [ + 3.602367628860395, + 8053.517333735306 + ], + [ + 3.627813982733635, + 8204.345798892344 + ], + [ + 3.639931294101845, + 8355.174264049381 + ], + [ + 3.6738597659328316, + 8405.450419101726 + ], + [ + 3.7077882377638183, + 8480.864651680246 + ], + [ + 3.7368697850475217, + 8455.726574154072 + ], + [ + 3.752622289826194, + 9410.97352014864 + ], + [ + 3.7635278700575827, + 8480.864651680246 + ] + ], + "Line8": [ + [ + 2.236746637663171, + 15745.76905674419 + ], + [ + 2.257346066989127, + 16072.564064584436 + ], + [ + 2.2743103029046208, + 17932.781801521225 + ], + [ + 2.294909732230577, + 19843.27569351036 + ], + [ + 2.3191443549669963, + 21904.598050656532 + ], + [ + 2.3458024399770574, + 24368.129648221468 + ], + [ + 2.385789567492149, + 27208.732408678996 + ], + [ + 2.4112359213653893, + 28214.25550972591 + ], + [ + 2.460916897975049, + 28214.25550972591 + ], + [ + 2.4996922943533195, + 27284.146641257514 + ], + [ + 2.5360442284579485, + 25675.309679582453 + ], + [ + 2.6257123325826996, + 23387.74462470073 + ], + [ + 2.686298889423748, + 21552.66496529011 + ], + [ + 2.742038521717512, + 20119.79454629826 + ], + [ + 2.8208010456108745, + 19038.857212672832 + ], + [ + 2.892293182683311, + 18536.095662149375 + ], + [ + 2.979537824534421, + 17983.05795657357 + ], + [ + 3.065570735248709, + 17480.296406050114 + ], + [ + 3.162509226194386, + 16826.70639036962 + ], + [ + 3.2109784716672243, + 16248.530607267647 + ], + [ + 3.2303661698563597, + 15796.045211796536 + ], + [ + 3.3127638871601848, + 14840.798265801968 + ], + [ + 3.3491158212648138, + 13106.270916496043 + ], + [ + 3.3927381421903684, + 11849.367040187402 + ], + [ + 3.404855453558578, + 12301.852435658513 + ], + [ + 3.41939622720043, + 12301.852435658513 + ], + [ + 3.4230314206108927, + 11572.8481873995 + ], + [ + 3.458171623578701, + 11899.643195239749 + ], + [ + 3.498158751093792, + 12804.61398618197 + ], + [ + 3.5163347181461067, + 13910.689397333574 + ], + [ + 3.5332989540616, + 15142.455196116043 + ], + [ + 3.5732860815766916, + 15745.76905674419 + ], + [ + 3.625390520459993, + 16173.116374689129 + ], + [ + 3.6241787893231723, + 16525.049460055547 + ], + [ + 3.6993061198060717, + 17002.672933052832 + ], + [ + 3.7368697850475217, + 16851.844467895797 + ] + ], + "Line9": [ + [ + 2.3116012235318193, + 31764.84090906388 + ], + [ + 2.3559597629309463, + 37557.48611007064 + ], + [ + 2.3959906399496704, + 41342.01430806173 + ], + [ + 2.4154651206614823, + 42346.07280956957 + ], + [ + 2.4630694068459116, + 42075.74936685592 + ], + [ + 2.5031002838646357, + 40762.74978796105 + ], + [ + 2.5160832710058436, + 39449.75020906619 + ], + [ + 2.5864077846873865, + 36823.75105127645 + ], + [ + 2.5928992782579905, + 36051.39835780888 + ], + [ + 2.6123737589698024, + 35549.36910705496 + ], + [ + 2.651322720393426, + 33541.25210403929 + ], + [ + 2.6913535974121503, + 31842.076178410636 + ], + [ + 2.774661098234901, + 29370.547559314415 + ], + [ + 2.897999476076376, + 27632.753999012384 + ], + [ + 3.087334705218991, + 26010.813342730493 + ], + [ + 3.180379446397648, + 24929.5195718759 + ], + [ + 3.2258199013918754, + 23848.2258010213 + ], + [ + 3.316700811380331, + 21994.579336699135 + ], + [ + 3.3556497728039547, + 19291.34490956265 + ], + [ + 3.395680649822679, + 17630.786618607373 + ], + [ + 3.401090227798182, + 18248.66877338143 + ], + [ + 3.417318961724692, + 18325.904042728187 + ], + [ + 3.423810455295296, + 17514.93371458724 + ], + [ + 3.4692509102895235, + 17978.34533066778 + ], + [ + 3.503872209332745, + 19716.13889096981 + ], + [ + 3.535247761590664, + 22728.314395493326 + ], + [ + 3.585015878965294, + 23693.755262327788 + ], + [ + 3.6218010091987165, + 24118.549243734953 + ], + [ + 3.6250467559840187, + 24659.19612916225 + ], + [ + 3.6845521137145547, + 25470.166457303196 + ], + [ + 3.739729809064688, + 25199.843014589547 + ] + ] +} \ No newline at end of file diff --git a/outputs/SCW6070_20220717_174215-contours.json b/outputs/SCW6070_20220717_174215-contours.json index 66bfb03d3ecd6f6a6301c45033af58566087d251..6306a0489c0024b03c20456d5e52ce4bd68fc650 100644 --- a/outputs/SCW6070_20220717_174215-contours.json +++ b/outputs/SCW6070_20220717_174215-contours.json @@ -1,130 +1,762 @@ { "Line1": [ [ - 6.26688839799352, - 14199.107027536345 + 7.9414817812548755, + 8275.419903568396 ], [ - 6.277897758643097, - 12502.845591823758 + 7.857669492651897, + 9593.06625712892 ], [ - 6.294411799617462, - 10672.668779607546 + 7.794810276199663, + 11102.869370583685 ], [ - 6.30542116026704, - 8887.130426225876 + 7.741263536258871, + 13134.240832322826 ], [ - 6.327439881566193, - 7592.615120024164 + 7.651630949836242, + 17526.395344191234 ], [ - 6.397165832346847, - 7280.145908182372 + 7.617873222482265, + 19146.002320442713 ], [ - 6.455882422477924, - 7994.361249535041 + 7.568982720797194, + 19832.276462922153 ], [ - 6.487075610985059, - 9913.814979420336 + 7.534060933879286, + 19283.2571489386 ], [ - 6.527443266700174, - 11877.907168140173 + 7.511943802164612, + 18844.04169775176 ], [ - 6.549461987999328, - 12458.207132989217 + 7.480514193938495, + 18596.983006459162 ], [ - 6.580655176506463, - 12592.122509492841 + 7.452576764404169, + 17993.061761077253 ], [ - 6.630197299429559, - 13529.530145018218 + 7.436279930509145, + 17334.238584296992 ], [ - 6.690748783002232, - 14333.02240403997 + 7.416490917922331, + 16895.023133110153 ], [ - 6.729281545275751, - 14154.468568701803 + 7.404850322283028, + 16757.768304614263 ], [ - 6.747630479691713, - 12726.037885996468 + 7.397865964899447, + 15934.239333638936 ], [ - 6.7586398403412895, - 11119.053367952964 + 7.376912892748702, + 15577.376779549628 ], [ - 6.787998135406828, - 10137.007273593044 + 7.345483284522585, + 14534.240082980883 ], [ - 6.810016856705982, - 10092.368814758504 + 7.32336615280791, + 13573.456283509666 ], [ - 6.837540258329924, - 9690.622685247627 + 7.294264663709654, + 11926.398341559012 ], [ - 6.86322876651227, - 9244.23809690221 + 7.269819412867118, + 10938.163576388619 ], [ - 6.887082381253021, - 8530.022755549542 + 7.234897625949211, + 10196.987502510827 ], [ - 6.921945356643348, - 8083.638167204124 + 7.198811779467373, + 9977.379776917405 ], [ - 6.951303651708886, - 7815.807414196874 + 7.154577516038024, + 10498.94812520178 + ], + [ + 7.033515321389277, + 16455.80768192331 + ], + [ + 6.974148283628835, + 18816.59073205258 + ], + [ + 6.941554615838787, + 19338.159080336955 + ], + [ + 6.889171935461926, + 18898.943629150115 + ], + [ + 6.820492421190041, + 17498.94437849206 + ], + [ + 6.779750336452482, + 16538.160579020845 + ], + [ + 6.743664489970644, + 15467.572916752917 + ], + [ + 6.709906762616667, + 13930.318837598974 + ], + [ + 6.683133392646271, + 12063.653170054899 + ], + [ + 6.642391307908712, + 10498.94812520178 + ], + [ + 6.600485163607223, + 9785.223017023163 + ], + [ + 6.551594661922152, + 10169.536536811647 + ], + [ + 6.49688386241743, + 13463.652420712955 + ], + [ + 6.484079207214197, + 14067.573666094864 + ], + [ + 6.470110492447034, + 15330.31808825703 + ], + [ + 6.428204348145545, + 16730.317338915087 + ], + [ + 6.407251275994801, + 18322.473349467386 + ], + [ + 6.374657608204754, + 20244.040948409813 ] ], "Line2": [ [ - 7.769666126660772, - 23617.821841624656 + 6.262907890067449, + 14122.475597493216 + ], + [ + 6.2873531409099845, + 11432.280958973817 + ], + [ + 6.303649974805008, + 9922.47784551905 + ], + [ + 6.305978093932869, + 8330.321834966751 + ], + [ + 6.339735821286846, + 7589.145761088957 + ], + [ + 6.393282561227638, + 7451.890932593069 + ], + [ + 6.44682930116843, + 7918.557349479088 + ], + [ + 6.478258909394547, + 9565.615291429742 + ], + [ + 6.4945557432895695, + 10581.30102229931 + ], + [ + 6.513180696312454, + 11075.418404884509 + ], + [ + 6.531805649335338, + 12255.809929949144 + ], + [ + 6.550430602358222, + 12365.613792745855 + ], + [ + 6.564399317125385, + 12695.025381135983 + ], + [ + 6.617946057066177, + 13271.495660818713 + ], + [ + 6.67847715439055, + 14232.279460289927 + ], + [ + 6.7227114178199, + 14451.887185883348 + ], + [ + 6.739008251714923, + 13765.613043403908 + ], + [ + 6.755305085609947, + 11981.300272957367 + ], + [ + 6.775094098196761, + 10498.94812520178 + ], + [ + 6.810015885114669, + 10251.889433909182 + ], + [ + 6.849593910288297, + 9538.164325730564 + ], + [ + 6.891500054589787, + 8467.576663462638 + ], + [ + 6.95552333060595, + 7863.6554180807325 + ], + [ + 7.023038785313905, + 7616.596726788135 + ], + [ + 7.082405823074348, + 8193.067006470865 + ], + [ + 7.119655729120116, + 10416.595228104246 + ], + [ + 7.148757218218372, + 12310.7118613475 + ], + [ + 7.172038409496977, + 12585.221518339273 + ], + [ + 7.199975839031303, + 12996.986003826936 + ] + ], + "Line3": [ + [ + 11.057985748810033, + 10928.566188741182 + ], + [ + 11.115140280590705, + 11170.211970805092 + ], + [ + 11.185797517175939, + 11562.922806526061 + ], + [ + 11.239917953709309, + 11520.467581042714 + ], + [ + 11.364695626827912, + 12040.544093213728 + ], + [ + 11.468426463516872, + 12348.344477968 + ], + [ + 11.530815300076174, + 12560.620605384742 + ], + [ + 11.578170682042874, + 12656.144862722274 + ], + [ + 11.71422344610593, + 13165.60756852245 + ], + [ + 11.751055409857806, + 13154.993762151615 + ], + [ + 11.7758606099356, + 12550.006799013903 + ], + [ + 11.812692573687478, + 11000.391068871699 + ] + ], + "Line4": [ + [ + 11.062810546875, + 21696.000000000004 + ], + [ + 11.190322265625, + 22908.44444444445 + ], + [ + 11.237185546875, + 23101.333333333336 + ], + [ + 11.3129296875, + 23680.000000000004 + ], + [ + 11.384314453125, + 24065.77777777778 + ], + [ + 11.42518359375, + 24332.14814814815 + ], + [ + 11.45025, + 24589.333333333336 + ], + [ + 11.502017578125, + 24864.88888888889 + ], + [ + 11.545611328125, + 25140.444444444445 + ], + [ + 11.5723125, + 25342.518518518522 + ], + [ + 11.600103515625001, + 25498.666666666668 + ], + [ + 11.705818359375002, + 26196.740740740745 + ], + [ + 11.75431640625, + 26242.666666666668 + ] + ], + "Line5": [ + [ + 11.545265625, + 17967.64444444445 + ], + [ + 11.587224609375001, + 19055.170370370375 + ], + [ + 11.619708984375, + 19388.44444444445 + ], + [ + 11.65625390625, + 19370.90370370371 + ], + [ + 11.67791015625, + 19090.251851851855 + ], + [ + 11.72934375, + 17230.933333333338 + ], + [ + 11.749646484375, + 16301.274074074077 + ], + [ + 11.757767578125002, + 16055.703703703708 + ], + [ + 11.755060546875, + 15739.970370370374 + ], + [ + 11.776716796875, + 14827.851851851856 + ], + [ + 11.820029296875001, + 12687.881481481485 + ], + [ + 11.868755859375, + 10705.777777777781 + ], + [ + 11.88905859375, + 10337.422222222225 + ], + [ + 11.925603515625001, + 10354.962962962965 + ], + [ + 11.95944140625, + 10951.34814814815 + ], + [ + 11.978390625000001, + 11881.007407407411 + ], + [ + 11.989218750000001, + 12950.992592592596 + ], + [ + 12.039298828125, + 14582.281481481485 + ], + [ + 12.109681640625, + 16757.333333333336 + ], + [ + 12.150287109375, + 17897.481481481485 + ], + [ + 12.223376953125001, + 20142.6962962963 + ], + [ + 12.250447265625, + 20475.970370370374 + ], + [ + 12.265335937500002, + 20160.23703703704 + ], + [ + 12.327597656250001, + 17301.096296296302 + ], + [ + 12.36008203125, + 15880.296296296301 + ], + [ + 12.380384765625001, + 14705.066666666671 + ], + [ + 12.403394531250001, + 12986.074074074077 + ], + [ + 12.420990234375001, + 11793.303703703707 + ], + [ + 12.445353515625001, + 10618.074074074077 + ], + [ + 12.477837890625, + 9916.444444444447 + ], + [ + 12.51167578125, + 10144.474074074076 + ], + [ + 12.561755859375001, + 11758.222222222226 + ], + [ + 12.572583984375001, + 12758.044444444447 + ], + [ + 12.591533203125001, + 13915.733333333337 + ], + [ + 12.63619921875, + 15599.644444444448 + ], + [ + 12.6984609375, + 17529.12592592593 + ], + [ + 12.755308593750001, + 19020.088888888895 + ], + [ + 12.785085937500002, + 20002.370370370376 + ], + [ + 12.818923828125001, + 20125.15555555556 + ], + [ + 12.840580078125, + 19862.04444444445 + ], + [ + 12.864943359375001, + 18897.303703703707 + ], + [ + 12.887953125000001, + 17932.56296296297 + ], + [ + 12.931265625000002, + 16301.274074074077 + ], + [ + 12.96104296875, + 14985.718518518523 + ], + [ + 12.988113281250001, + 13915.733333333337 + ], + [ + 13.012476562500002, + 12880.829629629632 ], [ - 7.7898499545183295, - 22189.39115891932 + 13.032779296875, + 12109.03703703704 ], [ - 7.833887397116637, - 20269.937429034024 + 13.089626953125, + 10775.940740740743 ], [ - 7.890769093806117, - 17904.099110803312 + 13.115343750000001, + 10214.637037037039 + ], + [ + 13.149181640625, + 9565.629629629631 + ], + [ + 13.199261718750002, + 9021.866666666669 + ] + ], + "Line6": [ + [ + 13.4533125, + 13603.022222222224 + ], + [ + 13.51125, + 13510.755555555557 + ], + [ + 13.523625, + 13018.666666666668 + ], + [ + 13.5601875, + 12588.088888888891 + ], + [ + 13.584375, + 12270.281481481483 + ], + [ + 13.591125, + 11091.31851851852 + ], + [ + 13.611374999999999, + 10527.466666666667 + ], + [ + 13.6186875, + 10117.392592592594 + ], + [ + 13.628812499999999, + 9553.540740740742 + ], + [ + 13.648499999999999, + 8251.555555555557 + ], + [ + 13.6580625, + 7728.711111111112 + ], + [ + 13.689, + 7636.444444444445 + ], + [ + 13.727812499999999, + 7851.733333333334 + ], + [ + 13.7548125, + 8138.785185185186 + ], + [ + 13.7570625, + 8507.851851851852 + ], + [ + 13.809937499999998, + 8999.940740740742 + ], + [ + 13.841999999999999, + 10332.681481481482 + ], + [ + 13.863375, + 10609.481481481482 + ], + [ + 13.86675, + 11398.874074074076 + ], + [ + 13.897687499999998, + 11860.207407407408 + ], + [ + 13.9325625, + 12977.65925925926 + ], + [ + 13.946062499999998, + 13182.696296296297 + ], + [ + 13.95225, + 13736.296296296297 + ], + [ + 13.987124999999999, + 14259.140740740742 + ], + [ + 13.995562499999998, + 14689.718518518519 + ] + ], + "Line7": [ + [ + 11.817544921875, + 16247.822222222221 + ], + [ + 11.832679687499999, + 16274.192592592592 + ], + [ + 11.8769765625, + 16849.066666666666 + ], + [ + 11.892111328124999, + 16875.437037037038 + ], + [ + 11.97369140625, + 17750.933333333334 + ], + [ + 12.0010078125, + 17861.68888888889 + ], + [ + 12.023894531249999, + 18088.474074074074 + ], + [ + 12.0718828125, + 18389.096296296295 + ], + [ + 12.08591015625, + 18658.074074074073 + ], + [ + 12.108427734374999, + 19106.370370370372 + ] + ], + "Line8": [ + [ + 13.028466796875, + 24251.259259259263 ], [ - 7.951320577378791, - 16029.283839752556 + 13.060107421875, + 22698.962962962964 ], [ - 7.991688233093906, - 14645.491615881761 + 13.099658203125, + 20966.4 ], [ - 8.015541847834657, - 13797.360898025468 + 13.146240234375, + 19153.71851851852 ], [ - 8.033890782250618, - 13931.276274529095 + 13.194580078125, + 18122.192592592593 ] ] } \ No newline at end of file diff --git a/parameters.py b/parameters.py index 2d35979ee6e7962a07c06cc50d0b91b9c6803aa4..e259304d3c559c5352014fb6c6a810caa0ab2670 100644 --- a/parameters.py +++ b/parameters.py @@ -3,10 +3,11 @@ from screeninfo import get_monitors ##### FIXED PARAMETERS ###### -width = get_monitors()[0].width # width of tk window -height = get_monitors()[0].height # height of tk window -left_panel_width = 26 # Size of window's left panel -nfft = 2048 # default fft value (can be changed by user) -hop_length = 512 # default hop_length value (can be changed by user) -clipping = -80 # default clipping value in dB (can be changed by user) -cmap = "viridis" # default cmap for spectrogram visualisation +_default_width = get_monitors()[0].width # width of tk window +_default_height = get_monitors()[0].height # height of tk window +_default_left_panel_width = 26 # Size of window's left panel +_default_nfft = 2048 # default fft value (can be changed by user) +_default_hop_length = 512 # default hop_length value (can be changed by user) +_default_clipping = -80 # default clipping value in dB (can be changed by user) +_default_cmap = "viridis" # default cmap for spectrogram visualisation +_default_bounds = [0.05, 0.05, 0.94, 0.9] # default boundaries for canvas in tkinter \ No newline at end of file diff --git a/post_annotation.py b/post_annotation.py new file mode 100644 index 0000000000000000000000000000000000000000..e0dc1305a7dfe2c74b1725225071d4fd4f582426 --- /dev/null +++ b/post_annotation.py @@ -0,0 +1,190 @@ +##### IMPORTATIONS ##### +import os +import json +import numpy as np + +from matplotlib.patches import Rectangle +import matplotlib.pyplot as plt + +from line_clicker.line_clicker import to_curve + +# Import external functions +from functions import load_waveform, wave_to_spectrogram, save_dict + +##### CLASS ###### +class Results(object): + """ + SR : int, optional. + Sampling rate of the waveform. + Default is 96 kHz. + n_fft : int, optional. + Desired size for fft window. Should be in [1, N-1]. + Default is 4098. + w_size : int, optional. + Desired size for hop length between two fft. Should be in [1, N-1]. + Default is 156. + clip : int, optional. + Clipping value for dB. If pixel value < clip, pixel is turned into NaN. + Default is -80. + cmap : str, optional. + Color map for matplotlib.pyplot plot. + Default is viridis. + """ + + colors = [ # Colors for categories. Will cycle through them. + '#1f77b4', # List can be appened or reduced. + '#ff7f0e', + '#2ca02c', + '#d62728', + '#9467bd', + '#8c564b', + '#e377c2', + '#7f7f7f', + '#bcbd22', + '#17becf' + ] + + def __init__( + self, + wavefile_name, + jsonfile_name, + SR = 96_000, + NFFT = 4098, + HOP_LENGTH = 156, + CLIPPING = -80, + cmap = 'viridis'): + + self.wavefile_name = wavefile_name + self.jsonfile_name = jsonfile_name + self.SR = SR + self.NFFT = NFFT + self.HOP_LENGTH = HOP_LENGTH + self.CLIPPING = CLIPPING + self.cmap = cmap + + self.coords = self.load_contours_file() + self.waveform = load_waveform(self.wavefile_name, self.SR) + self.spectrogram, self.duration = wave_to_spectrogram( + self.waveform, + self.SR, + self.NFFT, + self.HOP_LENGTH, + self.CLIPPING) + self.pcen, _ = wave_to_spectrogram( + self.waveform, + self.SR, + self.NFFT, + self.HOP_LENGTH, + self.CLIPPING, + as_pcen=True) + + def load_contours_file(self): + """ + A function to import the contours saved from the interface. + + ... + + Returns + ------- + contours : dict + Data contained in json file. + """ + with open(self.jsonfile_name, "r") as f: + contours = json.load(f) + return contours + + def display_image(self, img="spec"): + fig, ax = plt.subplots(figsize=(16,9)) + ax.imshow( + self.pcen[::-1] if img=="pcen" else self.spectrogram[::-1], + cmap=self.cmap, + interpolation='nearest', aspect='auto', + extent=(0, self.duration, 0, self.SR/2)) + + ax.set_xlabel("Time (in sec)") + ax.set_ylabel("Frequencies (in Hz)") + ax.set_title(f"Spectrogram of {os.path.basename(self.wavefile_name)}") + return fig, ax + + def display_contours(self, mode="curves", img="spec"): + """ + A function to show the results of annotations, after using the interface. + + ... + + Parameters + ---------- + mode : str + Wether to plot curves or straight lines between each point. + "curves" or any other string for straight lines. Default is "curves". + + Returns + ------- + None. Plots the contours fetched from a jsonfile onto a specgram. + """ + fig, ax = self.display_image(img) + + 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") + ax.plot(cx, cy, color=self.colors[idx%len(self.colors)]) + + else: + ax.plot( + np.array(self.coords[key])[:,0], + np.array(self.coords[key])[:,1], + linestyle="-", color=self.colors[idx%len(self.colors)]) + + ax.plot( + np.array(self.coords[key])[:,0], + np.array(self.coords[key])[:,1], + marker="s", mfc="white", linestyle="", + color=self.colors[idx%len(self.colors)]) + + plt.show() + + def display_as_BB(self, img="spec", tol=1/100): + 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_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]) + + ax.add_patch(Rectangle( + min_min, + max_max[0]-min_min[0], + max_max[1]-min_min[1], + facecolor='none', + edgecolor=self.colors[idx%len(self.colors)], + lw=1)) + + ax.text(min_min[0], max_max[1], key, + color="white", + bbox=dict( + boxstyle='square, pad=0', + fc=self.colors[idx%len(self.colors)], + ec='none')) + + plt.show() + +##### EXAMPLE ##### +if __name__ == '__main__': + + annot_data = Results(os.path.join( + ".", + "audio_examples", + "SCW1807_20200713_064554.wav"), + os.path.join( + ".", + "outputs", + "SCW1807_20200713_064554-contours.json")) + + annot_data.display_contours() # or annot_data.display_contours(img="pcen") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index dbccbca594fea5908dbc0ceb42e87b6cb6cf0af1..a0c2d324fd6db14ae7e41b45893d41d895f7f870 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ appdirs==1.4.4 audioread==3.0.0 -certifi @ file:///croot/certifi_1665076670883/work/certifi +certifi @ file:///croot/certifi_1671487769961/work/certifi cffi==1.15.1 -charset-normalizer==2.1.1 -contourpy==1.0.6 +charset-normalizer==3.0.1 +contourpy==1.0.7 cycler==0.11.0 decorator==5.1.1 fonttools==4.38.0 @@ -12,21 +12,22 @@ joblib==1.2.0 kiwisolver==1.4.4 librosa==0.9.2 llvmlite==0.39.1 -matplotlib==3.6.2 -mpl-point-clicker==0.3.1 +matplotlib==3.6.3 numba==0.56.4 numpy==1.23.5 -packaging==22.0 -Pillow==9.3.0 +packaging==23.0 +Pillow==9.4.0 pooch==1.6.0 pycparser==2.21 pyparsing==3.0.9 python-dateutil==2.8.2 -requests==2.28.1 +requests==2.28.2 resampy==0.4.2 scikit-learn==1.2.0 -scipy==1.9.3 +scipy==1.10.0 +screeninfo==0.8.1 six==1.16.0 soundfile==0.11.0 threadpoolctl==3.1.0 -urllib3==1.26.13 +tk==0.1.0 +urllib3==1.26.14