Skip to content
Snippets Groups Projects
Commit 8ec72e5c authored by Loïc Lehnhoff's avatar Loïc Lehnhoff
Browse files

Optimization of display

+ visibility depends on visible frame

~ changed canvas.draw() to canvas.draw_idle()

+ huge re-organisation packed in lite_line_clicker.py
parent 48ded44d
No related branches found
No related tags found
No related merge requests found
audio_examples/SCW1807_20200711_114100.wav
audio_examples/SCW1807_20200713_064545.wav audio_examples/SCW1807_20200713_064545.wav
outputs/SCW1807_20200711_114100-contours.json
line_clicker/__pycache__ line_clicker/__pycache__
__pycache__ __pycache__
\ No newline at end of file
...@@ -7,7 +7,7 @@ from librosa import load, amplitude_to_db, stft, pcen ...@@ -7,7 +7,7 @@ from librosa import load, amplitude_to_db, stft, pcen
from scipy.signal import resample from scipy.signal import resample
##### FUNCTIONS ##### ##### FUNCTIONS #####
def save_dict(dictionary, folder, name): def save_dict(dictionary, folder, name, contours=True):
""" """
A function that saves a dictionary to a given path. A function that saves a dictionary to a given path.
...@@ -28,6 +28,18 @@ def save_dict(dictionary, folder, name): ...@@ -28,6 +28,18 @@ def save_dict(dictionary, folder, name):
None : save dict to json file. None : save dict to json file.
""" """
if len(dictionary) > 0: if len(dictionary) > 0:
if contours:
# delete labels with empty labels
for key in dictionary.keys():
if len(dictionary[key])<=1:
del dictionary[key]
# sort contouts by starting time.
dictionary = dict(
sorted(dictionary.items(),
key=lambda item: (np.min(np.array(item[1])[:,0]))))
with open(os.path.join(folder, name), "w") as f: with open(os.path.join(folder, name), "w") as f:
json.dump(dictionary, f, indent=4) json.dump(dictionary, f, indent=4)
......
##### IMPORTATIONS ##### ##### IMPORTATIONS #####
import os
import json import json
import numpy as np import os
from tkinter import * from tkinter import *
from tkinter import simpledialog as sd
from tkinter import filedialog as fd from tkinter import filedialog as fd
from tkinter import simpledialog as sd
from tkinter import ttk from tkinter import ttk
from matplotlib.backends.backend_tkagg import (NavigationToolbar2Tk,
FigureCanvasTkAgg)
from matplotlib.figure import Figure
import matplotlib.colors as mc import matplotlib.colors as mc
from line_clicker.line_clicker import clicker import numpy as np
# Import external functions from functions import load_waveform, save_dict, wave_to_spectrogram
from functions import load_waveform, wave_to_spectrogram, save_dict from line_clicker.lite_line_clicker import clicker
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg,
NavigationToolbar2Tk)
from matplotlib.figure import Figure
from matplotlib.patches import Rectangle
##### CLASSES ##### ##### CLASSES #####
...@@ -250,9 +252,10 @@ class App(object): ...@@ -250,9 +252,10 @@ class App(object):
Saves contours and closes the app. Saves contours and closes the app.
""" """
from parameters import (_default_width, _default_height, _default_hop_length, from parameters import (_default_bounds, _default_clipping, _default_cmap,
_default_nfft, _default_clipping, _default_cmap, _default_bounds, _default_height, _default_hop_length,
_default_left_panel_width) _default_left_panel_width, _default_nfft,
_default_width)
def __init__( def __init__(
self, self,
...@@ -298,7 +301,8 @@ class App(object): ...@@ -298,7 +301,8 @@ class App(object):
self.root.style.theme_use('clam') self.root.style.theme_use('clam')
self.create_canvas() self.create_canvas()
# addon # addons
self.figure_bboxes = []
self.klicker = clicker( self.klicker = clicker(
axis=self.axis, axis=self.axis,
names=["Line" + str(i+1) for i in range(self.NAME0, self.NAME1)], names=["Line" + str(i+1) for i in range(self.NAME0, self.NAME1)],
...@@ -313,10 +317,11 @@ class App(object): ...@@ -313,10 +317,11 @@ class App(object):
# To avoid problems, disconnect matplotlib keypress # To avoid problems, disconnect matplotlib keypress
self.figure.canvas.mpl_disconnect(self.klicker.key_press) self.figure.canvas.mpl_disconnect(self.klicker.key_press)
self.root.bind('<Key>', self.get_key_pressed) self.root.bind('<Key>', self.get_key_pressed)
self.figure.canvas.mpl_connect('button_press_event', self._draw_bbox)
# just to be sure # just to be sure
self.root.protocol("WM_DELETE_WINDOW", self.on_close) self.root.protocol("WM_DELETE_WINDOW", self.on_close)
self.root.resizable(True, True)
self.root.mainloop() self.root.mainloop()
def on_close(self): def on_close(self):
...@@ -341,11 +346,10 @@ class App(object): ...@@ -341,11 +346,10 @@ class App(object):
(It uses the "wait" parameter to force straigth lines). (It uses the "wait" parameter to force straigth lines).
""" """
if self.CHECK_bspline.get(): if self.CHECK_bspline.get():
self.klicker.wait = 2 self.klicker.wait_before_interpolation = 2
else: else:
self.klicker.wait = np.inf self.klicker.wait_before_interpolation = np.inf
self.klicker.update_lines() self.klicker.update_lines()
self.klicker.figure.canvas.draw()
def create_canvas(self): def create_canvas(self):
""" """
...@@ -389,9 +393,10 @@ class App(object): ...@@ -389,9 +393,10 @@ class App(object):
""" """
self.FFT_IN = IntVar(value=self._default_nfft) self.FFT_IN = IntVar(value=self._default_nfft)
self.HOP_IN = IntVar(value=self._default_hop_length) self.HOP_IN = IntVar(value=self._default_hop_length)
self.CHECK_bspline = IntVar(value=1) self.CHECK_bspline = BooleanVar(value=True)
self.CHECK_bbox = BooleanVar(value=False)
self.CLIP_IN = DoubleVar(value=self._default_clipping) self.CLIP_IN = DoubleVar(value=self._default_clipping)
self.OPTIONS = Variable(value=self.klicker.legend_labels) self.OPTIONS = Variable(value=list(self.klicker.coords.keys()))
def get_key_pressed(self, event): def get_key_pressed(self, event):
""" """
...@@ -424,10 +429,10 @@ class App(object): ...@@ -424,10 +429,10 @@ class App(object):
self.klicker.add_category(False) self.klicker.add_category(False)
# if a category is added. Update listbox and canvas. # if a category is added. Update listbox and canvas.
if len(self.klicker.legend_labels) > self.listbox.size(): if len(self.klicker.coords) > self.listbox.size():
self.listbox.insert( self.listbox.insert(
self.listbox.size(), self.listbox.size(),
self.klicker.legend_labels[-1]) list(self.klicker.coords.keys())[-1])
self.listbox.itemconfig( self.listbox.itemconfig(
self.listbox.size()-1, self.listbox.size()-1,
{ {
...@@ -472,7 +477,7 @@ class App(object): ...@@ -472,7 +477,7 @@ class App(object):
self.root.geometry( self.root.geometry(
f"{str(self._default_width)}x{str(self._default_height)}") f"{str(self._default_width)}x{str(self._default_height)}")
self.root.rowconfigure(1, weight=1) self.root.rowconfigure(1, weight=1)
self.root.rowconfigure(14, weight=1) self.root.rowconfigure(15, weight=1)
self.root.configure(bg='gainsboro') self.root.configure(bg='gainsboro')
# Add Panel for line selection on Left side # Add Panel for line selection on Left side
...@@ -492,12 +497,19 @@ class App(object): ...@@ -492,12 +497,19 @@ class App(object):
command=self.bspline_activation) command=self.bspline_activation)
self.activate_bspline.grid(row=4, column=0) self.activate_bspline.grid(row=4, column=0)
self.show_bbox = Checkbutton(
self.root,
text='Show Bounding Boxes',
variable=self.CHECK_bbox,
command=self._draw_bbox)
self.show_bbox.grid(row=5, column=0)
# Add space between panels # Add space between panels
self.empty_frame = Label( self.empty_frame = Label(
self.root, self.root,
width=self._default_left_panel_width, width=self._default_left_panel_width,
height=30) height=30)
self.empty_frame.grid(row=5, column=0) self.empty_frame.grid(row=6, column=0)
# Add panel for spectrogram personalisation on Left side. # Add panel for spectrogram personalisation on Left side.
self.fft_label = Label( self.fft_label = Label(
...@@ -505,69 +517,69 @@ class App(object): ...@@ -505,69 +517,69 @@ class App(object):
width=self._default_left_panel_width, width=self._default_left_panel_width,
text='FFT window size:', text='FFT window size:',
font=('calibre',10, 'bold')) font=('calibre',10, 'bold'))
self.fft_label.grid(row=6, column=0) self.fft_label.grid(row=7, column=0)
self.fft_entry = Entry( self.fft_entry = Entry(
self.root, self.root,
width=self._default_left_panel_width, width=self._default_left_panel_width,
textvariable=self.FFT_IN, textvariable=self.FFT_IN,
font=('calibre',10,'normal')) font=('calibre',10,'normal'))
self.fft_entry.grid(row=7, column=0) self.fft_entry.grid(row=8, column=0)
self.win_label = Label( self.win_label = Label(
self.root, self.root,
width=self._default_left_panel_width, width=self._default_left_panel_width,
text='Hop length:', text='Hop length:',
font=('calibre',10,'bold')) font=('calibre',10,'bold'))
self.win_label.grid(row=8, column=0) self.win_label.grid(row=9, column=0)
self.win_entry = Entry( self.win_entry = Entry(
self.root, self.root,
width=self._default_left_panel_width, width=self._default_left_panel_width,
textvariable=self.HOP_IN, textvariable=self.HOP_IN,
font=('calibre',10,'normal')) font=('calibre',10,'normal'))
self.win_entry.grid(row=9, column=0) self.win_entry.grid(row=10, column=0)
self.clip_label = Label( self.clip_label = Label(
self.root, self.root,
width=self._default_left_panel_width, width=self._default_left_panel_width,
text='Clipping (dB):', text='Clipping (dB):',
font=('calibre',10,'bold')) font=('calibre',10,'bold'))
self.clip_label.grid(row=10, column=0) self.clip_label.grid(row=11, column=0)
self.clip_entry = Entry( self.clip_entry = Entry(
self.root, self.root,
width=self._default_left_panel_width, width=self._default_left_panel_width,
textvariable=self.CLIP_IN, textvariable=self.CLIP_IN,
font=('calibre',10,'normal')) font=('calibre',10,'normal'))
self.clip_entry.grid(row=11, column=0) self.clip_entry.grid(row=12, column=0)
self.submit_button = Button( self.submit_button = Button(
self.root, self.root,
text='Update display', text='Update display',
width=self._default_left_panel_width, width=self._default_left_panel_width,
command=self.submit) command=self.submit)
self.submit_button.grid(row=12, column=0) self.submit_button.grid(row=13, column=0)
self.switch_view_button = Button( self.switch_view_button = Button(
self.root, self.root,
text=self.initial_text_pcen, text=self.initial_text_pcen,
width=self._default_left_panel_width, width=self._default_left_panel_width,
command=self.switch) command=self.switch)
self.switch_view_button.grid(row=13, column=0) self.switch_view_button.grid(row=14, column=0)
# Add buttons at the bottom of the interface # Add buttons at the bottom of the interface
self.quit_button = Button( self.quit_button = Button(
self.root, self.root,
text="Save & Quit", text="Save & Quit",
command=self._quit) command=self._quit)
self.quit_button.grid(row=15, column=0) self.quit_button.grid(row=16, column=0)
self.explore_button = Button( self.explore_button = Button(
self.root, self.root,
text="Open file explorer", text="Open file explorer",
command=self.select_file) command=self.select_file)
self.explore_button.grid(row=15, column=1) self.explore_button.grid(row=16, column=1)
# Add matplotlib tools at the top of the interface # Add matplotlib tools at the top of the interface
self.toolbarFrame = Frame(self.root) self.toolbarFrame = Frame(self.root)
...@@ -747,6 +759,37 @@ class App(object): ...@@ -747,6 +759,37 @@ class App(object):
vmax=np.nanmax(self.spectrogram)) vmax=np.nanmax(self.spectrogram))
self.canvas.draw() self.canvas.draw()
def _draw_bbox(self, *args):
# first, fetch coords
self.bboxes = []
for line in self.klicker.lines:
x, y = line.get_data()
if len(x)>0:
self.bboxes += [[[min(x), min(y)], [max(x)-min(x), max(y)-min(y)], line.get_color()]]
# remove current bbox if there are
if len(self.figure_bboxes)>0:
for rectangle in self.figure_bboxes:
rectangle.remove()
# add new bbox if needed
self.figure_bboxes = []
if self.CHECK_bbox.get():
for bbox in self.bboxes:
self.figure_bboxes += [
Rectangle(
xy=(bbox[0][0], bbox[0][1]),
width=bbox[1][0], height=bbox[1][1],
edgecolor = bbox[2],
facecolor='none',
fill=False,
lw=2)]
for rectangle in self.figure_bboxes:
self.axis.add_patch(rectangle)
self.klicker.figure.canvas.draw()
def _rename_label(self, event): def _rename_label(self, event):
""" """
A function that allows the user to rename a category A function that allows the user to rename a category
...@@ -780,13 +823,13 @@ class App(object): ...@@ -780,13 +823,13 @@ class App(object):
# get new name from user # get new name from user
new_name = sd.askstring( new_name = sd.askstring(
"Rename window", "Rename window",
f"Insert new name for '{self.klicker.legend_labels[index_item]}':" f"Insert new name for '{list(self.klicker.coords.keys())[index_item]}':"
) )
if isinstance(new_name, str): if isinstance(new_name, str):
if new_name in self.klicker.legend_labels: if new_name in self.klicker.coords.keys():
count=0 count=0
for label in self.klicker.legend_labels: for label in self.klicker.coords.keys():
if (label==new_name) or ("_".join(label.split("_")[:-1])==new_name): if (label==new_name) or ("_".join(label.split("_")[:-1])==new_name):
count+=1 count+=1
new_name = new_name + f"_{count}" new_name = new_name + f"_{count}"
...@@ -797,14 +840,15 @@ class App(object): ...@@ -797,14 +840,15 @@ class App(object):
# destroy item # destroy item
self.listbox.delete( self.listbox.delete(
index_item) index_item)
old_name = self.klicker.legend_labels.pop(index_item) old_name = list(self.klicker.coords.keys())[index_item]
# handle klicker # new coordinates
self.klicker.legend_labels.insert(index_item, new_name) self.klicker.coords = {
self.klicker.set_legend() new_name if key==old_name else key:value
for key,value in self.klicker.coords.items()}
self.klicker._set_legend()
self.klicker.current_line = index_item self.klicker.current_line = index_item
if old_name in self.klicker.coords.keys():
self.klicker.coords[new_name] = self.klicker.coords.pop(old_name)
self.klicker.update_lines() self.klicker.update_lines()
# insert item at the same index in listbox, with new name # insert item at the same index in listbox, with new name
...@@ -842,7 +886,7 @@ class App(object): ...@@ -842,7 +886,7 @@ class App(object):
width=self._default_left_panel_width, width=self._default_left_panel_width,
selectmode=SINGLE, selectmode=SINGLE,
listvariable=self.OPTIONS) listvariable=self.OPTIONS)
for idx in range(len(self.klicker.legend_labels)): for idx in range(len(self.klicker.coords)):
self.listbox.itemconfig(idx, self.listbox.itemconfig(idx,
{ {
'bg': self.klicker.colors[idx%len(self.klicker.colors)], 'bg': self.klicker.colors[idx%len(self.klicker.colors)],
...@@ -875,19 +919,21 @@ class App(object): ...@@ -875,19 +919,21 @@ class App(object):
save_dict( save_dict(
self.klicker.coords, self.klicker.coords,
self.DIR_OUT, self.DIR_OUT,
os.path.basename(self.WAVEFILE)[:-4]+"-contours.json") os.path.basename(self.WAVEFILE)[:-4]+"-contours.json",
contours=True)
# save parameters # save parameters
save_dict( save_dict(
{ {
"PCEN": (self.switch_view_button['text'] == "Switch to PCEN"), "PCEN": (self.switch_view_button['text'] == "Switch to PCEN"),
"SR": self.NEW_SR, "SR": int(self.NEW_SR),
"NFFT":self.NFFT, "NFFT": int(self.NFFT),
"HOP_LENGTH":self.HOP_LENGTH, "HOP_LENGTH": int(self.HOP_LENGTH),
"CLIPPING":self.CLIPPING "CLIPPING": int(self.CLIPPING)
}, },
os.path.join(self.DIR_OUT, ".."), os.path.join(self.DIR_OUT, ".."),
"last-parameters-used.json" "last-parameters-used.json",
contours=False
) )
# quit window # quit window
......
File deleted
File deleted
...@@ -12,7 +12,7 @@ import matplotlib.lines as mlines ...@@ -12,7 +12,7 @@ import matplotlib.lines as mlines
from scipy.interpolate import interp1d from scipy.interpolate import interp1d
##### FUNCTIONS ##### ##### FUNCTIONS #####
def to_curve(x, y, kind="quadratic"): def to_curve(x, y, kind="quadratic", precision=10):
""" """
A function to compute a curvy line between two points. A function to compute a curvy line between two points.
It interpolates a line with 100 points. It interpolates a line with 100 points.
...@@ -29,6 +29,7 @@ def to_curve(x, y, kind="quadratic"): ...@@ -29,6 +29,7 @@ def to_curve(x, y, kind="quadratic"):
The interpolation method to use. Can be any method in : ‘linear’, The interpolation method to use. Can be any method in : ‘linear’,
‘nearest’, ‘nearest-up’, ‘zero’, ‘slinear’, ‘quadratic’, ‘cubic’, ‘nearest’, ‘nearest-up’, ‘zero’, ‘slinear’, ‘quadratic’, ‘cubic’,
‘previous’, or ‘next’. ‘previous’, or ‘next’.
precision : number of segments between each point.
Returns Returns
------- -------
...@@ -43,7 +44,7 @@ def to_curve(x, y, kind="quadratic"): ...@@ -43,7 +44,7 @@ def to_curve(x, y, kind="quadratic"):
f = interp1d(x,y, kind=kind) f = interp1d(x,y, kind=kind)
xi = np.linspace(x.min(), x.max(), 100 * (len(x)-1)) xi = np.linspace(x.min(), x.max(), precision * (len(x)-1))
yi = f(xi) yi = f(xi)
return xi, yi return xi, yi
...@@ -300,8 +301,6 @@ class clicker(object): ...@@ -300,8 +301,6 @@ class clicker(object):
'key_press_event', 'key_press_event',
self.get_key_event) self.get_key_event)
def add_category(self, show): def add_category(self, show):
""" """
A method to add a line (therefore a new category) to the plot. A method to add a line (therefore a new category) to the plot.
...@@ -401,14 +400,23 @@ class clicker(object): ...@@ -401,14 +400,23 @@ class clicker(object):
else: else:
self.figure_bsplines = [] self.figure_bsplines = []
for idx, legend_label in enumerate(self.legend_labels): for idx, legend_label in enumerate(self.legend_labels):
if np.array(self.coords[legend_label]).shape[0] > self.wait:
curves = to_curve( curves = to_curve(
np.array(self.coords[legend_label])[:,0], np.array(self.coords[legend_label])[:,0],
np.array(self.coords[legend_label])[:,1], np.array(self.coords[legend_label])[:,1],
kind=self.bspline) kind=self.bspline)
self.figure_bsplines += [mlines.Line2D(curves[0], curves[1], self.figure_bsplines += [mlines.Line2D(curves[0], curves[1],
label=legend_label, label=legend_label,
color=self.colors[idx%len(self.colors)], color=self.colors[idx%len(self.colors)],
**{'linestyle':self.linestyle})] **{'linestyle':self.linestyle})]
else:
self.figure_bsplines += [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)],
**{'linestyle':self.linestyle})]
def clear_category(self): def clear_category(self):
""" """
...@@ -504,7 +512,7 @@ class clicker(object): ...@@ -504,7 +512,7 @@ class clicker(object):
Returns Returns
------- -------
None : is used to trigger methods add_points and rm_points(). None : is used to trigger methods add_points and rm_points.
""" """
if self.figure.canvas.widgetlock.available(self): if self.figure.canvas.widgetlock.available(self):
pressed, x, y = event.button, event.xdata, event.ydata pressed, x, y = event.button, event.xdata, event.ydata
...@@ -528,7 +536,7 @@ class clicker(object): ...@@ -528,7 +536,7 @@ class clicker(object):
np.array(self.coords[self.legend_labels[self.current_line]]), np.array(self.coords[self.legend_labels[self.current_line]]),
event.xdata, event.ydata) event.xdata, event.ydata)
self.index, dist = np.argmin(distances), np.min(distances) self.index, dist = np.argmin(distances), np.min(distances)
if dist < 0.1: if dist < 0.25:
self.currently_pressed = True self.currently_pressed = True
elif ((pressed is self.param["move_point"]) and elif ((pressed is self.param["move_point"]) and
...@@ -744,9 +752,9 @@ class clicker(object): ...@@ -744,9 +752,9 @@ class clicker(object):
##### MAIN ##### ##### MAIN #####
if __name__ == '__main__': if __name__ == '__main__':
# dummy example # dummy example
img = np.array([[0,1,0],[1,0,1],[0,1,0]]) img = np.tile([[0, 0],[0, 0]], (25,25))
fig, ax = plt.subplots(figsize=(16, 9)) fig, ax = plt.subplots(figsize=(16, 9))
ax.imshow(img, cmap="viridis") ax.imshow(img, cmap="gray")
base = clicker(axis=ax, bspline="quadratic") base = clicker(axis=ax, bspline="quadratic")
plt.show(block=True) plt.show(block=True)
\ No newline at end of file
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment