class SyllSimSummaryPage(Screen): def __init__(self, *args, **kwargs): self.fig6, self.ax6 = plt.subplots() self.syllsim_hist_canvas = FigureCanvasKivyAgg(self.fig6) super(SyllSimSummaryPage, self).__init__(*args, **kwargs) def calculate_syllsim_thresh_stats(self): # syllable similarity thresholds from all the songs processed syllsim_thresholds = self.manager.get_screen( 'syllsim_threshold_page').syllsim_thresholds # clear the plot self.ax6.clear() # plot histogram of the syllable similarity thresholds used if len(np.unique(syllsim_thresholds)) > 20: self.ax6.hist(x=syllsim_thresholds, bins='auto', color=(0.196, 0.643, 0.80), alpha=0.7) else: # the trick is to set up the bins centered on the integers, i.e. # -0.5, 0.5, 1,5, 2.5, ... up to max(data) + 1.5. Then you substract -0.5 to # eliminate the extra bin at the end. bins = np.arange(min(syllsim_thresholds), max(syllsim_thresholds) + 1.5, 1) - 0.5 self.ax6.hist(x=syllsim_thresholds, bins=bins, color=(0.196, 0.643, 0.80), alpha=0.7) self.ax6.set_xlabel('Syllable Similarity Threshold') self.ax6.set_ylabel('Number of Songs with Threshold') self.syllsim_hist_canvas.draw() self.ids.syllsim_hist.clear_widgets() self.ids.syllsim_hist.add_widget(self.syllsim_hist_canvas) # calculate stats for the submitted thresholds and add them to the screen self.ids.num_files.text = 'Number of Files: ' + str( (len(syllsim_thresholds))) self.ids.avg_syllsim_thresh.text = 'Average: ' + str( round(np.mean(syllsim_thresholds), 1)) self.ids.std_dev_syllsim_thresh.text = 'Standard Deviation: ' + str( round(np.std(syllsim_thresholds), 1)) self.ids.min_syllsim_thresh.text = 'Minimum: ' + str( min(syllsim_thresholds)) self.ids.max_syllsim_thresh.text = 'Maximum: ' + str( max(syllsim_thresholds)) # set the user input to the average as a default (they can change this before submitting) self.ids.submitted_syllsim_thresh_input.text = str( round(np.mean(syllsim_thresholds), 1)) def submit_syllsim_thresh(self): # update the landing page with the syllable similarity threshold the user chooses/submits self.manager.get_screen( 'landing_page' ).ids.syll_sim_input.text = self.ids.submitted_syllsim_thresh_input.text
class MainApp(App): def build(self): """ some initialization Args: rect: a dummy rectangle, whose width and height will be reset trigger by user event x0, y0: mouse click location x1, y1: mouse release location canvas: FigureCanvasKivyAgg object. Note that I'm using this back end right now as the FigureCanvas backend has bug with plt.show() selectors: store user-drawn rectangle data offsetx: translation of origin on x direction offsety: translation of origin on y direction """ self.selectors = [] img = mpimg.imread(sys.argv[1]) self.fig, self.ax = plt.subplots(1) plt.imshow(img) width, height = self.fig.canvas.get_width_height() pxorigin = self.ax.transData.transform([(0, 0)]) self.offsetx = pxorigin[0][0] self.offsety = height - pxorigin[0][1] print('offsetx, offsety', self.offsetx, self.offsety) self.rect = Rectangle((0, 0), 1, 1) self.rect.set_fill(False) self.rect.set_edgecolor('b') self.x0 = None self.y0 = None self.x1 = None self.y1 = None self.canvas = FigureCanvasKivyAgg( plt.gcf()) # get the current reference of plt box = BoxLayout() self.ax.add_patch(self.rect) # attach our rectangle to axis self.canvas.mpl_connect("button_press_event", self.on_press) self.canvas.mpl_connect("button_release_event", self.on_release) box.add_widget(self.canvas) return box def on_press(self, event): """ record user click location """ self.x0 = event.xdata self.y0 = event.ydata print('x0, y0', self.x0, self.y0) def on_release(self, event): """ record user mouse release location """ self.x1 = event.xdata self.y1 = event.ydata self.rect.set_width(self.x1 - self.x0) self.rect.set_height(self.y1 - self.y0) self.rect.set_xy((self.x0, self.y0)) xy_pixels = self.ax.transData.transform( np.vstack([self.x0, self.y0]).T) px, py = xy_pixels.T width, height = self.fig.canvas.get_width_height() # transform from origin on lower left to orgin on upper right py = height - py # account for translation factor px -= self.offsetx py -= self.offsety self.selectors.append( [px, py, abs(self.x1 - self.x0), abs(self.y1 - self.y0)]) print('your rectangle', px, py, abs(self.x1 - self.x0), abs(self.y1 - self.y0)) self.canvas.draw()
class SyllSimThresholdPage(Screen): user_noise_thresh = StringProperty() user_syll_sim_thresh = StringProperty() def __init__(self, *args, **kwargs): self.fig5, self.ax5 = plt.subplots() self.plot_syllsim_canvas = FigureCanvasKivyAgg(self.fig5) self.ax5 = plt.Axes(self.fig5, [0., 0., 1., 1.]) self.ax5.set_axis_off() self.fig5.add_axes(self.ax5) super(SyllSimThresholdPage, self).__init__(*args, **kwargs) self.log = get_logger(__name__) def setup(self): self.syllsim_thresholds = [] self.i = 0 self.files = self.parent.files self.record = True self.next() def next(self): # if not first entering the app, record the threshold if self.i > 0 and self.record: self.syllsim_thresholds.append(float(self.ids.user_syllsim.text)) # otherwise it is the first time, so reset syllable similarity threshold to the default else: self.ids.user_syllsim.text = self.user_syll_sim_thresh # if it is the last song go to syllable similarity threshold # summary page, otherwise process song' if self.i == len(self.files): self.manager.current = 'syllsim_summary_page' else: errors = '' f_name = os.path.join(self.parent.directory, self.files[self.i]) try: self.update(f_name) self.i += 1 self.record = True except Exception as e: self.ids.syllsim_graph.clear_widgets() self.ids.similarity.text = '' self.ids.song_syntax.text = '' errors += "WARNING : Skipped file {0}\n{1}\n".format(f_name, e) # raise e self.log.info(errors) self.record = False all_noise = ErrorInSyllSimThresholdWidgetPopup(self, errors) all_noise.open() self.i += 1 self.log.info(self.i) def update(self, f_name): self.ids.user_syllsim.text = self.ids.user_syllsim.text ons, offs, thresh, ms, htz = analyze.load_bout_data(f_name) self.onsets = ons self.offsets = offs self.syll_dur = self.offsets - self.onsets self.threshold_sonogram = thresh # zero anything before first onset or after last offset # (not offset row is already zeros, so okay to include) # this will take care of any noise before or after the song threshold_sonogram_crop = self.threshold_sonogram.copy() threshold_sonogram_crop[:, 0:self.onsets[0]] = 0 threshold_sonogram_crop[:, self.offsets[-1]:-1] = 0 # ^connectivity 1=4 or 2=8(include diagonals) self.labeled_sonogram = label(threshold_sonogram_crop, connectivity=1) corrected_sonogram = remove_small_objects( self.labeled_sonogram, min_size=float(self.user_noise_thresh) + 1, # add one to make =< threshold connectivity=1) self.son_corr, son_corr_bin = analyze.get_sonogram_correlation( sonogram=corrected_sonogram, onsets=self.onsets, offsets=self.offsets, syll_duration=self.syll_dur, corr_thresh=float(self.ids.user_syllsim.text)) self.init_plot() self.new_thresh() def init_plot(self): # prepare graph and make plot take up the entire space rows, cols = np.shape(self.threshold_sonogram) data = np.zeros((rows, cols)) self.ax5.clear() self.ax5 = plt.Axes(self.fig5, [0., 0., 1., 1.]) self.ax5.set_axis_off() self.fig5.add_axes(self.ax5) # plot placeholder data cmap = plt.cm.tab20b cmap.set_under(color='black') cmap.set_over(color='gray') cmap.set_bad(color='white') self.plot_syllsim = self.ax5.imshow(data + 3, extent=[0, cols, 0, rows], aspect='auto', cmap=cmap, vmin=0, vmax=1000.) self.trans = tx.blended_transform_factory(self.ax5.transData, self.ax5.transAxes) self.lines_on, = self.ax5.plot(np.repeat(self.onsets, 3), np.tile([0, .75, np.nan], len(self.onsets)), linewidth=0.75, color='g', transform=self.trans) self.lines_off, = self.ax5.plot(np.repeat(self.offsets, 3), np.tile([0, .90, np.nan], len(self.offsets)), linewidth=0.75, color='g', transform=self.trans) self.ids.syllsim_graph.clear_widgets() self.ids.syllsim_graph.add_widget(self.plot_syllsim_canvas) def new_thresh(self): # get syllable correlations for entire sonogram # create new binary matrix with new threshold son_corr_bin = np.zeros(self.son_corr.shape) son_corr_bin[self.son_corr >= float(self.ids.user_syllsim.text)] = 1 # get syllable pattern syll_pattern = analyze.find_syllable_pattern(son_corr_bin) display_pattern = ", ".join(str(x) for x in syll_pattern) self.ids.song_syntax.text = 'Song Syntax: {}'.format(display_pattern) syll_stereotypy, syll_stereotypy_max, syll_stereotypy_min = \ analyze.calc_syllable_stereotypy(self.son_corr, syll_pattern) # Formatting for summary spacing1 = '{:<12}{:<8}{:<8}{:<8}\n' spacing2 = '{:<16}{:<8}{:<8}{:<8}\n' spacing3 = '{:<15}{:<8}{:<8}{:<8}\n' stereotypy_text = spacing1.format('Syllable', 'Avg', 'Min', 'Max') for idx in range(len(syll_stereotypy)): if not np.isnan(syll_stereotypy[idx]): if idx >= 10: spacing = spacing3 else: spacing = spacing2 stereotypy_text += spacing.format( str(idx), round(syll_stereotypy[idx], 1), round(syll_stereotypy_min[idx], 1), round(syll_stereotypy_max[idx], 1), ) if stereotypy_text == spacing1.format('Syllable', 'Avg', 'Min', 'Max'): stereotypy_text += 'No Repeated Syllables' self.ids.similarity.text = stereotypy_text syll_labeled = self.threshold_sonogram.copy() # making background color black (negative number will) syll_labeled[syll_labeled == 0] = -10 # need to find the max number to define the image u, indices = np.unique(syll_pattern, return_inverse=True) num_unique = len(u) # set clip so that anything over will be colored grey self.plot_syllsim.set_clim(0, num_unique) grey = num_unique + 1 # color syllable patterns for on, off, syll in zip(self.onsets, self.offsets, indices): syll_labeled[:, on:off][syll_labeled[:, on:off] == 1] = syll # color noise white, this value will be set to nan. But it will be # overwritten in the noise below # we are using a number larger than grey. to_nan = grey + 1 for region in regionprops(self.labeled_sonogram): if region.area <= int(self.user_noise_thresh): syll_labeled[self.labeled_sonogram == region.label] = to_nan # color signal before and after song to grey on = self.onsets[0] off = self.offsets[-1] syll_labeled[:, 0:on][syll_labeled[:, 0:on] == 1] = grey syll_labeled[:, off:-1][syll_labeled[:, off:-1] == 1] = grey # color signal between syllables grey for off, on in zip(self.offsets[:-1], self.onsets[1:]): syll_labeled[:, off:on][syll_labeled[:, off:on] >= 0] = grey # little hack to make noise regions white only if inside onset/offsets syll_labeled[syll_labeled == to_nan] = np.nan # update image in widget # plot the actual data now self.plot_syllsim.set_data(syll_labeled) self.plot_syllsim_canvas.draw() def syllsim_thresh_instructions(self): syllsim_popup = SyllSimThreshInstructionsPopup() syllsim_popup.open()
class NoiseThresholdPage(Screen): user_noise_thresh = StringProperty() def __init__(self, *args, **kwargs): self.fig3, self.ax3 = plt.subplots() self.plot_noise_canvas = FigureCanvasKivyAgg(self.fig3) self.ax3 = plt.Axes(self.fig3, [0., 0., 1., 1.]) self.ax3.set_axis_off() self.fig3.add_axes(self.ax3) super(NoiseThresholdPage, self).__init__(*args, **kwargs) def setup(self): self.noise_thresholds = [] self.i = 0 # self.files = [os.path.basename(i) for i in glob.glob(self.parent.directory + '*.gzip')] self.files = self.parent.files self.next() def next(self): # if not first entering the app, record the threshold if self.i > 0: self.noise_thresholds.append(int(self.ids.user_noise_size.text)) # otherwise it is the first time, # so reset noise size threshold to the default else: self.ids.user_noise_size.text = self.user_noise_thresh # if it is the last song go to noise threshold summary page, # otherwise process song if self.i == len(self.files): self.manager.current = 'noise_summary_page' else: self.ids.user_noise_size.text = self.ids.user_noise_size.text ons, offs, thresh, ms, htz = analyze.load_bout_data( os.path.join(self.parent.directory, self.files[self.i])) self.onsets = ons self.offsets = offs self.threshold_sonogram = thresh [self.rows, self.cols] = np.shape(self.threshold_sonogram) # prepare graph and make plot take up the entire space data = np.zeros((self.rows, self.cols)) self.ax3.clear() self.ax3 = plt.Axes(self.fig3, [0., 0., 1., 1.]) self.ax3.set_axis_off() self.fig3.add_axes(self.ax3) cmap = plt.cm.prism cmap.set_under(color='black') cmap.set_bad(color='white') self.plot_noise = self.ax3.imshow( data, extent=[0, self.cols, 0, self.rows], aspect='auto', cmap=cmap, norm=matplotlib.colors.LogNorm(), vmin=3.01) self.ids.noise_graph.clear_widgets() self.ids.noise_graph.add_widget(self.plot_noise_canvas) self.new_thresh() self.i += 1 def new_thresh(self): # find notes and label based on connectivity # zero anything before first onset or after last offset # (not offset row is already zeros, so okay to include) # this will take care of any noise before or after the song # before labeling the notes threshold_sonogram_crop = self.threshold_sonogram.copy() # Make onsets and offsets and offsets black (hidden) threshold_sonogram_crop[:, 0:self.onsets[0]] = 0 threshold_sonogram_crop[:, self.offsets[-1]:-1] = 0 for off, on in zip(self.offsets[:-1], self.onsets[1:]): threshold_sonogram_crop[:, off:on][ threshold_sonogram_crop[:, off:on] >= 0] = 0 # ^connectivity 1=4 or 2=8(include diagonals) labeled_sonogram = label(threshold_sonogram_crop, connectivity=1) # change label of all notes with size > threshold to be the same # and all < to be the same for region in regionprops(labeled_sonogram): if region.area > int(self.ids.user_noise_size.text): labeled_sonogram[labeled_sonogram == region.label] = region.area else: labeled_sonogram[labeled_sonogram == region.label] = 1 # mask noise white labeled_sonogram = np.ma.masked_where(labeled_sonogram == 1, labeled_sonogram) # update image in widget self.plot_noise.set_data(labeled_sonogram + 3) # plot the actual data now self.plot_noise_canvas.draw() def noise_thresh_instructions(self): noise_popup = NoiseThreshInstructionsPopup() noise_popup.open()
class ControlPanel(Screen): # these connect the landing page user input to the control panel find_gzips = BooleanProperty() user_signal_thresh = StringProperty() user_min_silence = StringProperty() user_min_syllable = StringProperty() def __init__(self, **kwargs): self.top_image = ObjectProperty(None) self.mark_boolean = False self.click = 0 self.direction_to_int = {'left': -1, 'right': 1} # bottom_image = ObjectProperty(None) self.register_event_type('on_check_boolean') self.fig2 = matplotlib.figure.Figure() # self.fig2, self.ax2 = plt.subplots() self.plot_binary_canvas = FigureCanvasKivyAgg(self.fig2) self.fig2.canvas.mpl_connect('key_press_event', self.move_mark) # self.ax2 = self.fig2.add_subplot(111) self.ax2 = self.fig2.add_axes([0., 0., 1., 1.]) # self.ax2 = plt.Axes(self.fig2, [0., 0., 1., 1.]) self.ax2.set_axis_off() # self.fig2.add_axes(self.ax2) # all songs and files self.file_names = None self.files = None self.output_path = None # attributes for song that is being worked on self.i = None self.song = None self.current_file = None self.syllable_onsets = None self.syllable_offsets = None # place holders for plots self.plot_binary = None self.trans = None self.lines_on = None self.lines_off = None # for plotting self.index = None self.mark = None self.graph_location = None super(ControlPanel, self).__init__(**kwargs) def on_check_boolean(self): if self.click >= 2: marks_popup = popups.FinishMarksPopup(self) marks_popup.open() def on_touch_down(self, touch): super(ControlPanel, self).on_touch_down(touch) if self.mark_boolean is True: self.click += 1 self.dispatch('on_check_boolean') ControlPanel.disabled = True return True def reset_panel(self): self.mark_boolean = False self.click = 0 ControlPanel.disabled = False def move_mark(self, event, move_interval=7): if self.ids.add.state == 'down': # adding if event.key in self.direction_to_int and \ (25 <= self.graph_location < self.song.cols - 25): self.graph_location += self.direction_to_int[ event.key] * move_interval self.update_mark(self.graph_location) elif event.key == 'enter': if self.ids.syllable_beginning.state == 'down': self.add_onsets() else: self.add_offsets() self.mark_boolean = False self.click = 0 ControlPanel.disabled = False elif event.key == 'x': self.cancel_mark() # deleting elif self.ids.delete.state == 'down': if event.key in self.direction_to_int: self.index += self.direction_to_int[event.key] # onsets if self.ids.syllable_beginning.state == 'down': if self.index < 0: self.index = len(self.syllable_onsets) - 1 if self.index >= len(self.syllable_onsets): self.index = 0 self.update_mark(self.syllable_onsets[self.index]) # offsets else: if self.index < 0: self.index = len(self.syllable_offsets) - 1 if self.index >= len(self.syllable_offsets): self.index = 0 self.update_mark(self.syllable_offsets[self.index]) elif event.key == 'enter': if self.ids.syllable_beginning.state == 'down': self.delete_onsets() else: self.delete_offsets() self.mark_boolean = False self.click = 0 ControlPanel.disabled = False elif event.key == 'x': self.cancel_mark() def enter_mark(self): if self.ids.add.state == 'down': # adding if self.ids.syllable_beginning.state == 'down': self.add_onsets() else: self.add_offsets() elif self.ids.delete.state == 'down': # deleting if self.ids.syllable_beginning.state == 'down': self.delete_onsets() else: self.delete_offsets() self.mark_boolean = False self.click = 0 ControlPanel.disabled = False def cancel_mark(self): self.mark.remove() self.image_syllable_marks() self.mark_boolean = False self.click = 0 ControlPanel.disabled = False def update_mark(self, new_mark): self.mark.set_xdata(new_mark) self.plot_binary_canvas.draw() def add_mark(self, touchx, touchy): self.mark_boolean = True conversion = self.song.sonogram.shape[1] / self.ids.graph_binary.size[0] self.graph_location = math.floor( (touchx - self.ids.graph_binary.pos[0]) * conversion) ymax = 0.75 if self.ids.syllable_beginning.state == 'down' else 0.90 # graph as another color/group of lines self.mark = self.ax2.axvline(self.graph_location, ymax=ymax, color='m', linewidth=0.75) self.plot_binary_canvas.draw() def add_onsets(self): # https://stackoverflow.com/questions/29408661/add-elements-into-a # -sorted-array-in-ascending-order if self.graph_location is None: return else: self.syllable_onsets = np.insert( self.syllable_onsets, np.searchsorted(self.syllable_onsets, self.graph_location), self.graph_location) self.mark.remove() self.image_syllable_marks() self.graph_location = None def add_offsets(self): if self.graph_location is None: return else: self.syllable_offsets = np.insert( self.syllable_offsets, np.searchsorted(self.syllable_offsets, self.graph_location), self.graph_location) self.mark.remove() self.image_syllable_marks() self.graph_location = None def delete_mark(self, touchx, touchy): self.mark_boolean = True conversion = self.song.sonogram.shape[1] / \ self.ids.graph_binary.size[0] self.graph_location = math.floor( (touchx - self.ids.graph_binary.pos[0]) * conversion) if self.ids.syllable_beginning.state == 'down': ymax = 0.75 # find nearest onset self.index = self.take_closest(self.syllable_onsets, self.graph_location) location = self.syllable_onsets[self.index] else: ymax = 0.90 # find nearest offset self.index = self.take_closest(self.syllable_offsets, self.graph_location) location = self.syllable_offsets[self.index] self.mark = self.ax2.axvline(location, ymax=ymax, color='m', linewidth=0.75) self.plot_binary_canvas.draw() def delete_onsets(self): if self.index is None: return else: onsets_list = list(self.syllable_onsets) onsets_list.remove(self.syllable_onsets[self.index]) self.syllable_onsets = np.array(onsets_list) self.mark.remove() self.image_syllable_marks() self.index = None def delete_offsets(self): if self.index is None: return else: offsets_list = list(self.syllable_offsets) offsets_list.remove(self.syllable_offsets[self.index]) self.syllable_offsets = np.array(offsets_list) self.mark.remove() self.image_syllable_marks() self.index = None # called in kv just before entering control panel screen (on_pre_enter) def setup(self): Logger.info("Setting up") # storage for parameters self.save_parameters_all = {} self.save_syllables_all = {} self.save_tossed = {} self.save_conversions_all = {} self.i = 0 self.files = self.parent.files self.file_names = self.parent.file_names # these are the dictionaries that are added to with each song self.output_path = os.path.join( self.parent.directory, "SegSyllsOutput_{}".format(time.strftime("%Y%m%d_T%H%M%S"))) if not os.path.isdir(self.output_path): os.makedirs(self.output_path) self.next() def update_panel_text(self): # this updates the text or slider limits on the control panel screen self.ids.slider_threshold_label.text = '{}%'.format( self.song.percent_keep) # have to round these because of the conversion self.ids.slider_min_silence_label.text = "{} ms".format( round(self.song.min_silence * self.song.ms_pix, 1)) self.ids.slider_min_syllable_label.text = '{} ms'.format( round(self.song.min_syllable * self.song.ms_pix, 1)) # want max to be 50ms self.ids.slider_min_silence.max = 50 / self.song.ms_pix # want max to be 350ms self.ids.slider_min_syllable.max = 350 / self.song.ms_pix self.ids.normalize_amp.state = self.song.normalized self.ids.slider_threshold.value = self.song.percent_keep self.ids.slider_min_silence.value = self.song.min_silence self.ids.slider_min_syllable.value = self.song.min_syllable self.ids.syllable_beginning.state = 'down' self.ids.syllable_ending.state = 'normal' self.ids.add.state = 'normal' self.ids.delete.state = 'normal' self.ids.slider_frequency_filter.value1 = self.song.filter_boundary[0] self.ids.slider_frequency_filter.value2 = self.song.filter_boundary[1] self.ids.slider_frequency_filter.min = 0 self.ids.slider_frequency_filter.max = self.song.rows self.ids.range_slider_crop.value1 = self.song.bout_range[0] self.ids.range_slider_crop.value2 = self.song.bout_range[1] self.ids.range_slider_crop.min = 0 self.ids.range_slider_crop.max = self.song.cols def reset_parameters(self): self.song.reset_params(user_signal_thresh=self.user_signal_thresh, user_min_silence=self.user_min_silence, user_min_syllable=self.user_min_syllable, id_min_sil=self.ids.slider_min_silence.min, id_min_syl=self.ids.slider_min_syllable.min) self.ids.normalize_amp.state = self.song.normalized self.update_panel_text() self._update() def next(self): self.current_file = self.files[self.i] # increment i so next file will be opened on submit/toss self.i += 1 # get initial data Logger.info("Loading file {}".format(self.current_file)) f_path = os.path.join(self.parent.directory, self.current_file) f_size = os.path.getsize(f_path) # 1 000 000 bytes is 1 megabyte max_file_size = 3000000 if f_size > max_file_size: Logger.info("Large song") popups.LargeFilePopup(self, self.current_file, str(round(f_size / 1000000, 1))).open() else: self.process() def toss(self): Logger.info("Tossing {}".format(self.current_file)) # save file name to dictionary self.save_tossed[self.i - 1] = {'FileName': self.current_file} # remove from saved parameters and associated gzip if # file ends up being tossed if self.current_file in self.save_parameters_all: del self.save_parameters_all[self.current_file] del self.save_syllables_all[self.current_file] del self.save_conversions_all[self.current_file] os.remove(self.output_path + '/SegSyllsOutput_' + self.file_names[self.i - 1] + '.gzip') # write if last file otherwise go to next file if self.i == len(self.files): self.save_all_parameters() else: self.next() def process(self): self.song = Sonogram(wavfile=self.current_file, directory=self.parent.directory, find_gzips=self.find_gzips) self.ids.freq_axis_middle.text = str( round( self.song.rows * self.song.hertzPerPixel / 2 / 1000)) + " kHz" # reset default parameters for new song # (will be used by update to graph the first attempt) Logger.info("Setting default params") self.song.set_song_params(user_signal_thresh=self.user_signal_thresh, user_min_silence=self.user_min_silence, user_min_syllable=self.user_min_syllable, id_min_sil=self.ids.slider_min_silence.min, id_min_syl=self.ids.slider_min_syllable.min) prev_onsets = self.song.prev_onsets prev_offsets = self.song.prev_offsets # if the user goes back to previous song and then goes forward again, # it will pull what they had already submitted (so the user does not # lose progress) if len(self.save_parameters_all) > 0: if self.current_file in self.save_parameters_all: params = self.save_parameters_all[self.current_file] prev_onsets = np.asarray( self.save_syllables_all[self.current_file]['Onsets']) prev_offsets = np.asarray( self.save_syllables_all[self.current_file]['Offsets']) Logger.info("Updating params based on previous run") self.song.update_by_params(params) Logger.info("Updating panel text") if self.song.params: self.song.update_by_params(self.song.params) self.update_panel_text() # update the label stating the current file and the file number out # of total number of files # use self.i since you have not yet incremented self.ids.current_file.text = "{}\nFile {} out of {}".format( self.file_names[self.i - 1], self.i, len(self.files)) # initialize the matplotlib figures/axes (no data yet) # ImageSonogram is its own class and top_image is an instance of it # (defined in kv) - had trouble doing this for the bottom image Logger.info("Creating initial sonogram") self.top_image.image_sonogram_initial(self.song.rows, self.song.cols) Logger.info("Creating initial binary") self.image_binary_initial() Logger.info("Updating") # run update to load images for the first time for this file self._update(prev_run_onsets=prev_onsets, prev_run_offsets=prev_offsets) Logger.info("Done with automation portion") def update(self, filter_boundary, bout_range, percent_keep, min_silence, min_syllable, normalized): self.song.set_song_params(filter_boundary=filter_boundary, bout_range=bout_range, percent_keep=percent_keep, min_silence=min_silence, min_syllable=min_syllable, normalized=normalized, user_signal_thresh=self.user_signal_thresh, user_min_silence=self.user_min_silence, user_min_syllable=self.user_min_syllable, id_min_sil=self.ids.slider_min_silence.min, id_min_syl=self.ids.slider_min_syllable.min) self.update_panel_text() self._update() def _update(self, prev_run_onsets=None, prev_run_offsets=None): # must do this for image to update for some reason sonogram = self.song.sonogram.copy() # run HPF, scale based on average amplitude # (increases low amplitude sections), and graph sonogram freqfiltered_sonogram = seg.frequency_filter(self.song.filter_boundary, sonogram) # switch next two lines if you don't want amplitude scaled if self.ids.normalize_amp.state == 'down': scaled_sonogram = seg.normalize_amplitude(freqfiltered_sonogram) else: scaled_sonogram = freqfiltered_sonogram # plot resultant sonogram in the top graph in control panel self.top_image.image_sonogram(scaled_sonogram) # apply threshold to signal self.thresh_sonogram = seg.threshold_image(self.song.percent_keep, scaled_sonogram) # calculate onsets and offsets using binary (thresholded) image onsets, offsets, silence_durations, sum_sonogram_scaled = \ seg.initialize_onsets_offsets(self.thresh_sonogram) # update the automatic onsets and offsets based on the slider values # for min silence and min syllable durations syllable_onsets, syllable_offsets = seg.set_min_silence( self.song.min_silence, onsets, offsets, silence_durations) syllable_onsets, syllable_offsets = seg.set_min_syllable( self.song.min_syllable, syllable_onsets, syllable_offsets) # lastly, remove onsets and offsets that are outside of the crop # values (on the time axis) self.syllable_onsets, self.syllable_offsets = \ seg.crop(self.song.bout_range, syllable_onsets, syllable_offsets) # check if the song has been run before (if gzip data was loaded) if prev_run_onsets is None: prev_run_onsets = np.empty([0]) prev_run_offsets = np.empty([0]) # change the onsets and offsets to those in gzip if gzip was loaded if prev_run_onsets.size: self.syllable_onsets = prev_run_onsets self.syllable_offsets = prev_run_offsets # plot resultant binary sonogram along with onset and offset lines self.image_binary() self.image_syllable_marks() # self.bottom_image.image_syllable_marks(self.syllable_onsets, # self.syllable_offsets) def image_binary_initial(self): # make plot take up the entire space self.ax2.clear() self.ax2.set_axis_off() data = np.zeros((self.song.rows, self.song.cols)) # plot data self.plot_binary = self.ax2.imshow( np.log(data + 3), cmap='hot', extent=[0, self.song.cols, 0, self.song.rows], aspect='auto') self.trans = tx.blended_transform_factory(self.ax2.transData, self.ax2.transAxes) self.lines_on, = self.ax2.plot(np.repeat(0, 3), np.tile([0, .75, np.nan], 1), linewidth=0.75, color='#2BB34B', transform=self.trans) self.lines_off, = self.ax2.plot(np.repeat(0, 3), np.tile([0, .90, np.nan], 1), linewidth=0.75, color='#2BB34B', transform=self.trans) hundred_ms_in_pix = 100 / self.song.ms_pix scalebar = AnchoredSizeBar(self.ax2.transData, hundred_ms_in_pix, '100 ms', 1, pad=0.1, color='white', frameon=False, size_vertical=2) self.ax2.add_artist(scalebar) self.ids.graph_binary.clear_widgets() self.ids.graph_binary.add_widget(self.plot_binary_canvas) def image_binary(self): self.plot_binary.set_data(np.log(self.thresh_sonogram + 3)) self.plot_binary.autoscale() def image_syllable_marks(self): self.lines_on.set_xdata(np.repeat(self.syllable_onsets, 3)) self.lines_on.set_ydata( np.tile([0, .75, np.nan], len(self.syllable_onsets))) self.lines_off.set_xdata(np.repeat(self.syllable_offsets, 3)) self.lines_off.set_ydata( np.tile([0, .90, np.nan], len(self.syllable_offsets))) self.plot_binary_canvas.draw() def back(self): if self.i != 1: self.i -= 2 self.next() # called when the user hits submit # before saving it checks for errors with onsets and offsets def save(self): Logger.info("Adding {} to save dictionaries".format(self.current_file)) # check if there are no syllable lines at all if len(self.syllable_onsets) == 0 and len(self.syllable_offsets) == 0: check_sylls = popups.CheckForSyllablesPopup() check_sylls.open() # if there are lines, check that there are equal number of ons and offs elif len(self.syllable_onsets) != len(self.syllable_offsets): check_length = popups.CheckLengthPopup() check_length.len_onsets = str(len(self.syllable_onsets)) check_length.len_offsets = str(len(self.syllable_offsets)) check_length.open() # check that you start with onset and end with offset elif self.syllable_onsets[0] > self.syllable_offsets[0] or \ self.syllable_onsets[-1] > self.syllable_offsets[-1]: check_beginning_end = popups.CheckBeginningEndPopup() check_beginning_end.start_onset = not self.syllable_onsets[0] > \ self.syllable_offsets[0] check_beginning_end.end_offset = not self.syllable_onsets[-1] > \ self.syllable_offsets[-1] check_beginning_end.open() # check that onsets and offsets alternate else: combined_onsets_offsets = list(self.syllable_onsets) binary_list = [0] * len(self.syllable_onsets) for i in range(len(self.syllable_offsets)): insertion_pt = bisect_right(combined_onsets_offsets, self.syllable_offsets[i]) binary_list.insert(insertion_pt, 1) insort(combined_onsets_offsets, self.syllable_offsets[i]) if sum(binary_list[::2]) != 0 \ or sum(binary_list[1::2]) \ != len(binary_list) / 2: # using python slices check_order = popups.CheckOrderPopup() check_order.order = binary_list check_order.open() # passed all checks, now info can be stored/written for the song else: Logger.info("Saving {}".format(self.current_file)) self.save_parameters_all[ self.current_file] = self.song.save_dict() self.save_conversions_all[self.current_file] = { 'timeAxisConversion': self.song.ms_pix, 'freqAxisConversion': self.song.hertzPerPixel } self.save_syllables_all[self.current_file] = { 'Onsets': self.syllable_onsets.tolist(), 'Offsets': self.syllable_offsets.tolist() } filename_gzip = "{}/SegSyllsOutput_{}.gzip".format( self.output_path, self.file_names[self.i - 1]) dictionaries = [ self.save_parameters_all[self.current_file], self.save_syllables_all[self.current_file], { 'Sonogram': self.thresh_sonogram.tolist() }, self.save_conversions_all[self.current_file] ] save_gzip_pickle(filename_gzip, dictionaries) # remove from tossed list if file ends up being submitted if self.i - 1 in self.save_tossed: del self.save_tossed[self.i - 1] # write if last file otherwise go to next file if self.i == len(self.files): self.save_all_parameters() else: self.next() def save_all_parameters(self): Logger.info("Saving parameters") if self.save_parameters_all: df_parameters = pd.DataFrame.from_dict(self.save_parameters_all, orient='index') for r in df_parameters.BoutRange: # adjust bout ranges so that they do not include the padding of the # spectrogram (150 pixels each side), so user can convert # correctly using human-readable files r[:] = [x - 150 for x in r] if r[0] < 0: r[0] = 0 if r[-1] > (self.song.cols - 300): r[-1] = (self.song.cols - 300) df_parameters.index.name = 'FileName' df_parameters.to_csv(os.path.join( self.output_path, 'segmentedSyllables_parameters_all.txt'), sep="\t") df_syllables = pd.DataFrame.from_dict(self.save_syllables_all, orient='index') # adjust onsets and offests so that they do not include the padding of # the spectrogram (150 pixels each side), so user can convert # correctly using human-readable files for on, off in zip(df_syllables.Onsets, df_syllables.Offsets): on[:] = [x - 150 for x in on] off[:] = [y - 150 for y in off] df_syllables.index.name = 'FileName' df_syllables.to_csv(os.path.join( self.output_path, 'segmentedSyllables_syllables_all.txt'), sep="\t") df_conversions = pd.DataFrame.from_dict(self.save_conversions_all, orient='index') df_conversions.index.name = 'FileName' df_conversions.to_csv(os.path.join( self.output_path, 'segmentedSyllables_conversions_all.txt'), sep="\t") df_tossed = pd.DataFrame.from_dict(self.save_tossed, orient='index') df_tossed.to_csv(os.path.join(self.output_path, 'segmentedSyllables_tossed.txt'), sep="\t", index=False) else: df_tossed = pd.DataFrame.from_dict(self.save_tossed, orient='index') df_tossed.to_csv(os.path.join(self.output_path, 'segmentedSyllables_tossed.txt'), sep="\t", index=False) self.done_window() def play_song(self): self.song.sound.play() @staticmethod def done_window(): popups.DonePopup().open() @staticmethod def take_closest(myList, myNumber): """ Assumes myList is sorted. Returns index of closest value to myNumber. If two numbers are equally close, return the index of the smallest number. From: https://stackoverflow.com/questions/12141150/from-list-of -integers-get-number-closest-to-a-given-value """ pos = bisect_left(myList, myNumber) if pos == 0: return pos if pos == len(myList): return -1 before = myList[pos - 1] after = myList[pos] if after - myNumber < myNumber - before: return pos else: return pos - 1